乐趣区

关于postgresql:PostgreSQLK8s-性能优化记

本文作者蔡松露,是云猿生数据 CTO & 联结创始人,前阿里云数据库资深技术专家。目前负责云猿生数据产品研发工作,率领团队实现云原生数据库管理系统 KubeBlocks 的设计。在此文中,他对 PG on ECS(下文中以 ECS PG 代指)和 PG on K8s 两种计划做了性能比照,并提出了 PG on K8s 上的性能优化计划,以确保数据库在 K8s 上能满足用户对性能和稳定性的要求。

背景

近年来,很多企业的基础架构都有打算 all-in-K8s 的打算,心愿采纳基于 K8s 的数据库管控平台(如 KubeBlocks)作为自建 PostgreSQL 托管计划(下文以 KubeBlocks-PG 为例)。此外,数据库的容器化和 K8s 化是比拟新的话题,很多人对有状态利用上 K8s 抱有比拟大的狐疑态度,咱们心愿验证数据库在 K8s 上性能是否能满足生产要求。本文提供在私有云 ECS 上自建 PostgreSQL(下文中以 ECS PG 代指)和基于 K8s 的数据库管控平台作为自建 PostgreSQL 托管计划进行比照,并提出如何在 K8s 上优化 PG 性能的计划。

环境筹备

版本 CPU 内存 磁盘 网络 规格族 复制协定
ESC PG 12.14 16C 64G ESSD PL1 500G SLB 独占 主备异步
ApeCloud PG 12.14 16C 64G ESSD PL1 300G SLB 独占 主备异步
  1. 在云厂商托管的 ACK 服务上购买 k8s 集群并部署 KubeBlocks,网络模式采纳 Terway,Terway 生产进去的 Pod IP 为 VPC IP,保障一个 VPC 内的网络可达,简化了网络管理和利用开发的老本,node 的规格为 16C64G。
  2. 生产实例,一开始在独占的 node 上无奈生产出 16C64G 的规格,因为 kubelet 等 agent 还耗费局部资源,所以调低 request 和 limit 到 14C56G 后生产胜利。

应用 kubectl edit 编辑 pg cluster 的 resource spec,去掉对 request 和 limit 的限度,保障压测过程中能够应用到 16C CPU,buffers 设置为 16GB,创立 PG 实例。

kbcli cluster create --cluster-definition = postgresql

测试计划

Sysbench Read-intensive 测试:80% read + 20% write。

该测试场景读多写少,比拟靠近理论的生产场景。

第一轮压测:TPS 跌 0

从 ECS 压测机发动压测,通过 VPC IP 拜访 PG。

Threads Throughput Latency(ms)
ApeCloud PG ECS PG ApeCloud PG ECS PG
25 87264 91310 31.94 28.67
50 111063 140559 55.82 40.37
100 83032 159386 132.49 92.42
150 61865 140938 272.27 186.54
175 56487 134933 350.33 240.02

发现三个问题:

  1. CPU 无奈打满:从 ECS 压测 DB,DB 所在 node CPU 无奈压满。
  2. 并发衰减快:随着压测并发数回升,ApeCloud PG 性能衰减要比 ECS PG 快。
  3. TPS 间歇性跌 0:在压测的过程中经常出现间歇性的 TPS 跌 0(307s 开始)。

此时因为 client 和 server 端的 CPU 都无奈压满,所以狐疑是两头的网络链路有问题,尤其是狐疑 SLB 的规格是否达到下限,所以把 SLB 规格换成了 slb.s3.large 从新压测,ACK SLB 的默认规格是 slb.s2.small。

换成 slb.s3.large 之后持续压测,问题仍然存在。

第二轮压测:网络链路排查

针对 SLB 提早设计测试 case,应用 sysbench select 1 来模仿全链路网络提早,单纯的 ping 测试尽管也能反映局部网络提早,然而存在很多缺点,而且不能保障刺穿全链路,比方 SLB 设施对 ping 产生的 ICMP 报文会间接返回,导致 SLB 到 Pod 的后续链路无奈被探测到。

