头图
本文作者蔡松露,是云猿生数据 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 PG12.1416C64GESSD PL1 500GSLB独占主备异步
ApeCloud PG12.1416C64GESSD PL1 300GSLB独占主备异步
  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 ThroughputLatency(ms)
ApeCloud PGECS PGApeCloud PGECS PG
25872649131031.9428.67
5011106314055955.8240.37
10083032159386132.4992.42
15061865140938272.27186.54
17556487134933350.33240.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

测试结果如下:

ThreadsThroughputLatency(ms)
ApeCloud PGECS PGApeCloud PGECS PG
Pod IPSLB IPSLB IPPod IPSLB IPSLB IP
25107309105298921630.300.300.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上不会带来性能上的损耗,在做过通用调优之后可以达到很好的效果,对用户来说限制更少,有更强的自主性。

小猿姐
6 声望3 粉丝

每个开发者都想知道的云原生和数据库技术