本文作者蔡松露,是云猿生数据 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 | 独占 | 主备异步 |
- 在云厂商托管的ACK服务上购买k8s集群并部署KubeBlocks,网络模式采用Terway,Terway生产出来的Pod IP为VPC IP,保证一个VPC内的网络可达,简化了网络管理和应用开发的成本,node的规格为16C64G。
- 生产实例,一开始在独占的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 |
发现三个问题:
- CPU无法打满:从ECS压测DB,DB所在node CPU无法压满。
- 并发衰减快:随着压测并发数上升,ApeCloud PG性能衰减要比ECS PG快。
- 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主机监控图。
发现两个现象:
- 磁盘读写带宽达到了对应规格的瓶颈,ESSD带宽和磁盘容量正相关,具体计算公式为: min{120+0.5*容量, 350}。300GB磁盘对应的带宽为270MB,从监控上看基本达到了瓶颈。
- 通过排查日志发现,在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和事务的:
- PostgreSQL Checkpoint为何比其他数据库冲击要大?之前也测了一下MySQL,发现MySQL在做Checkpoint时抖动相对要小很多。
- 即使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 创建的具体流程:
- stat(pg_wal/00000010000002F200000093)找不到文件
- 使用pg_wal/xlogtemp.129来创建
- 清零pg_wal/xlogtemp.129
- 建立软连接link("pg_wal/xlogtemp.129", "pg_wal/00000010000002F200000093")
- 打开pg_wal/00000010000002F200000093
- 在尾部写入元数据
- 加载并应用该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也几乎没有抖动。
又加测了三种场景:
- 开启full_page_write+16MB WAL segment size;
- 开启full_page_write+1GB WAL segment size;
- 关闭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在性能上相差不大,在并发数比较低的时候性能上略好一些,整体稳定性上会更好一些。
结论
- WAL清零对PG的性能和稳定性都有比较大的影响,如果文件系统支持清零特性,可以关闭wal_init_zero选项,可有效降低CPU和TPS抖动。
- full_page_write 对PG的性能和稳定性也有比较大的影响,如果能从存储或备份上能保证数据的安全性,可以考虑关闭,可有效降低CPU和TPS抖动。
- 增加WAL segment size大小,可降低日志轮转过程中加锁的频率,也可以降低CPU和TPS抖动,只是效果没那么明显。
- PG是多进程模型,引入pgBouncer可支持更大的并发链接数,并大幅提升稳定性,如果条件允许,可以开启Huge Page,虽然原理不同,但效果和pgBouncer类似。
- PG在默认参数下,属于IO-Bound,在经过上述优化后转化为CPU-Bound。
- ACK和SLB网络实现比较健壮,性能和稳定性上都满足要求。
- 在K8s上对文件系统、PG参数等选项的调整非常方便,可以快速有效进行不同的组合测试,而且数据库跑在K8s上不会带来性能上的损耗,在做过通用调优之后可以达到很好的效果,对用户来说限制更少,有更强的自主性。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。