测试的发动端仍然是 ECS,测试场景为:

ECS->Pod IP 应用 VPC IP 拜访,网络可中转

ECS->SLB IP->Pod IP 两头多了一层 SLB

ECS-> ECS SLB IP ECS 默认在 PG 前端内置了一层 SLB

测试后果如下:

Threads Throughput Latency(ms)
ApeCloud PG ECS PG ApeCloud PG ECS PG
Pod IP SLB IP SLB IP Pod IP SLB IP SLB IP
25 107309 105298 92163 0.30 0.30 0.32

后果阐明 ACK 和 SLB 的网络都是失常的,性能稳定的概率不大,所以对 SLB 的狐疑根本能够排除。

第三轮压测:IO 带宽调整

还是依照第一轮打算进行压测,这次从系统分析动手定性分析,查看云监控的 ECS 主机监控图。

发现两个景象:

  1. 磁盘读写带宽达到了对应规格的瓶颈,ESSD 带宽和磁盘容量正相干,具体计算公式为:min{120+0.5* 容量, 350}。300GB 磁盘对应的带宽为 270MB,从监控上看根本达到了瓶颈。
  2. 通过排查日志发现,在 TPS 跌 0 的工夫点 CPU 使用率也有对应的上涨。

因为之前磁盘带宽达到了下限,所以针对 IO 带宽又加了一组测试,测试 500GB 磁盘的体现状况,500GB 磁盘对应的带宽为 min{120+0.5*500, 350} = 350MB,压测过程中发现在磁盘跑满的时候,CPU 仍然有锯齿状稳定,依据以往教训,这种抖动可能和 checkpoint 无关,然而也不至于到跌 0 的境地。

在一直减少磁盘带宽的过程中发现 TPS 跌 0 的景象失去缓解,因而针对这个发现一次性把磁盘带宽调到最高,换成 ESSD PL2 1TB 磁盘,对应带宽 620MB,从图上看抖动仍然存在,但失去很大缓解,CPU 使用率跌幅收窄。

再激进一点,间接降级到了 ESSD PL3 2TB,磁盘带宽达到 700MB。

TPS 跌 0 根本缓解,然而仍然有比拟大的抖动,TPS 从 2400 到 1400,跌幅差不多 40%,CPU 抖动幅度收窄但仍然存在(@8183s)。

这一轮测试的论断就是 IO 带宽对 CPU 和 TPS 的影响很大,随着 IO 带宽的减少抖动幅度一直缩小,TPS 跌 0 的问题隐没,然而即便 IO 带宽不做限度,TPS 仍然有 40% 的上涨抖动,在排除了硬件的瓶颈束缚之后,这种抖动只可能和 PG 自身无关。

第四轮压测:Checkpoint 与锁剖析

这次把眼光聚焦到 Checkpoint 上来,次要是把传导机制搞清楚,剖析 IO 限流是如何反馈到 Checkpoint 和事务的:

  1. PostgreSQL Checkpoint 为何比其余数据库冲击要大?之前也测了一下 MySQL,发现 MySQL 在做 Checkpoint 时抖动绝对要小很多。
  2. 即便 IO 限流,然而从监控看 IO 还是满的,事务不应该跌 0,是不是此时带宽都被 Checkpoint 占用了?为了更好地监控数据库和主机指标,关上 KubeBlocks 集成的 Node Exporter 监控。

再一次压测,发现跌 0 的时候有一次比拟大的内存回收,内存一次性被回收了 10GB,这个量有点大,在不开 Huge Page 的时候,一个 page frame 4KB,10GB 大略是 2.5MB 的 page 数量,大量 page 的遍历和回收对 os kernel page reclaim 模块会有很大的压力,而且在那个工夫点上 os 卡了几十秒,导致下面的过程也都 hang 住,这种回收个别和 dirty_background_ratio 设置不合理无关,具体原理不再赘述。

