PostgreSQL 把数据写到共享缓冲区,再交给 Linux 页缓存,最终由内核刷回磁盘。这条链路看起来顺理成章,但 Linux 的刷盘节奏并不总是对数据库友好——脏页堆积到阈值后,内核可能一次性倾泻大量 I/O,造成查询卡顿甚至整个系统抖动。为了夺回主动权,PostgreSQL 从 9.6 起引入了四个 *_flush_after GUC,而 backend_flush_after 是其中最保守、也最容易被忽视的那一个。
Linux 页缓存:沉默的中间人
当 PostgreSQL 的后台进程执行 write(),数据并没有直达磁盘。它先进入 Linux 页缓存(page cache),标记为脏页,等待内核择机刷回。内核的择机逻辑取决于:
/proc/sys/vm/dirty_ratio——脏页占内存比例超过此值,写操作被阻塞强制刷盘/proc/sys/vm/dirty_background_ratio——超过此值,内核后台线程开始异步刷盘/proc/sys/vm/dirty_writeback_centisecs——内核刷盘线程的唤醒间隔
问题在于:如果 dirty_background_ratio 设得宽松,脏页可以堆积到很大规模;一旦触及 dirty_ratio,所有写操作被阻塞直到脏页清空。对数据库来说,这种"平时不刷、突然猛刷"的模式会带来不可预测的延迟尖峰。
四个 flush_after,四种节奏
PostgreSQL 用四个 GUC 分别控制不同角色的刷盘时机:
| GUC | 作用对象 | 默认值(PG 9.6+) |
|---|---|---|
backend_flush_after |
普通查询后端进程 | 0(禁用)→ 后续版本 128kB |
bgwriter_flush_after |
后台写进程 | 512kB |
checkpoint_flush_after |
检查点进程 | 256kB |
wal_writer_flush_after |
WAL 写进程 | 1MB |
每个参数的含义相同:当对应进程累计写入达到此阈值,就主动调用操作系统层面的 flush(不是 fsync,而是让内核尽早处理已提交的脏页),把大块突发 I/O拆成连续的小波。
backend_flush_after 之所以被称为"保守",是因为它只约束单个后端进程——也就是你跑的那条 INSERT 或 UPDATE 所在的进程。它不影响检查点的大批量刷盘,也不干预 WAL 写进程的节奏。换句话说,它管的是"细水长流"的那一支,而不是"开闸放水"的主渠。
为什么默认值曾经是 0
早期版本 backend_flush_after 默认禁用,理由很简单:单个后端进程的写量通常不大,让内核自行调度似乎就够了。但在两类场景下,这个假设会失灵:
- 大批量数据导入——一条
COPY或批量INSERT可以在短时间内产生大量脏页,单个后端进程的写量远超日常。 - Linux 脏页阈值配置激进——云厂商默认的
dirty_background_ratio往往偏高,脏页堆积到检查点触发时才集中刷盘,造成 I/O 尖峰。
开启 backend_flush_after 后,后端进程每写 128kB(或你设定的值)就提醒内核"这些页可以处理了",避免脏页在页缓存里过度囤积。
实践:观察与调优
下面给出一个可以直接在 Linux + PostgreSQL 环境下运行的检查与调优流程。
第一步:查看当前配置与内核脏页参数
# 查看 PostgreSQL 四个 flush_after 的当前值
psql -c "SELECT name, setting, unit, short_desc
FROM pg_settings
WHERE name LIKE '%flush_after%';"
# 查看 Linux 内核脏页相关参数
echo "dirty_background_ratio = $(cat /proc/sys/vm/dirty_background_ratio)"
echo "dirty_ratio = $(cat /proc/sys/vm/dirty_ratio)"
echo "dirty_writeback_centisecs = $(cat /proc/sys/vm/dirty_writeback_centisecs)"
# 查看当前脏页实际占比
grep -E 'Dirty|Writeback' /proc/meminfo
运行后你会看到类似输出:
name | setting | unit | short_desc
----------------------+---------+------+---------------------------------
backend_flush_after | 0 | kB | ...
bgwriter_flush_after | 512 | kB | ...
checkpoint_flush_after| 256 | kB | ...
wal_writer_flush_after| 1024 | kB | ...
dirty_background_ratio = 10
dirty_ratio = 20
Dirty: 1048576 kB
Writeback: 0 kB
如果 backend_flush_after 是 0,且 dirty_background_ratio ≥ 10,你的系统就有脏页堆积的风险。
第二步:开启 backend_flush_after 并做压力测试
# 在 postgresql.conf 中设置(或用 ALTER SYSTEM)
psql -c "ALTER SYSTEM SET backend_flush_after = '128kB';"
psql -c "SELECT pg_reload_conf();"
# 确认生效
psql -c "SHOW backend_flush_after;"
然后用 pgbench 做一次对比测试:
# 初始化 pgbench 数据(规模 100,约 1.5GB 数据)
pgbench -i -s 100 mydb
# 禁用 backend_flush_after 时的 TPS
pgbench -c 10 -j 4 -T 60 mydb > /tmp/result_off.txt
# 开启后重新测试
psql -c "ALTER SYSTEM SET backend_flush_after = '128kB'; SELECT pg_reload_conf();"
pgbench -c 10 -j 4 -T 60 mydb > /tmp/result_on.txt
# 对比平均 TPS 和延迟抖动
grep -E 'tps|latency' /tmp/result_off.txt /tmp/result_on.txt
重点不是看平均 TPS 提升多少,而是看延迟的标准差是否缩小——backend_flush_after 的核心价值在于平滑 I/O,减少尖峰。
第三步:配合内核参数一起调
如果你的工作负载以写入为主,建议同时收紧 Linux 脚页阈值:
# 临时修改(重启失效)
sysctl -w vm.dirty_background_ratio=5
sysctl -w vm.dirty_ratio=10
# 持久化
echo "vm.dirty_background_ratio = 5" >> /etc/sysctl.d/99-postgres.conf
echo "vm.dirty_ratio = 10" >> /etc/sysctl.d/99-postgres.conf
sysctl -p /etc/sysctl.d/99-postgres.conf
两层配合的逻辑:内核更早开始异步刷盘(dirty_background_ratio=5),PostgreSQL 后端进程也更频繁地提醒内核处理脏页(backend_flush_after=128kB),双重保险避免脏页雪崩。
调优边界与注意事项
- 不要把
backend_flush_after设得太小——比如 16kB,意味着每写两三个 8kB 页就 flush 一次,频繁的系统调用反而拖慢写入。64kB–256kB 是合理的探索区间。 - 只影响后端进程——检查点期间的刷盘节奏由
checkpoint_flush_after控制,两者需要独立考虑。如果你只调了backend_flush_after而忽略检查点参数,检查点到来时仍可能出现 I/O 风暴。 - SSD 上效果更明显——机械盘上小批量刷盘的收益有限(随机写本身就慢),NVMe SSD 上细粒度 flush 能更好地利用硬件并行度。
- 只管"提醒",不管"保证"——
*_flush_after触发的是write()后的内核侧处理提示,不是fsync()。数据持久性仍由检查点和 WAL 的fsync保证。
一份快速检查清单
在部署写入密集的 PostgreSQL 实例时,建议按以下顺序排查:
- ✅ 确认
backend_flush_after不为 0(至少 128kB) - ✅ 确认
checkpoint_flush_after和bgwriter_flush_after有合理值 - ✅ 检查
vm.dirty_background_ratio是否 ≤ 5(写入密集场景) - ✅ 用
pgbench或真实负载做延迟抖动对比测试 - ✅ 监控
/proc/meminfo中Dirty页的峰值,确认不再出现大规模堆积
backend_flush_after 不是什么银弹,但它是 PostgreSQL 向 Linux 页缓存争夺 I/O 节奏控制权的一个具体抓手。理解它"保守"的定位——只管后端进程的细水长流——才能把它放在正确的位置上,和检查点参数、内核脏页配置一起构成完整的防抖策略。