Hotdry.

Article

PostgreSQL 全页写入关闭:批量写入场景下的双写消除与 WAL 裁剪实战

通过关闭 Postgres 全页写入(FPW)消除双写开销,在批量写入场景下实现 5 倍吞吐提升的工程参数与 WAL 日志裁剪原理。

2026-05-10systems

PostgreSQL 在每次检查点后的首次页面修改时,会将该页面完整内容写入 WAL(Write-Ahead Log),这一机制称为全页写入(Full Page Write,简称 FPW)。默认情况下该选项处于开启状态,其目的是防止因系统崩溃导致的页面撕裂(torn write)风险。然而在批量写入场景中,这一保护机制会带来显著的 I/O 开销 —— 每次检查点后的第一个周期内,WAL 写入量会暴增数倍,导致事务吞吐量出现明显的波峰波谷。通过关闭全页写入,可以在支持原子性写入的存储系统上消除这笔双写开销,从而实现批量写入场景下 5 倍甚至更高的吞吐提升。

全页写入的底层逻辑与双写开销成因

PostgreSQL 采用的是 “物理 - 逻辑” 混合日志机制(physio-logical logging)。在大多数情况下,WAL 记录以逻辑方式描述对页面的修改操作,例如 “删除第 7 个元组”。这类逻辑记录的体积通常只有几十字节,相较于完整页面要轻量得多。但为了应对崩溃场景下页面可能处于部分写入状态的问题,PostgreSQL 在每个检查点之后的第一次页面修改时,会将该页面的完整 8KB 内容作为 WAL 记录写入,而不是仅记录逻辑修改。这就是全页写入的核心逻辑。

这笔开销的大小取决于检查点频率和数据更新模式。以默认配置为例,假设检查点间隔为 5 分钟,每分钟产生 100MB 的逻辑 WAL 记录。那么在检查点刚结束后的第一分钟内,由于大量页面是检查点后的首次修改,系统会额外生成大量完整页面镜像,WAL 写入量可能达到每分钟 500MB 甚至更多。这种写入量的周期性波动在批量写入场景中尤为突出,因为此时数据库处于高负载状态,检查点后的性能骤降会直接影响整体吞吐。

双写开销不仅体现在 WAL 写入量上。PostgreSQL 采用的是日志先写(write-ahead)策略:数据页写入磁盘之前,对应的 WAL 必须已经刷写到磁盘。当全页写入开启时,系统需要先写入完整的 8KB 页面镜像到 WAL,再写入实际的数据页。这意味着在每个检查点周期内,存储子系统需要处理的写入总量接近翻倍。对于依赖机械硬盘或云存储的部署,这种双写开销会直接成为批量写入的瓶颈。

WAL 裁剪原理:关闭 FPW 后的日志体积变化

关闭全页写入后,WAL 的记录方式会发生根本性改变。系统将不再在检查点后的首次页面修改时记录完整页面内容,而是仅记录该次修改的逻辑操作。这意味着 WAL 的平均记录大小将从原来的数百字节(包含完整页面镜像的 8KB)降低到几十字节,具体取决于操作类型。

从数学上看,假设一个表包含 1000 个页面,每个页面在检查点周期内被修改一次。在全页写入开启的情况下,WAL 需要写入 1000 个完整页面镜像,即约 8MB 的数据。当全页写入关闭后,WAL 仅记录 1000 条逻辑修改记录,假设每条记录平均 100 字节,总量仅为 100KB,削减比例接近 98%。这一数据量的削减直接转化为写入性能的提升,尤其在存储带宽受限的场景下效果显著。

需要注意的是,WAL 体积的削减并非线性变化。检查点刚结束后的首次写入周期内,WAL 体积削减幅度最大;随着周期推进,所有活跃页面都已被首次修改,后续的 WAL 将回归到纯粹的逻辑记录模式。因此,从监控角度看,关闭全页写入后 WAL 生成速度会更加平稳,周期性波动会显著减小。

适用场景判断:存储原子性与风险边界

关闭全页写入并非在所有场景下都是安全的。PostgreSQL 依赖存储系统提供 8KB 级别的原子性写入保证,以防止页面撕裂。如果存储系统无法保证这一点,关闭全页写入后发生系统崩溃,可能导致部分页面处于不一致状态,恢复过程无法正确还原数据。

当前已知能够提供 8KB 原子性写入保证的存储系统主要是支持记录级原子性的文件系统,例如 ZFS。当 ZFS 的 recordsize 参数设置为 8KB 或更大时,其 Copy-on-Write 机制可以确保每次写入的原子性,从而允许安全地关闭全页写入。实践表明,在 ZFS 上关闭全页写入后,系统的事务吞吐量曲线会更加平滑,WAL 写入量也会显著降低。

对于使用传统文件系统(如 XFS、ext4)的部署,关闭全页写入存在一定风险。这些文件系统通常只保证 4KB 级别的原子性,而 PostgreSQL 的默认页面大小为 8KB。理论上 4KB 的原子性可以覆盖现代 Sector 大小的页面写入,但缺乏文件系统层面的明确承诺,实际效果因存储栈差异而异。建议在这类系统上保持全页写入开启,除非经过充分的压力测试和崩溃恢复验证。

此外,云环境中的块存储服务(如 AWS EBS、Azure Managed Disks)的原子性保证通常足够可靠,但仍需结合具体的存储类型和配置进行评估。部分云服务商提供的本地 NVMe 存储可能不具备足够的原子性保证,关闭全页写入前应进行针对性测试。