执行 sysctl -a | grep dirty_background_ratio,发现 vm.dirty_background_ratio = 10。

调整 background ratio 为 5%:sysctl -w vm.dirty_background_ratio=5。

这个调整会让一些脏掉的 page cache 尽早刷下去,这个比例设置之所以要害,和 PostgreSQL 的实现有很大关系,PostgreSQL 依赖 os page cache,与 Oracle、MySQL 这些数据库的 IO 架构不同。MySQL 应用 DirectIO,不依赖零碎 page cache,给内存治理模块带来的压力和反过来受到的影响会小很多,当然某些场景下 DirectIO 提早比写 buffer cache 会更大一些。

此时也开始关注 PostgreSQL 内核实现和日志,登录到 Pod 中,有如下发现:
一个 WAL 日志默认大小 16MB。
root@postgres-cluster-postgresql-0:/home/postgres/pgdata/pgroot/data/pg_wal# du -sh 0000000A000001F300000077 16M 0000000A000001F300000077

压测过程中,PostgreSQL 后盾过程会清理 pg_wal 目录下的 WAL 日志以腾出空间,通过 strace 发现最多一次删除了几百个文件,总计大小 12GB(日志中的工夫都要 +8 个时区,所以 5:42 对应北京工夫 13:42):

2023-05-18 05:42:42.352 GMT,,,129,,64657f66.81,134,,2023-05-18 01:29:10 GMT,,0,LOG,00000,"checkpoint complete: wrote 680117 buffers (32.4%); 0 WAL file(s) added, 788 removed, 0 recycled; write=238.224 s, sync=35.28 6 s, total=276.989 s; sync files=312, longest=1.348 s, average=0.114 s; distance=18756500 kB, estimate=19166525 kB",,,,,,,,,""2023-05-18 05:42:42.362 GMT,,,129,,64657f66.81,135,,2023-05-18 01:29:10 GMT,,0,LOG,00000,"checkpoint starting: wal",,,,,,,,,"" 2023-05-18 05:42:44.336 GMT,"sysbenchrole","pgbenchtest",65143,"::1:43962",6465928f.fe77,1157,"SELECT",2023-05-18 02:50:55 GMT,36/46849938,0,LOG,00000,"duration: 1533.532 ms execute sbstmt1641749330-465186528: SEL ECT c FROM sbtest46 WHERE id=$1","parameters: $1 ='948136'",,,,,,,,"" 2023-05-18 05:42:44.336 GMT,"sysbenchrole","pgbenchtest",65196,"::1:44028",6465928f.feac,1137,"UPDATE",2023-05-18 02:50:55 GMT,57/43973954,949436561,LOG,00000,"duration: 1533.785 ms execute sbstmt493865735-6481814 15: UPDATE sbtest51 SET k=k+1 WHERE id=$1","parameters: $1 ='996782'",,,,,,,,""

能够看到,做 Checkpoint 的一瞬间,cpu idle 就飙涨到了 80%(对应 TPS 根本跌 0)。

日志中局部事务的 duration 上涨到 1S+。

TPS 跌 0 也在 13:44:20 这个工夫点完结。

2023-05-18 05:44:20.693 GMT,"sysbenchrole","pgbenchtest",65145,"::1:43964",6465928f.fe79,1178,"SELECT",2023-05-18 02:50:55 GMT,48/45617265,0,LOG,00000,"duration: 1942.633 ms execute sbstmt-1652152656-473838068: SE LECT c FROM sbtest37 WHERE id=$1","parameters: $1 ='1007844'",,,,,,,,""

13:45:41 开始做 vacuum。

2023-05-18 05:45:41.512 GMT,,,87995,,646596d6.157bb,71,,2023-05-18 03:09:10 GMT,64/3879558,0,LOG,00000,"automatic aggressive vacuum of table""pgbenchtest.public.sbtest45"": index scans: 1 pages: 0 removed, 66886 remain, 0 skipped due to pins, 2328 skipped frozen tuples: 14166 removed, 2005943 remain, 15904 are dead but not yet removable, oldest xmin: 944519757

