PostgreSQL@K8s 性能优化记
本文作者蔡松露,是云猿生数据 CTO & 联合创始人,前阿里云数据库资深技术专家。目前负责云猿生数据产品研发工作,带领团队完成云原生数据库管理系统 KubeBlocks 的设计。在此文中,他对 PG on ECS(下文中以 ECS PG 代指)和 PG on K8s 两种方案做了性能对比,并提出了 PG on K8s 上的性能优化方案,以确保数据库在 K8s 上能满足用户对性能和稳定性的要求。
近年来,很多企业的基础架构都有计划 all-in-K8s 的计划,希望采用基于 K8s 的数据库管控平台(如 KubeBlocks[1]——《给你介绍一个有趣的开源项目 - 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 | 独占 | 主备异步 |
KubeBlocks PG | 12.14 | 16C | 64G | ESSD PL1 300G | SLB | 独占 | 主备异步 |
在云厂商托管的 ACK 服务上购买 K8s 集群并部署 KubeBlocks[2],网络模式采用 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。
该测试场景读多写少,比较接近实际的生产场景。
从 ECS 压测机发起压测,通过 VPC IP 访问 PG。
Threads | Throughput | Latency (ms) | ||
KubeBlocks PG | ECS PG | KubeBlocks 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 无法压满。 并发衰减快:随着压测并发数上升,KubeBlocks 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 访问,网络可直达。 ECS -> SLB IP -> Pod IP 中间多了一层 SLB。
ECS -> ECS SLB IP ECS 默认在 PG 前端内置了一层 SLB。
测试结果如下:
Threads | Throughput | Latency (ms) | ||||
KubeBlocks PG | ECS PG | KubeBlocks 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 的怀疑基本可以排除。
还是按照第一轮计划进行压测,这次从系统分析入手定性分析,查看云监控的 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 上来,主要是把传导机制搞清楚,分析 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)。
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 刷脏时间段基本一致:
此外还发现,在刷脏过程中,锁的数量一直比较高,与非刷脏状态下的对比非常明显:
具体的锁有:
有时多个进程会抢同一把锁:
而且发现平时做 IO 的时候,磁盘带宽虽然会打满,但是事务之间很少抢锁,TPS 也不会跌 0,当锁竞争比较明显的时候,就很容易跌0,而锁的竞争又和 Checkpoint 直接相关。
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
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 日志清零和判断 WAL 日志槽是否正常有关,本质上是一种不良好但比较省力的实现,最好的解决方案应该是 WAL 日志能自解释,不依赖清零来保证正确性,这种方案需要修改 PG 内核,所以不大现实;还有一种方案是虽然还需要清零,但是可以由文件系统来完成,不需要 PG 内核显式调用,当然这需要文件系统支持该清零特性。
ZFS 和 XFS 正好具备这个特性[3] 。我们当前测试使用的 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 前后 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,KubeBlocks PG 因为部署在 ACK 上,所以没有开启 Huge Page。
对比时为了公平,KubeBlocks 在下面的测试中开启了 full_page_write。
可以看出在引入 pgBouncer 之后,PG 能够承载更多的链接数而不会引起性能退化,KubeBlocks 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 上不会带来性能上的损耗,在做过通用调优之后可以达到很好的效果,对用户来说限制更少,有更强的自主性。
参考资料
[1]KubeBlocks: https://github.com/apecloud/kubeblocks
[2]部署 KubeBlocks: https://kubeblocks.io/docs/preview/user_docs/installation/install-with-kbcli/install-kbcli
[3]ZFS 和 XFS 正好具备这个特性: https://www.reddit.com/r/bcachefs/comments/fhws6h/the_state_of_linux_cow_file_systems_what_to_choose/
戳「阅读全文」 Star KubeBlocks 🌟