工程参数配置清单

在确认存储系统能够提供足够的原子性保证后,可以通过以下步骤关闭全页写入并验证效果。

首先是参数配置。打开 PostgreSQL 配置文件(postgresql.conf),找到 full_page_writes 参数并设置为 off。该参数支持通过 ALTER SYSTEM 命令动态修改,修改后需要重启 PostgreSQL 实例才能生效。建议在非生产时段执行此操作,并提前创建完整的数据库备份。

-- 使用 ALTER SYSTEM 动态修改(需重启)
ALTER SYSTEM SET full_page_writes = off;

对于 PostgreSQL 15 及以上版本,建议同时评估 recovery_prefetch 参数。该参数可以在崩溃恢复期间预取数据页,减少因缺少全页镜像而导致的 I/O 停顿。配置方式如下:

ALTER SYSTEM SET recovery_prefetch = on;

其次是 WAL 相关参数调优。关闭全页写入后,wal_buffers 可以适当增大,以容纳更多的逻辑 WAL 记录写入。建议将 wal_buffers 设置为 16MB 到 32MB 之间,具体数值根据并发写入量进行微调。同时,监控 max_wal_size 参数,确保 WAL 保留空间足够容纳检查点周期内的写入量。

# postgresql.conf
full_page_writes = off
wal_buffers = 16MB
max_wal_size = 1GB
min_wal_size = 80MB
checkpoint_timeout = 10min

第三是检查点频率优化。检查点会触发新一轮的全页写入周期,因此调整检查点策略有助于进一步降低 WAL 写入波动。建议将 checkpoint_timeout 设置为 10 到 15 分钟,同时结合 checkpoint_completion_target 参数(如 0.9)来平滑检查点的 I/O 压力:

checkpoint_timeout = 15min
checkpoint_completion_target = 0.9

对于批量写入场景,还可以考虑使用 archive_mode = off 关闭 WAL 归档,进一步减少 I/O 开销。但需要注意,这会丧失基于归档的 Point-in-Time 恢复能力,仅适用于可以接受数据丢失的场景。

监控与恢复验证

关闭全页写入后,需要通过多个维度的监控指标验证系统运行状态和性能提升效果。

WAL 生成量是最直观的指标。在关闭全页写入前后,对比相同负载下单位时间内生成的 WAL 体积,预期削减幅度在 70% 到 95% 之间。可以使用 PostgreSQL 的 pg_stat_bgwriter 视图中的 wal_bytes 字段或外部监控工具(如 pg_walinspect 扩展)进行跟踪。

事务吞吐量监控同样重要。使用 pg_stat_database 或应用层的性能监控工具,跟踪 TPS(每秒事务数)在检查点前后的变化。关闭全页写入后,吞吐量曲线应趋于平稳,不再出现检查点后的明显波谷。

崩溃恢复时间是必须验证的风险指标。在测试环境中模拟系统崩溃(如通过 pg_ctl stop -m immediate 或断电模拟),测量从启动到完全恢复的时间。预期恢复时间会略有增加,因为系统需要读取数据页而非直接从 WAL 中恢复完整页面。如果恢复时间超过业务可接受的范围,需要重新评估是否适合关闭全页写入。

对于 ZFS 用户,建议同时监控 ZFS 的 ARC(Adaptive Replacement Cache)命中率。由于关闭全页写入后,崩溃恢复时需要从数据文件读取页面而非 WAL,ARC 的有效性直接影响恢复性能。可以通过 zpool iostat 命令监控 ZFS 的读取 I/O 操作,评估是否存在大量数据页读取导致的 I/O 峰值。

风险边界与降级策略

即便在支持原子性写入的存储系统上,关闭全页写入仍可能带来一些非预期的风险。因此,建议制定明确的降级策略,以便在出现问题时快速恢复。

降级方案很简单:将 full_page_writes 参数重新设置为 on 并重启 PostgreSQL 实例。在重启前,建议记录当前的 wal_sizecheckpoint_location,以便后续分析和对比。同时,保留一份关闭全页写入前的完整备份,确保在降级后数据一致性得到验证。

对于需要持续运行的生产环境,建议使用流复制(Streaming Replication)进行热备。关闭全页写入后,主库的崩溃恢复可能需要更长时间,但备库可以继续提供只读服务,减少业务中断时间。

最后,建议在监控系统中设置告警阈值。当 WAL 生成量突然暴增或事务吞吐量显著下降时,自动触发告警并记录相关参数,供后续分析使用。

结论

关闭 PostgreSQL 全页写入是批量写入场景下消除双写开销的有效手段。通过将 WAL 日志从物理 - 逻辑混合模式切换为纯逻辑模式,可以在保证数据安全的前提下实现 WAL 体积 70% 到 95% 的削减,进而带来数倍的吞吐量提升。该方案适用于支持 8KB 原子性写入的存储系统(如 ZFS),对于传统文件系统需要谨慎评估。配合 recovery_prefetch 和检查点调优,可以在性能提升和恢复可靠性之间取得良好的平衡。

资料来源:PostgreSQL Wiki - Full Page Writes(wiki.postgresql.org/wiki/Full_page_writes)。

systems

内容声明:本文无广告投放、无付费植入。

如有事实性问题,欢迎发送勘误至 i@hotdrydog.com