13:47:04 checkpoint 真正实现。

2023-05-18 05:47:04.920 GMT,,,129,,64657f66.81,136,,2023-05-18 01:29:10 GMT,,0,LOG,00000,"checkpoint complete: wrote 680483 buffers (32.4%); 0 WAL file(s) added, 753 removed, 0 recycled; write=226.176 s, sync=32.53

整个过程的监控图:

发现 CPU busy 抖动和 Checkpoint 刷脏过程根本吻合。

全过程磁盘带宽始终打满:

跌 0 的时间段和 checkpoint 刷脏时间段基本一致:

通过看内存的稳定状况,发现内存回收导致的 hang 根本被打消,阐明之前 dirty_background_ratio 的参数调整无效。

此外还发现,在刷脏过程中,锁的数量始终比拟高,与非刷脏状态下的比照非常明显:

具体的锁有:

有时多个过程会抢同一把锁:

而且发现平时做 IO 的时候,磁盘带宽尽管会打满,然而事务之间很少抢锁,TPS 也不会跌 0,当锁竞争比拟显著的时候,就很容易跌 0,而锁的竞争又和 Checkpoint 间接相干。

第五轮压测:PG 内核代码剖析与 trace

持续从 Checkpoint 实现动手剖析跌 0 起因,浏览了大量 PostgreSQL Checkpoint 和 WAL 局部的代码实现,并对 PostgreSQL backend 过程进行 Trace,发现 WAL 日志创立存在问题,其中的 duration 数据是通过脚本剖析日志计算得出的:

duration:550 ms 11:50:03.951036 openat(AT_FDCWD, "pg_wal/archive_status/00000010000002EE000000E7.ready", O_WRONLY|O_CREAT|O_TRUNC, 0666) = 22 
duration:674 ms 11:50:09.733902 openat(AT_FDCWD, "pg_wal/archive_status/00000010000002EF00000003.ready", O_WRONLY|O_CREAT|O_TRUNC, 0666) = 22 
duration:501 ms 11:50:25.263054 openat(AT_FDCWD, "pg_wal/archive_status/00000010000002EF0000004B.ready", O_WRONLY|O_CREAT|O_TRUNC, 0666) = 23 
duration:609 ms 11:50:47.875338 openat(AT_FDCWD, "pg_wal/archive_status/00000010000002EF000000A8.ready", O_WRONLY|O_CREAT|O_TRUNC, 0666) = 25 
duration:988 ms 11:50:53.596897 openat(AT_FDCWD, "pg_wal/archive_status/00000010000002EF000000BD.ready", O_WRONLY|O_CREAT|O_TRUNC, 0666) = 29 
duration:1119 ms 11:51:10.987796 openat(AT_FDCWD, "pg_wal/archive_status/00000010000002EF000000F6.ready", O_WRONLY|O_CREAT|O_TRUNC, 0666) = 29 
duration:1442 ms 11:51:42.425118 openat(AT_FDCWD, "pg_wal/archive_status/00000010000002F000000059.ready", O_WRONLY|O_CREAT|O_TRUNC, 0666) = 45 
duration:1083 ms 11:51:52.186613 openat(AT_FDCWD, "pg_wal/archive_status/00000010000002F000000071.ready", O_WRONLY|O_CREAT|O_TRUNC, 0666) = 51 
duration:503 ms 11:52:32.879828 openat(AT_FDCWD, "pg_wal/archive_status/00000010000002F0000000D8.ready", O_WRONLY|O_CREAT|O_TRUNC, 0666) = 75 
duration:541 ms 11:52:43.078011 openat(AT_FDCWD, "pg_wal/archive_status/00000010000002F0000000EB.ready", O_WRONLY|O_CREAT|O_TRUNC, 0666) = 84 
duration:1547 ms 11:52:56.286199 openat(AT_FDCWD, "pg_wal/archive_status/00000010000002F10000000C.ready", O_WRONLY|O_CREAT|O_TRUNC, 0666) = 84 
duration:1773 ms 11:53:19.821761 openat(AT_FDCWD, "pg_wal/archive_status/00000010000002F10000003D.ready", O_WRONLY|O_CREAT|O_TRUNC, 0666) = 94 
duration:2676 ms 11:53:30.398228 openat(AT_FDCWD, "pg_wal/archive_status/00000010000002F10000004F.ready", O_WRONLY|O_CREAT|O_TRUNC, 0666) = 101 
duration:2666 ms 11:54:05.693044 openat(AT_FDCWD, "pg_wal/archive_status/00000010000002F100000090.ready", O_WRONLY|O_CREAT|O_TRUNC, 0666) = 122 
duration:658 ms 11:54:55.267889 openat(AT_FDCWD, "pg_wal/archive_status/00000010000002F1000000E5.ready", O_WRONLY|O_CREAT|O_TRUNC, 0666) = 139 
duration:933 ms 11:55:37.229660 openat(AT_FDCWD, "pg_wal/archive_status/00000010000002F200000025.ready", O_WRONLY|O_CREAT|O_TRUNC, 0666) = 163 
duration:2681 ms 11:57:02.550339 openat(AT_FDCWD, "pg_wal/archive_status/00000010000002F200000093.ready", O_WRONLY|O_CREAT|O_TRUNC, 0666) = 197

