本文作者蔡松露,是云猿生数据 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 上不会带来性能上的损耗,在做过通用调优之后能够达到很好的成果,对用户来说限度更少,有更强的自主性。