上一篇把 Checkpointer、BgWriter 和 WAL Writer 放在一起看了一遍。
写到这里,这个 PostgreSQL 内核系列稍微拐个弯。
前面的文章大多在追一个机制:PostgreSQL 为什么这样设计,状态怎么流转,Crash 之后又怎么把数据接回来。这一篇换个视角,看看前面讲过的这些内核机制到了真实系统里会表现成什么问题,又能怎么指导调优。
Checkpoint 很适合做这个尝试。
它一头连着 Dirty Page、WAL 和 Recovery,另一头又直接出现在 DBA 经常遇到的问题里:
周期性的 I/O 尖峰
P99 延迟抖动
WAL 突然变多
pg_wal 越来越大
Crash Recovery 太慢
所以这篇不打算把几个参数挨个解释一遍,也不继续沿源码调用链往下走。
还是从一个实际问题开始。
假设某天下午,监控变成了这样:
P99 Latency
/\
/ \
-------------------/ \----------------
/\
/ \
--------------------------------/ \----
Disk Write
███████
-----------------███████-----------------
███████
------------------------------███████-----
差不多每隔几分钟来一次。
翻一下日志,时间又刚好和 Checkpoint 对得上。
这时几个参数很容易一起冒出来:
checkpoint_timeout
max_wal_size
checkpoint_completion_target
checkpoint_flush_after
到底该动哪个?
我现在更习惯先看:这次 Checkpoint 的成本到底落在哪一段。
位置找对了,参数反而好理解。
来得太勤,还是每次太重?
Checkpoint 和延迟毛刺同时出现,先把两种情况分开:
Checkpoint
|
+-----------+-----------+
| |
v v
来得太频繁? 每次都太重?
| |
v v
为什么总触发? 慢在哪里?
如果每轮都很平滑,只是几分钟就来一次,先查触发频率。
如果半小时才跑一次,但每次一来磁盘就打满、P99 就抖,那就跟着这一轮 Checkpoint 往下看。
两种现象最后都可能被归结为“Checkpoint 影响性能”,处理方式却完全不同。
先看频率。
为什么 Checkpoint 总来?
自动 Checkpoint 最常见的两个触发来源,是时间和 WAL。
时间这边比较直接:
时间到了
|
v
checkpoint_timeout
|
v
Checkpoint
另外一边是 WAL:
WAL 持续产生
|
v
可用的 WAL 预算越来越紧
|
v
Checkpoint
所以发现 Checkpoint 很频繁时,我一般不会先改参数,而是先看看这段时间到底发生了什么。
PostgreSQL 17 才开始有独立的 pg_stat_checkpointer;num_done 是 PG18 才加的。下面以 PG18 为例:
SELECT
num_timed,
num_requested,
num_done,
write_time,
sync_time,
buffers_written
FROM pg_stat_checkpointer;
先看几个计数:
num_timed
因 checkpoint_timeout 调度的 Checkpoint
num_requested
由请求触发的 Checkpoint
(WAL 压力、手工 CHECKPOINT 等)
num_done
真正执行完成的 Checkpoint
num_timed 和 num_requested 把完成的、跳过的都算了进去,num_done 才是真正执行的次数。num_timed + num_requested 和 num_done 对不上,说明有些 Checkpoint 最后被跳过,并没有真正执行。典型的情况是系统自上一轮以来一直空闲。
如果长期主要是 num_timed 在增长,checkpoint_timeout 自然值得关注。
如果 num_requested 增长很快,就要继续找是谁在请求:WAL 超过 max_wal_size 会算在这里,手工执行 CHECKPOINT 也会。所以 num_requested 多,并不能直接等同于 max_wal_size 太小。
不过在持续写入的数据库里,WAL 压力通常更值得优先排查。
接下来更值得问的是:
当前 workload 到底以多快的速度在产生 WAL?
4GB 的 max_wal_size,到底算大还是小?
假设两套数据库都配了 max_wal_size = 4GB:
系统 A: 5 MB WAL/s
系统 B: 200 MB WAL/s
同样的 4GB,意义完全不同:
4GB WAL 预算
系统 A
|------------------------------------------|
系统 B
|-----|
很快就吃完
PostgreSQL 内部当然不是简单地拿 max_wal_size / WAL 生成速率 来决定下一次 Checkpoint。
但这个除法很适合建立容量上的直觉。
它把“max_wal_size 到底应该配多大”这个难回答的问题,换成了另一个更容易讨论的问题:
按照当前 WAL 的生成速度,这份 WAL 预算大概能撑多久?
平时系统可能只有 10MB/s WAL。
来一次批量 COPY、大批量 UPDATE 或数据导入,WAL 生成速率突然升到 300MB/s,同样的配置就可能表现得完全不同。
如果日志里还频繁出现 Checkpoint 过近的告警,max_wal_size 和当前 workload 是否匹配就更值得检查:
LOG: checkpoints are occurring too frequently (18 seconds apart)
HINT: Consider increasing the configuration parameter "max_wal_size".
到这里,频率问题大概可以收成这样一张图:
Checkpoint 太频繁
|
+--> 时间主导
| |
| v
| checkpoint_timeout
|
+--> WAL / request 主导
|
v
WAL 生成速率
max_wal_size
其他 request 来源
如果 Checkpoint 次数其实没什么异常,那就该看另一边了:
为什么每一轮都这么重?
跟着一次 Checkpoint 往下走
上一篇已经把 Online Checkpoint 的机制讲过了。
调优时不用再把完整源码重新过一遍,只保留和性能有关的部分:
确定本轮 redo point
|
v
+-------------------+
| write phase |
| |
| 写本轮 Dirty Page |
+-------------------+
|
v
+-------------------+
| sync phase |
| |
| 确保文件 durable |
+-------------------+
|
v
checkpoint record
pg_control 更新
|
v
新的 Recovery 边界正式可用
所以“Checkpoint 很慢”还能继续拆:是 write 慢,还是 sync 慢?
后面的分析,就从这里分岔。
第一站:短时间是不是写了太多 Page?
假设这一轮最后要写 10GB Dirty Page。
如果用了 200 秒,平均就是 10GB / 200s ≈ 51MB/s,磁盘看到的大概是:
▂▂▂▂▂▂▂▂▂▂▂▂▂▂▂▂▂▂▂
-------------------------------->
如果集中在 20 秒,平均会到 10GB / 20s ≈ 512MB/s,就更像:
████████
------------████████-------------------->
最终写出去的数据量没有变化。
差别在于,同样一批 I/O 被摊在了多长的时间里。
这也是理解 checkpoint_completion_target 比较直观的方式。
completion_target:别急着把这一轮写完
Checkpoint 开始以后,PostgreSQL 不会看到一批 Dirty Page 就按照磁盘极限速度一口气写完。
Checkpoint write 本身有 pacing。
简单画一下:
当前 Checkpoint 下一次
|------------------------------------------|
^ ^
| |
开始 目标完成附近
<------- 尽量把写入摊开 ------->
checkpoint_completion_target 控制的,就是尽量利用 Checkpoint interval 的多大一部分来完成写入,默认值是 0.9。
如果把完成时间压得很短:
▂▂▂▂▂▂▂▂▂▂
就会逐渐变成:
████████
Checkpoint 确实更早结束了,但 Checkpointer 和前台业务也更容易在短时间内争抢存储。
所以看到“Checkpoint 一来,Disk Write 就冲高”,我会先把三个量放在一起:
这一轮写了多少?
|
v
buffers_written
用了多久?
|
v
write_time
底层存储稳定能写多少?
|
v
Storage Throughput
比如一轮 Checkpoint 写了 12GB、持续 180 秒,平均也就六七十 MB/s。如果底层设备稳定能跑几百 MB/s,这个数字本身未必有什么问题。
反过来,如果同样 12GB 几十秒就写完,而存储稳定写能力只有 200MB/s,业务本身还在大量做 I/O,延迟被一起带起来就很正常了。
这里也能看到 checkpoint_completion_target 的边界:
它可以调整 I/O 的节奏,不能增加磁盘带宽。
存储已经长期接近饱和时,再怎么微调 Checkpoint pacing,也很难解决根因。
第二站:write 很平滑,为什么最后还是卡一下?
再换一种情况。
整个 Checkpoint 期间:
Disk Write
▂▂▂▂▂▂▂▂▂▂▂▂▂▂▂▂
----------------------------->
看起来都正常。
可到了尾部:
P99 Latency
█████
-------------------------█████
这时就该往后看 sync phase 了。
上一篇已经讲过,write() 和 durable 之间还有距离。
很多时候 Data Page write 先到的是操作系统:
Shared Buffers
|
v
write()
|
v
OS Page Cache
从 Shared Buffers 的角度看,这次 write 已经完成。
但存储设备还没有承诺数据一定可靠落盘。
Checkpoint 最后还得把这笔账收掉:
OS Page Cache
|
v
fsync / fdatasync
|
v
Storage
于是可能出现:
PostgreSQL 一路 write
|
v
OS Dirty Pages
████████████████████████
最后
|
v
fsync
|
v
Storage Flush
前面没有消失的成本,最后集中暴露出来。
pg_stat_checkpointer 正好把两段时间拆开:
SELECT
write_time,
sync_time,
buffers_written
FROM pg_stat_checkpointer;
如果主要是 write_time 高,重点继续留在 Dirty Set、Pacing 和 Storage Write Bandwidth。
如果 sync_time 很突出,问题就开始往 OS Writeback、fsync latency 和 Storage Flush latency 移动。
还有一点要注意:这些都是累计统计。排查时要看一段时间里的增量,或者结合单次 Checkpoint 的日志,而不是拿从上次 reset 到现在的总数直接比较。
checkpoint_flush_after 为什么会出现在这里?
理解前面的链路以后,这个参数就没那么抽象了。
如果 PostgreSQL 一直 write、write、write,OS Page Cache 里的 Dirty Data 会越积越多:
OS Dirty
████████████████████████████
最后 fsync 时,就可能出现比较明显的尾部压力。
checkpoint_flush_after 的思路,是在写到一定程度以后,提前推动操作系统把一部分 Dirty Data 往下写:
████
↓
writeback
████
↓
writeback
████
而不是一直积到:
████████████████████████
|
v
fsync
Linux 上它的默认值是 256kB,其他平台默认 0;0 表示关闭强制回写。Linux 上的实现最终是通过 sync_file_range() 请求内核回写。
这个参数也不是越积极越好。
一些 workload 本身能很好地利用 OS Page Cache,过早推动 writeback 反而可能影响性能。
所以最好等确认 sync / writeback 确实是主要问题以后再考虑。
到这里,一轮 Checkpoint 自己的性能问题基本就能定位了:
为什么来得这么勤? -> Trigger
为什么这一轮写得这么重? -> Write
为什么尾部又卡了一下? -> Sync
不过 Checkpoint 调优还有另一半。
你可以把 Checkpoint 自己调得更轻,却不代表这些成本真的消失了。
Checkpoint 变少,WAL 也会跟着少吗?
先看 full_page_writes。
前面讲 WAL Record 时已经见过 FPI:新的 Checkpoint 周期开始以后,一个 Page 第一次被修改时,为了防止 Partial Page Write 带来的恢复问题,通常需要在 WAL 里记录 Full Page Image。
所以:
Checkpoint
|
v
Page A 第一次修改 -> FPI
Page B 第一次修改 -> FPI
Page C 第一次修改 -> FPI
Page D 第一次修改 -> FPI
随着越来越多热点 Page 已经经历过这一周期的第一次修改,后面 FPI 的密度通常会慢慢降下来。
如果 workload 本身比较稳定,FPI 带来的那部分额外 WAL 往往更集中在 Checkpoint 之后;总的 WAL 生成速度是否真的呈现这条曲线,还要看业务自己的写入:
Checkpoint
|
v
████████
██████
████
██
█
-------------------------->
这样一来,Checkpoint 频率和 WAL 之间就形成了联系:
Checkpoint 更频繁
|
v
新的 FPI 周期更频繁
|
v
FPI 增多
|
v
WAL 增多
|
v
WAL 预算消耗更快
所以为了缩短 Recovery 而把 Checkpoint 调得很频繁,正常运行期间的 WAL 成本也可能跟着增加。
可以看:
SELECT
wal_records,
wal_fpi,
wal_bytes
FROM pg_stat_wal;
调整 Checkpoint 参数以后,比起只看“Checkpoint 次数少了多少”,我更愿意把 Checkpoint 频率、wal_fpi 和 wal_bytes 放在一起比较。
否则很容易只看到一边。
Checkpoint 拉长以后,Recovery 会怎么样?
既然频繁 Checkpoint 会增加正常运行成本,那很自然会考虑把 checkpoint_timeout 和 max_wal_size 往上调。
Checkpoint 少一点以后:
Checkpoint 频率 ↓
FPI 周期切换 ↓
日常 Checkpoint 干扰可能下降
但 Checkpoint 还有一个职责:建立新的 Recovery 边界。
Crash 后,大致需要从可用的 redo point 往后 replay:
redo point WAL end
| |
v v
----X----------------------------------X
|<--------- WAL Replay ----------->|
Checkpoint 拉得更远,这段 Recovery 需要处理的 WAL 也可能变长。
所以“正常运行更轻”的另一面,可能就是“Crash Recovery 做更多工作”。
这时候就没办法只讨论数据库参数了,业务对恢复时间的要求也会参与进来。
例如:
系统 A
Crash 后 30 秒必须恢复
系统 B
Crash 后几分钟可以接受
但在线交易延迟希望尽量稳定
两套系统的 Checkpoint 策略自然不会一样。
上一篇讲 Online Checkpoint 时还留下一个细节,在这里也有用。
这一轮新的 redo point 很早就确定了:
new redo point
|
v
write...
|
v
sync...
|
v
checkpoint 完成
|
v
新的 Recovery 入口正式可用
如果 Checkpoint 中途 Crash:
上一轮成功 Checkpoint
|
新的 Checkpoint
|
| write...
|
CRASH
新的 Checkpoint 还没有完成,不能直接把它当成新的恢复入口。
所以除了 Checkpoint 的间隔,一轮 Checkpoint 自己拖多久,也会影响崩溃时能退回到哪个已经完成的恢复边界。
pg_wal 很大,也是 Checkpoint 的问题吗?
讲到 WAL 空间,很容易遇到另一个问题:
/pg_wal = 80GB
然后第一反应往往是:是不是 max_wal_size 配太大了?
但 max_wal_size 从来都不是 /pg_wal 的硬上限。
Checkpoint 推进以后,只说明从 Crash Recovery 角度看,一部分旧 WAL 已经不再需要。
一个 WAL Segment 最后能不能回收,还要看有没有其他地方需要它:
这段旧 WAL 能删吗?
|
+-----------------+-----------------+
| | |
v v v
Recovery Replication Archive
还需要? Slot 还需要? 完成了吗?
例如 replication slot 还停在很老的位置:
Replication Slot
|
v
--------X------------------------------------> Current WAL
|
|<----- 这些 WAL 还得留着 --------->|
Checkpoint 已经很正常,这些 WAL 仍然不能回收。
停下来查一下:
SELECT slot_name,
active,
restart_lsn,
pg_size_pretty(
pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn)
) AS wal_retained
FROM pg_replication_slots;
类似地,wal_keep_size、归档延迟、Replication Slot 都会影响 WAL 的保留时间。
所以这里最好把两个看起来很像的问题拆开:
为什么 WAL 产生得这么多?
|
v
Workload
FPI
Checkpoint 频率
为什么旧 WAL 还没有回收?
|
v
Recovery
Slot
Archive
wal_keep_size
两边最后都会表现成“WAL 很多”,排查方向却完全不同。
把整条链放在一起
现在再回到最开始那几个参数:
checkpoint_timeout
max_wal_size
checkpoint_completion_target
checkpoint_flush_after
它们其实分别站在不同的位置上。
Checkpoint
|
为什么来得这么频繁?
|
+-----------+-----------+
| |
timeout WAL / request
| |
v v
checkpoint_timeout max_wal_size / WAL 生成速率
|
v
一轮 Checkpoint 开始
|
+-----------+-----------+
| |
v v
write sync
| |
Dirty Set / Pacing OS Writeback
Write Bandwidth fsync latency
| |
v v
completion_target flush_after
|
v
Checkpoint 完成
|
后面还有几笔账
|
+-----------+-----------+
| | |
v v v
WAL / FPI WAL 空间 Recovery
前半段关注的,是这一轮 Checkpoint 自己发生了什么。
后半段关注的,是 Checkpoint 频率和持续时间变化以后,WAL、空间和 Recovery 跟着发生了什么。
到这里,几个参数之间的关系基本就能放进同一张图里了。
调优之前,先给这些成本定个预算
走到这里,checkpoint_timeout 应该大还是小,max_wal_size 应该给多少,已经很难脱离业务单独回答了。
更实际的是先给几笔成本定个范围。
比如:
正常运行
P99 延迟最多能接受多少波动?
存储
Checkpoint 可以长期占多少写带宽?
WAL
/pg_wal 愿意准备多少磁盘空间?
Recovery
Crash 后希望多久恢复?
这几项目标往往没法同时做到最好。
如果在线延迟最重要,就可能愿意让 Checkpoint 用更长时间慢慢写,也愿意为更长的 Checkpoint 周期多留一些 WAL 空间。
如果 RTO 很严格,希望 Crash 后尽快恢复,那恢复边界就不能长期停在很老的位置,相应地要接受更频繁的 Checkpoint,以及可能更多的 FPI 和正常运行 I/O。
如果磁盘空间本来就紧张,max_wal_size、Replication Slot 和归档延迟又不能只从性能角度考虑。
最后调的其实是几笔预算之间的关系:
Checkpoint 间隔、pacing、单轮范围
|
+-------------+-------------+
| | |
v v v
日常 I/O 抖动 WAL 生成量 WAL 磁盘占用
| | |
+-------------+-------------+
|
v
Crash Recovery 时间
Checkpoint 很难把其中哪一项成本直接消掉。
更多时候是在决定:这笔成本现在付多少、什么时候付,是留在正常运行里,还是留给 Crash Recovery。
理解了 Checkpoint 的内核机制以后,这几个参数也就不像一套需要背下来的“最佳配置”了。
它们决定的只是这几笔账在什么时间、以什么节奏结账。