这几个 WAL 日志文件从开始创立到 ready 须要 500ms 以上,有的甚至到了 2.6S,这也是咱们观测到有些事务 duration 大于 2S 的起因,因为事务要挂起期待 WAL 文件 ready 能力持续写入。

WAL 创立的具体流程:

  1. stat(pg_wal/00000010000002F200000093) 找不到文件
  2. 应用 pg_wal/xlogtemp.129 来创立
  3. 清零 pg_wal/xlogtemp.129
  4. 建设软连贯 link(“pg_wal/xlogtemp.129”, “pg_wal/00000010000002F200000093”)
  5. 关上 pg_wal/00000010000002F200000093
  6. 在尾部写入元数据
  7. 加载并利用该 WAL 文件

查看 PostgreSQL 日志发现,那个时刻客户端有链接被重置,有的事务执行超过 10s。

2023-05-22 11:56:08.355 GMT,,,442907,"100.127.12.1:23928",646b5858.6c21b,1,"",2023-05-22 11:56:08 GMT,,0,LOG,08006,"could not receive data from client: Connection reset by peer",,,,,,,,,"" 2023-05-22 11:56:10.427 GMT,,,442925,"100.127.12.1:38942",646b585a.6c22d,1,"",2023-05-22 11:56:10 GMT,,0,LOG,08006,"could not receive data from client: Connection reset by peer",,,,,,,,,"" 2023-05-22 11:56:12.118 GMT,,,442932,"100.127.13.2:41985",646b585c.6c234,1,"",2023-05-22 11:56:12 GMT,,0,LOG,08006,"could not receive data from client: Connection reset by peer",,,,,,,,,"" 2023-05-22 11:56:13.401 GMT,"postgres","pgbenchtest",3549,"::1:45862",646ae5d3.ddd,3430,"UPDATE waiting",2023-05-22 03:47:31 GMT,15/95980531,1420084298,LOG,00000,"process 3549 still waiting for ShareLock on transac tion 1420065380 after 1000.051 ms","Process holding the lock: 3588. Wait queue: 3549.",,,,"while updating tuple (60702,39) in relation""sbtest44""","UPDATE sbtest44 SET k=k+1 WHERE id=$1",,,""

通过比照日志发现每次 WAL segment 耗时较长时,客户端就会产生一批慢查问(>1s)日志
PG 内核中清零的具体实现为:

    /* do not use get_sync_bit() here --- want to fsync only at end of fill */
    fd = BasicOpenFile(tmppath, open_flags);
    if (fd < 0)
        ereport(ERROR,
                (errcode_for_file_access(),
                 errmsg("could not create file \"%s\": %m", tmppath)));

    pgstat_report_wait_start(WAIT_EVENT_WAL_INIT_WRITE);
    save_errno = 0;
    if (wal_init_zero)
    {
        ssize_t        rc;

        /*
         * Zero-fill the file.  With this setting, we do this the hard way to
         * ensure that all the file space has really been allocated.  On
         * platforms that allow "holes" in files, just seeking to the end
         * doesn't allocate intermediate space.  This way, we know that we
         * have all the space and (after the fsync below) that all the
         * indirect blocks are down on disk.  Therefore, fdatasync(2) or
         * O_DSYNC will be sufficient to sync future writes to the log file.
         */
        rc = pg_pwrite_zeros(fd, wal_segment_size, 0); // buffer write

        if (rc < 0)
            save_errno = errno;
    }
    else
    {
        /*
         * Otherwise, seeking to the end and writing a solitary byte is
         * enough.
         */
        errno = 0;
        if (pg_pwrite(fd, "\0", 1, wal_segment_size - 1) != 1)
        {
            /* if write didn't set errno, assume no disk space */
            save_errno = errno ? errno : ENOSPC;
        }
    }
    pgstat_report_wait_end();

    if (save_errno)
    {
        /*
         * If we fail to make the file, delete it to release disk space
         */
        unlink(tmppath);

        close(fd);

        errno = save_errno;

        ereport(ERROR,
                (errcode_for_file_access(),
                 errmsg("could not write to file \"%s\": %m", tmppath)));
    }

    pgstat_report_wait_start(WAIT_EVENT_WAL_INIT_SYNC);
    if (pg_fsync(fd) != 0)  // fsync data to disk
    {
        save_errno = errno;
        close(fd);
        errno = save_errno;
        ereport(ERROR,
                (errcode_for_file_access(),
                 errmsg("could not fsync file \"%s\": %m", tmppath)));
    }
    pgstat_report_wait_end();

从代码中能够看出 WAL 清零操作是先做异步写,每次写一个 page block,直到循环写完,而后再一次性做 fsync,异步写个别很快,当零碎负载很低的时候,异步写 8KB 的数据响应工夫是 us 级别,当零碎负载比拟重的时候,一个异步 IO 提早甚至能达到 30ms+,异步写时延变长和 os kernel 的 io path 有很大关系,当内存压力大时,异步写可能会被 os 转成同步写,而且 IO 过程和 page reclaim 的 slowpath 交错在一起,所以实践上就有可能耗时很久,在理论 trace 中也的确如此。上面是监测到的紧邻的两次 WAL 清零 IO 操作,能够看到两次异步 IO 操作的距离达到了 30ms+。

11:56:57.238340 write(3, "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"..., 8192) = 8192 11:56:57.271551 write(3, "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"..., 8192) = 8192

过后的磁盘带宽:

咱们能够测算一下,对于一个 16MB 的 WAL segment,须要 2K 次清零操作,如果每次操作耗时 1ms,那么须要至多 2s 来能实现整体清零。

以某个正在执行的事务为例子:

#trace 一个正在执行事务的 PostgreSQL Backend 过程,两头等锁耗时 1.5s
02:27:52.868356 recvfrom(10, "*\0c\304$Es\200\332\2130}\32S\250l\36\202H\261\243duD\344\321p\335\344\241\312/"..., 92, 0, NULL, NULL) = 92 
02:27:52.868409 getrusage(RUSAGE_SELF, {ru_utime={tv_sec=232, tv_usec=765624}, ru_stime={tv_sec=59, tv_usec=963504}, ...}) = 0 
02:27:52.868508 futex(0x7f55bebf9e38, FUTEX_WAIT_BITSET|FUTEX_CLOCK_REALTIME, 0, NULL, FUTEX_BITSET_MATCH_ANY) = 0 
02:27:54.211960 futex(0x7f55bebfa238, FUTEX_WAKE, 1) = 1 
02:27:54.215049 write(2, "\0\0\36\1\377\334\23\0T2023-05-23 02:27:54.215"..., 295) = 295 
02:27:54.215462 getrusage(RUSAGE_SELF, {ru_utime={tv_sec=232, tv_usec=765773}, ru_stime={tv_sec=59, tv_usec=963504}, ...}) = 0 

对应的 SQL 是:

2023-05-23 02:27:54.215 GMT,"postgres","pgbenchtest",1301759,"::1:56066",646c1ef3.13dcff,58,"SELECT",2023-05-23 02:03:31 GMT,43/198458539,0,LOG,00000,"duration: 1346.558 ms execute sbstmt-13047857631771152290: SEL ECT c FROM sbtest39 WHERE id=$1","parameters: $1 ='1001713'",,,,,,,,""

至此根本能够确定 Checkpoint 时 TPS 跌 0、CPU 抖动和 WAL 清零无关,具体传导机制是:
WAL 创立 ->WAL 清零 -> 刷脏和清零操作 IO 争抢 -> 事务期待变长 -> 持有锁工夫变长 -> 被梗塞的事务过程越来越多 -> 事务大面积超时。

清零的最大问题是会产生大量 IO,并且须要所有事务挂起期待清零数据 sync 实现,直到新的 WAL 文件 ready,在这个过程中所有事务都要期待 WALWrite 和 wal_insert 锁,这是抖动的最大本源。不过问题的实质还是 IO 争抢,如果 IO 负载很低,清零速度比拟快,观测到的抖动也不显著,问题也不会裸露,目前观测到的激烈抖动也只呈现在压测过程中,所以后面几轮测试中放大 IO 带宽也有助于缓解 TPS 跌 0 和 CPU 抖动。

因为在创立新的 WAL 文件的时候须要加锁,所以通过调整 WAL 文件大小来升高加锁的频率也是优化方向之一。

第六轮压测:敞开 wal_init_zero

问题定位后,解决方案也就比拟好找了,WAL 日志清零和判断 WAL 日志槽是否失常无关,实质上是一种不良好但比拟省力的实现,最好的解决方案应该是 WAL 日志能自解释,不依赖清零来保障正确性,这种计划须要批改 PG 内核,所以不大事实;还有一种计划是尽管还须要清零,然而能够由文件系统来实现,不须要 PG 内核显式调用,当然这须要文件系统反对该清零个性。

[ZFS 和 XFS 正好具备这个个性](
https://www.reddit.com/r/bcachefs/comments/fhws6h/the_state_o… “ZFS 和 XFS 正好具备这个个性 ”)。咱们以后测试应用的 EXT4 并不具备这个个性,所以咱们先尝试把文件系统改为 ZFS。

然而在测试 ZFS 的过程中,发现了好几次文件系统挂起的状况:

root@pgclusterzfs-postgresql-0:~# cat /proc/4328/stack

[<0>] zil_commit_impl+0x105/0x650 [zfs] 

[<0>] zfs_fsync+0x71/0xf0 [zfs] 

[<0>] zpl_fsync+0x63/0x90 [zfs] 

[<0>] do_fsync+0x38/0x60 

[<0>] __x64_sys_fsync+0x10/0x20 

[<0>] do_syscall_64+0x5b/0x1d0 

[<0>] entry_SYSCALL_64_after_hwframe+0x44/0xa9 

[<0>] 0xffffffffffffffff

因而基于稳定性的思考,ZFS 被临时搁置,转而采纳 XFS,并 set wal_init_zero = OFF,同时为了升高 WAL 日志文件创建的频率,咱们把 wal_segment_size 从 16MB 调整到了 1GB,这样加锁频率也会升高。

通过测试,跌 0 和 CPU 抖动缓解很显著:

尽管打消清零操作和升高加锁频率能解决局部抖动问题,然而因为 Checkpoint 时刷脏和事务写 WAL 日志仍然会抢带宽、抢锁,所以在 Checkpoint 时抖动仍然存在,只是和之前相比有了很大的缓解,所以如果再持续优化,只能从升高单个事务的 IO 量上动手。

为了数据安全思考,之前的压测都开启了 full_page_write,该个性用来保障断电时 page block 数据损坏场景下的数据恢复,具体原理能够参考《PG. 个性剖析.full page write 机制》http://mysql.taobao.org/monthly/2015/11/05/,如果存储能保障原子写(不会呈现局部胜利、局部失败的状况)或 PG 能从某个备份集中复原(正确的全量数据 + 增量 WAL 回放),那么在不影响数据安全的前提下能够尝试敞开 full_page_write。

第七轮压测:敞开 full_page_write

敞开 full_page_write 前后 CPU 和 IO 带宽比照都非常明显:

能够看出 IO 争抢对 PG 的影响很大,而且在敞开 full_page_write 之后即便有 Checkpoint,CPU 也简直没有抖动。

又加测了三种场景:

  1. 开启 full_page_write+16MB WAL segment size;
  2. 开启 full_page_write+1GB WAL segment size;
  3. 敞开 full_page_write+1GB WAL segment size。

能够看出,在开启 full_page_write 时 1GB segment 比 16MB segment 体现要略好,也印证了通过减少 segment size 升高加锁频率的计划可行;敞开 full_page_write 后 PG 体现十分顺滑。

所以最终抉择了一组 (wal_init_zero off + XFS) + (full_page_write off) + (wal_segment_size 1GB) 的组合测试,成果如下:

能够看到在 Checkpoint 时抖动隐没,零碎十分顺滑,PG 也从 IO-Bound 变成了 CPU-Bound,此时的瓶颈应该在 PG 的外部锁机制上。

第八轮压测:最终性能比照

不过依据以往的教训,PG 因为是过程模型,一个会话对应一个过程,当并发数比拟高的时候,页表和过程上下文切换的代价会比拟高,所以又引入了 pgBouncer;用户自建 ECS PG 为了解决并发问题,开启了 Huge Page,ApeCloud PG 因为部署在 ACK 上,所以没有开启 Huge Page。

比照时为了偏心,ApeCloud 在上面的测试中开启了 full_page_write。

能够看出在引入 pgBouncer 之后,PG 可能承载更多的链接数而不会引起性能进化,ApeCloud PG 比 PG 在性能上相差不大,在并发数比拟低的时候性能上略好一些,整体稳定性上会更好一些。

论断

  1. WAL 清零对 PG 的性能和稳定性都有比拟大的影响,如果文件系统反对清零个性,能够敞开 wal_init_zero 选项,可无效升高 CPU 和 TPS 抖动。
  2. full_page_write 对 PG 的性能和稳定性也有比拟大的影响,如果能从存储或备份上能保证数据的安全性,能够思考敞开,可无效升高 CPU 和 TPS 抖动。
  3. 减少 WAL segment size 大小,可升高日志轮转过程中加锁的频率,也能够升高 CPU 和 TPS 抖动,只是成果没那么显著。
  4. PG 是多过程模型,引入 pgBouncer 可反对更大的并发链接数,并大幅晋升稳定性,如果条件容许,能够开启 Huge Page,尽管原理不同,但成果和 pgBouncer 相似。
  5. PG 在默认参数下,属于 IO-Bound,在通过上述优化后转化为 CPU-Bound。
  6. ACK 和 SLB 网络实现比拟强壮,性能和稳定性上都满足要求。
  7. 在 K8s 上对文件系统、PG 参数等选项的调整十分不便,能够疾速无效进行不同的组合测试,而且数据库跑在 K8s 上不会带来性能上的损耗,在做过通用调优之后能够达到很好的成果,对用户来说限度更少,有更强的自主性。
退出移动版