在PostgreSQL的架构设计中,预写日志(WAL)是保障数据一致性和崩溃恢复的核心机制。而全页写(Full Page Write)作为WAL的重要组成部分,却是一个容易被开发者忽视的"幕后功臣"。本文将从原理、性能影响、调优实践等角度,深入剖析全页写机制对WAL日志的影响,助你掌握PostgreSQL性能优化的关键细节。
一、全页写机制的原理与作用
1.1. 什么是全页写?
在PostgreSQL中,每个数据页的大小默认为8KB。全页写机制规定:在每次检查点(Checkpoint)后的第一次修改中,无论数据变动多小,都会将整个数据页的内容写入WAL日志(而非仅记录增量修改)。
默认配置下,该机制通过参数full_page_writes=on启用。
1.2. 为什么需要全页写?
防止部分页写入(Partial Page Write)
当操作系统或存储设备发生崩溃时,可能导致数据页仅部分写入磁盘(例如只写入了4KB)。此时,若WAL仅记录增量修改,恢复时会因基础页损坏而无法修复数据。全页写记录的完整页可作为恢复的"基线",确保后续增量修改的正确性。
1.3. 全页写的代价
WAL日志体积显著增加:全页写记录的8KB完整页,可能比增量日志大数倍。
额外的I/O压力:更大的WAL日志意味着更多的磁盘写入操作。
二、全页写对WAL的四大影响
2.1 WAL日志体积膨胀
典型场景
检查点后的首次数据页修改(如批量插入、索引创建)会触发全页写。例如,一个仅修改了10字节的UPDATE操作,若发生在检查点后,则需写入8KB的完整页到WAL。
数据对比:
操作类型 | 增量WAL记录大小 | 全页写WAL记录大小 |
---|---|---|
单行INSERT | ~200字节 | 8KB |
创建B-tree索引 | 若干增量记录 | 全页写×N个数据页 |
影响:存储成本增加,日志备份和传输时间延长。
2.2 崩溃恢复的"双刃剑"
优势:
全页写是崩溃恢复的最后一道防线。即使存储层发生部分页写入,PostgreSQL仍能从WAL中提取完整页进行修复。
风险:
若关闭全页写(full_page_writes=off),且存储设备不支持原子写入(如机械硬盘或部分SSD),崩溃后可能面临数据永久损坏。
2.3 性能波动与写入延迟
写入瓶颈:
全页写产生的额外I/O可能成为高并发写入场景的性能瓶颈。例如,在OLTP系统中,频繁的小事务可能导致大量全页写操作,拖慢整体吞吐量。
检查点频率的关联:
参数checkpoint_timeout(默认5分钟)控制检查点触发频率。检查点越频繁,全页写的触发次数越多,但恢复时间更短。需权衡两者关系。
2.4 与WAL压缩的协同效应
缓解空间占用:
PostgreSQL 13+支持wal_compression=on,可对WAL日志(包括全页写内容)进行LZ4或ZSTD压缩,降低日志体积(压缩率通常达30%~70%)。
CPU开销:
压缩操作消耗额外CPU资源,需在高I/O和低CPU环境中谨慎启用。
三、实验验证
PostgreSQL 9.5以后的pg_waldump都带有统计功能,可以查看不同类型的WAL记录的数量,大小以及FPI的比例。
3.1 postgres.conf配置
下面是一个未经特别优化的配置
cpp
shared_buffers = 32GB
checkpoint_completion_target = 0.9
checkpoint_timeout = 5min
min_wal_size = 1GB
max_wal_size = 4GB
wal_log_hints = on
wal_level = replica
wal_keep_size = 1000
3.2 测试
3.2.1 准备pgbench测试环境
cpp
[postgres@db2 ~]$ pgbench -i
dropping old tables...
NOTICE: table "pgbench_accounts" does not exist, skipping
NOTICE: table "pgbench_branches" does not exist, skipping
NOTICE: table "pgbench_history" does not exist, skipping
NOTICE: table "pgbench_tellers" does not exist, skipping
creating tables...
generating data (client-side)...
100000 of 100000 tuples (100%) done (elapsed 0.09 s, remaining 0.00 s)
vacuuming...
creating primary keys...
done in 0.33 s (drop tables 0.00 s, create tables 0.01 s, client-side generate 0.13 s, vacuum 0.09 s, primary keys 0.10 s).
3.2.2先手动执行checkpoint
cpp
[postgres@db2 ~]$ pgbench -n -c 64 -j 64 -T 10
pgbench (16.1)
transaction type: <builtin: TPC-B (sort of)>
scaling factor: 1
query mode: simple
number of clients: 64
number of threads: 64
maximum number of tries: 1
duration: 10 s
number of transactions actually processed: 4474
number of failed transactions: 0 (0.000%)
latency average = 138.536 ms
initial connection time = 491.308 ms
tps = 461.973032 (without initial connection time)
3.2.3记录当前的LSN
cpp
[postgres@db2 ~]$ psql -c "select pg_current_wal_lsn()"
pg_current_wal_lsn
--------------------
5/D5022100
(1 row)
3.2.4 日志统计
统计压测期间产生的WAL
cpp
[postgres@db2 ~]$ pg_waldump -t 6 --stats=record -s 5/D4000000 -e 5/D5022100
WAL statistics between 5/D4000028 and 5/D5022100:
Type N (%) Record size (%) FPI size (%) Combined size (%)
---- - --- ----------- --- -------- --- ------------- ---
Transaction/COMMIT 4477 ( 12.68) 152474 ( 6.22) 0 ( 0.00) 152474 ( 0.91)
Standby/RUNNING_XACTS 3 ( 0.01) 162 ( 0.01) 0 ( 0.00) 162 ( 0.00)
Standby/INVALIDATIONS 3 ( 0.01) 270 ( 0.01) 0 ( 0.00) 270 ( 0.00)
Heap2/PRUNE 2276 ( 6.45) 150920 ( 6.15) 0 ( 0.00) 150920 ( 0.90)
Heap2/VACUUM 29 ( 0.08) 1556 ( 0.06) 0 ( 0.00) 1556 ( 0.01)
Heap2/VISIBLE 94 ( 0.27) 5561 ( 0.23) 24576 ( 0.17) 30137 ( 0.18)
Heap/INSERT 4420 ( 12.52) 351748 ( 14.34) 10756 ( 0.08) 362504 ( 2.16)
Heap/UPDATE 2190 ( 6.20) 369994 ( 15.09) 9860 ( 0.07) 379854 ( 2.26)
Heap/HOT_UPDATE 11129 ( 31.52) 803331 ( 32.76) 7644 ( 0.05) 810975 ( 4.83)
Heap/LOCK 8198 ( 23.22) 450277 ( 18.36) 12251380 ( 85.46) 12701657 ( 75.66)
Heap/INPLACE 3 ( 0.01) 470 ( 0.02) 7388 ( 0.05) 7858 ( 0.05)
Heap/INSERT+INIT 60 ( 0.17) 4740 ( 0.19) 0 ( 0.00) 4740 ( 0.03)
Heap/UPDATE+INIT 110 ( 0.31) 16026 ( 0.65) 0 ( 0.00) 16026 ( 0.10)
Btree/INSERT_LEAF 2306 ( 6.53) 144537 ( 5.89) 2024340 ( 14.12) 2168877 ( 12.92)
Btree/VACUUM 2 ( 0.01) 264 ( 0.01) 0 ( 0.00) 264 ( 0.00)
CommitTs/ZEROPAGE 6 ( 0.02) 180 ( 0.01) 0 ( 0.00) 180 ( 0.00)
-------- -------- -------- --------
Total 35306 2452510 [14.61%] 14335944 [85.39%] 16788454 [100%]
这个统计结果显示FPI的比例占到了85.39%。Record size的比例占了14.61%,也就是说WAL被放大了近6倍,这个的比例是相当高的。
可以通过计算WAL距离的方式,算出准确的FPI比例。
cpp
[postgres@db2 ~]$ pg_waldump -t 6 --stats=record -s 5/D4000000 -e 5/D5022100
postgres=# select pg_wal_lsn_diff('5/D5022100','5/D4000000');
pg_wal_lsn_diff
-----------------
16916736
(1 row)
postgres=# select 16916736.0/ 16788454;
?column?
--------------------
1.0076410847597998
(1 row)
这里计算的有点问题,大小1了
三、调优实践:如何平衡性能与可靠性?
3.1 逻辑层
在应用的写负载不变的情况下,减少WAL生成量主要有下面几种办法。
1)延长checkpoint时间间隔
FPI产生于checkpoint之后第一次变脏的page,在下次checkpoint到了之前,已经输出过PFI的page是不需要再次输出FPI。因此checkpoint时间间隔越长,FPI产生的频度会越低。增大checkpoint_timeout和max_wal_size可以延长checkpoint时间间隔。
2)增加HOT_UPDATE比例
普通的UPDATE经常需要更新2个数据块,并且可能还要更新索引page,这些又都有可能产生FPI。而HOT_UPDATE只修改1个数据块,需要写的WAL量也大大减少。
3)压缩
PostgreSQL9.5新增加了一个wal_compression参数,设为on可以对FPI进行压缩,削减WAL的大小。另外还可以在外部通过SSL/SSH的压缩功能减少主备间的通信流量,已经自定义归档脚本对归档的WAL进行压缩。
3.2 参数层
3.2.1. 关键参数配置
参数 | 推荐值 | 说明 |
---|---|---|
full_page_writes | on(默认) | 除非存储支持原子写入(如ZFS或企业级存储),否则勿关闭! |
checkpoint_timeout | 10min~1h | 延长检查点间隔以减少全页写频率,但需配合max_wal_size避免磁盘占满。 |
wal_compression | on(可选) | 启用压缩以缓解WAL体积,需监控CPU利用率。 |
3.2.2. 高性能存储场景的特殊处理
原子写入存储设备:
若使用支持原子写入的存储(如ZFS、带断电保护的NVMe SSD),可尝试关闭全页写:
cpp
ALTER SYSTEM SET full_page_writes = off;
SELECT pg_reload_conf();
警告:需通过pg_test_fsync工具验证存储的原子性,并严格测试崩溃恢复流程。
3.3.3. 监控与诊断工具
- 查看全页写占比:
通过WAL解析工具pg_waldump,观察FPW(全页写)记录的比例:
cpp
pg_waldump -p /data/pg_wal 0000000100000001000000A0 | grep FPW
- 检查点性能监控:
查询pg_stat_bgwriter视图,关注以下指标:
(1)checkpoints_timed:计划内检查点次数
(2)checkpoints_req:计划外检查点次数
(3)buffers_checkpoint:检查点期间写入的缓冲区数量
3.4优化测试
3.4.1 延长checkpoint时间
首先优化checkpoint相关参数
postgres.conf:
cpp
shared_buffers = 32GB
checkpoint_completion_target = 0.1
checkpoint_timeout = 60min
min_wal_size = 4GB
max_wal_size = 64GB
wal_log_hints = on
wal_level = replica
wal_keep_segments = 1000
1)先手动执行checkpoint
cpp
[postgres@db2 pg_wal]$ psql -c "checkpoint"
CHECKPOINT
2) 再利用pgbench做一个压测10w个事务
cpp
psql -c "select pg_current_wal_lsn()" ; pgbench -n -c 100 -j 100 -t 1000 ;psql -c "select pg_current_wal_lsn()"
pg_current_wal_lsn
--------------------
5/D8FD807
pgbench (16.1)
transaction type: <builtin: TPC-B (sort of)>
scaling factor: 1
query mode: simple
number of clients: 100
number of threads: 100
maximum number of tries: 1
number of transactions per client: 1000
number of transactions actually processed: 100000/100000
number of failed transactions: 0 (0.000%)
latency average = 182.966 ms
initial connection time = 554.102 ms
tps = 546.550591 (without initial connection time)
pg_current_wal_lsn
--------------------
5/DCCD5B20
(1 row)
3) 记录当前的LSN
cpp
[postgres@db2 pg_wal]$ psql -c "select pg_current_wal_lsn()"
pg_current_wal_lsn
--------------------
5/D8FC31A8
(1 row)
4) 日志统计
统计压测期间产生的WAL
- 第一次执行
cpp
[postgres@db2 pg_wal]$ pg_waldump -t 6 --stats=record -s 5/D8FD8070 -e 5/DCCD5B20
WAL statistics between 5/D8FD8070 and 5/DCCD5B20:
Type N (%) Record size (%) FPI size (%) Combined size (%)
---- - --- ----------- --- -------- --- ------------- ---
Transaction/COMMIT 100018 ( 13.77) 3402996 ( 7.29) 0 ( 0.00) 3402996 ( 5.47)
Storage/TRUNCATE 1 ( 0.00) 46 ( 0.00) 0 ( 0.00) 46 ( 0.00)
CLOG/ZEROPAGE 4 ( 0.00) 120 ( 0.00) 0 ( 0.00) 120 ( 0.00)
Standby/LOCK 1 ( 0.00) 42 ( 0.00) 0 ( 0.00) 42 ( 0.00)
Standby/RUNNING_XACTS 11 ( 0.00) 4364 ( 0.01) 0 ( 0.00) 4364 ( 0.01)
Standby/INVALIDATIONS 10 ( 0.00) 1140 ( 0.00) 0 ( 0.00) 1140 ( 0.00)
Heap2/PRUNE 77430 ( 10.66) 5120674 ( 10.97) 12716608 ( 82.03) 17837282 ( 28.68)
Heap2/VACUUM 186 ( 0.03) 11740 ( 0.03) 0 ( 0.00) 11740 ( 0.02)
Heap2/VISIBLE 861 ( 0.12) 50814 ( 0.11) 24576 ( 0.16) 75390 ( 0.12)
Heap/INSERT 99306 ( 13.67) 7844974 ( 16.80) 53180 ( 0.34) 7898154 ( 12.70)
Heap/UPDATE 5780 ( 0.80) 875796 ( 1.88) 117896 ( 0.76) 993692 ( 1.60)
Heap/HOT_UPDATE 294009 ( 40.48) 21248245 ( 45.51) 457920 ( 2.95) 21706165 ( 34.90)
Heap/LOCK 141566 ( 19.49) 7644594 ( 16.37) 48456 ( 0.31) 7693050 ( 12.37)
Heap/INPLACE 36 ( 0.00) 7084 ( 0.02) 7388 ( 0.05) 14472 ( 0.02)
Heap/INSERT+INIT 694 ( 0.10) 54826 ( 0.12) 0 ( 0.00) 54826 ( 0.09)
Heap/UPDATE+INIT 279 ( 0.04) 34720 ( 0.07) 0 ( 0.00) 34720 ( 0.06)
Btree/INSERT_LEAF 6059 ( 0.83) 384729 ( 0.82) 2075940 ( 13.39) 2460669 ( 3.96)
Btree/VACUUM 8 ( 0.00) 3212 ( 0.01) 0 ( 0.00) 3212 ( 0.01)
CommitTs/ZEROPAGE 122 ( 0.02) 3660 ( 0.01) 0 ( 0.00) 3660 ( 0.01)
-------- -------- -------- --------
Total 726381 46693776 [75.08%] 15501964 [24.92%] 62195740 [100%]
[postgres@db2 pg_wal]$

- 第二次执行
注意不要执行检查点
cpp
[postgres@db2 pg_wal]$ psql -c "select pg_current_wal_lsn()" ; pgbench -n -c 100 -j 100 -t 1000 ;psql -c "select pg_current_wal_lsn()"
pg_current_wal_lsn
--------------------
5/E45E9D00
(1 row)
pgbench (16.1)
transaction type: <builtin: TPC-B (sort of)>
scaling factor: 1
query mode: simple
number of clients: 100
number of threads: 100
maximum number of tries: 1
number of transactions per client: 1000
number of transactions actually processed: 100000/100000
number of failed transactions: 0 (0.000%)
latency average = 164.796 ms
initial connection time = 295.767 ms
tps = 606.811368 (without initial connection time)
pg_current_wal_lsn
--------------------
5/E7387038
(1 row)
查看WAL产生的情况
cpp
[postgres@db2 pg_wal]$ pg_waldump -t 6 --stats=record -s 5/E09C0E70 -e 5/E45E4F38
WAL statistics between 5/E09C0E70 and 5/E45E4F38:
Type N (%) Record size (%) FPI size (%) Combined size (%)
---- - --- ----------- --- -------- --- ------------- ---
Transaction/COMMIT 100019 ( 14.01) 3402998 ( 7.46) 0 ( 0.00) 3402998 ( 5.53)
CLOG/ZEROPAGE 3 ( 0.00) 90 ( 0.00) 0 ( 0.00) 90 ( 0.00)
Standby/RUNNING_XACTS 12 ( 0.00) 5045 ( 0.01) 0 ( 0.00) 5045 ( 0.01)
Standby/INVALIDATIONS 4 ( 0.00) 504 ( 0.00) 0 ( 0.00) 504 ( 0.00)
Heap2/PRUNE 76745 ( 10.75) 5193777 ( 11.38) 12584052 ( 79.36) 17777829 ( 28.92)
Heap2/VACUUM 208 ( 0.03) 12024 ( 0.03) 0 ( 0.00) 12024 ( 0.02)
Heap2/VISIBLE 572 ( 0.08) 33763 ( 0.07) 24576 ( 0.15) 58339 ( 0.09)
Heap/INSERT 99320 ( 13.92) 7844680 ( 17.19) 277344 ( 1.75) 8122024 ( 13.21)
Heap/UPDATE 1542 ( 0.22) 172852 ( 0.38) 108344 ( 0.68) 281196 ( 0.46)
Heap/HOT_UPDATE 298298 ( 41.79) 21518813 ( 47.16) 1175964 ( 7.42) 22694777 ( 36.91)
Heap/LOCK 134183 ( 18.80) 7245882 ( 15.88) 0 ( 0.00) 7245882 ( 11.79)
Heap/INPLACE 31 ( 0.00) 6103 ( 0.01) 7388 ( 0.05) 13491 ( 0.02)
Heap/INSERT+INIT 680 ( 0.10) 53720 ( 0.12) 0 ( 0.00) 53720 ( 0.09)
Heap/UPDATE+INIT 216 ( 0.03) 18160 ( 0.04) 0 ( 0.00) 18160 ( 0.03)
Btree/INSERT_LEAF 1758 ( 0.25) 110191 ( 0.24) 1672340 ( 10.55) 1782531 ( 2.90)
Btree/DELETE 2 ( 0.00) 193 ( 0.00) 7400 ( 0.05) 7593 ( 0.01)
Btree/VACUUM 8 ( 0.00) 2440 ( 0.01) 0 ( 0.00) 2440 ( 0.00)
CommitTs/ZEROPAGE 122 ( 0.02) 3660 ( 0.01) 0 ( 0.00) 3660 ( 0.01)
-------- -------- -------- --------
Total 713723 45624895 [74.21%] 15857408 [25.79%] 61482303 [100%]

这个统计结果显示FPI的占比明显减少
次数 | TPS | 非FPI | FPI | FPI占比 | WAL总量 |
---|---|---|---|---|---|
1 | 553 | 46693776 | 15501964 | 24.92% | 62195740 |
2 | 606 | 46059408 | 245240 | 0.53% | 46304648 |
可以多做几次测试对比效果。
3.4.2 增加HOT_UPDATE比例
HOT_UPDATE比例过低的一个很常见的原因是更新频繁的表的fillfactor设置不恰当。fillfactor的默认值为100%,可以先将其调整为90%。
对于宽表,要进一步减小fillfactor使得至少可以保留一个tuple的空闲空间。可以查询pg_class系统表估算平均tuple大小,并算出合理的fillfactor值。
cpp
postgres=# select 1 - relpages/reltuples max_fillfactor from pg_class where relname='big_tb';
max_fillfactor
----------------------
0.69799901185770750988
(1 row)
再上面估算出的69%的基础上,可以把fillfactor再稍微设小一点,比如设成65% 。
- 在前面优化过的参数的基础上,先保持fillfactor=100不变,执行10w事务的压测
cpp
[postgres@db2 pg_wal]$ psql -c "checkpoint;select pg_current_wal_lsn()" ; pgbench -n -c 100 -j 100 -t 1000 ;
psql -c "select pg_current_wal_lsn()"
CHECKPOINT
pg_current_wal_lsn
--------------------
5/E738CB90
(1 row)
pgbench (16.1)
transaction type: <builtin: TPC-B (sort of)>
scaling factor: 1
query mode: simple
number of clients: 100
number of threads: 100
maximum number of tries: 1
number of transactions per client: 1000
number of transactions actually processed: 100000/100000
number of failed transactions: 0 (0.000%)
latency average = 165.636 ms
initial connection time = 398.058 ms
tps = 603.732351 (without initial connection time)
[postgres@db2 pg_wal]$ psql -c "select pg_current_wal_lsn()"
pg_current_wal_lsn
--------------------
5/EAFB96D8
(1 row)
查看WAL占比
cpp
postgres@db2 pg_wal]$ pg_waldump -t 6 --stats=record -s 5/E738CB90 -e 5/EAFB96D8
WAL statistics between 5/E738CB90 and 5/EAFB96D8:
Type N (%) Record size (%) FPI size (%) Combined size (%)
---- - --- ----------- --- -------- --- ------------- ---
Transaction/COMMIT 100011 ( 13.88) 3401830 ( 7.40) 0 ( 0.00) 3401830 ( 5.53)
CLOG/ZEROPAGE 3 ( 0.00) 90 ( 0.00) 0 ( 0.00) 90 ( 0.00)
Standby/RUNNING_XACTS 10 ( 0.00) 3863 ( 0.01) 0 ( 0.00) 3863 ( 0.01)
Standby/INVALIDATIONS 5 ( 0.00) 642 ( 0.00) 0 ( 0.00) 642 ( 0.00)
Heap2/PRUNE 79265 ( 11.00) 5349495 ( 11.64) 12960248 ( 83.19) 18309743 ( 29.75)
Heap2/VACUUM 127 ( 0.02) 8216 ( 0.02) 0 ( 0.00) 8216 ( 0.01)
Heap2/VISIBLE 908 ( 0.13) 53587 ( 0.12) 24576 ( 0.16) 78163 ( 0.13)
Heap/INSERT 99352 ( 13.79) 7846308 ( 17.07) 564988 ( 3.63) 8411296 ( 13.67)
Heap/UPDATE 1397 ( 0.19) 138376 ( 0.30) 4072 ( 0.03) 142448 ( 0.23)
Heap/HOT_UPDATE 298490 ( 41.43) 21517154 ( 46.81) 909692 ( 5.84) 22426846 ( 36.44)
Heap/LOCK 138431 ( 19.21) 7475274 ( 16.26) 0 ( 0.00) 7475274 ( 12.15)
Heap/INPLACE 24 ( 0.00) 4705 ( 0.01) 7388 ( 0.05) 12093 ( 0.02)
Heap/INSERT+INIT 648 ( 0.09) 51192 ( 0.11) 0 ( 0.00) 51192 ( 0.08)
Heap/UPDATE+INIT 148 ( 0.02) 12456 ( 0.03) 0 ( 0.00) 12456 ( 0.02)
Btree/INSERT_LEAF 1545 ( 0.21) 97340 ( 0.21) 1108660 ( 7.12) 1206000 ( 1.96)
Btree/DELETE 4 ( 0.00) 526 ( 0.00) 0 ( 0.00) 526 ( 0.00)
Btree/VACUUM 6 ( 0.00) 2420 ( 0.01) 0 ( 0.00) 2420 ( 0.00)
CommitTs/ZEROPAGE 122 ( 0.02) 3660 ( 0.01) 0 ( 0.00) 3660 ( 0.01)
-------- -------- -------- --------
Total 720496 45967134 [74.69%] 15579624 [25.31%] 61546758 [100%]

- 设置fillfactor=90
cpp
postgres=# alter table pgbench_accounts set (fillfactor=90);
ALTER TABLE
postgres=# vacuum full pgbench_accounts;
VACUUM
postgres=# alter table pgbench_tellers set (fillfactor=90);
ALTER TABLE
postgres=# vacuum full pgbench_tellers;
VACUUM
postgres=# alter table pgbench_branches set (fillfactor=90);
ALTER TABLE
postgres=# vacuum full pgbench_branches;
VACUUM
再执行10w事务的压测
cpp
postgres@db2 pg_wal]$ psql -c "checkpoint;select pg_current_wal_lsn()" ; pgbench -n -c 100 -j 100 -t 1000 ;psql -c "select pg_current_wal_lsn()"
CHECKPOINT
pg_current_wal_lsn
--------------------
5/EBE90698
(1 row)
pgbench (16.1)
transaction type: <builtin: TPC-B (sort of)>
scaling factor: 1
query mode: simple
number of clients: 100
number of threads: 100
maximum number of tries: 1
number of transactions per client: 1000
number of transactions actually processed: 100000/100000
number of failed transactions: 0 (0.000%)
latency average = 202.112 ms
initial connection time = 281.672 ms
tps = 494.775446 (without initial connection time)
[postgres@db2 pg_wal]$ psql -c "checkpoint;select pg_current_wal_lsn()"
CHECKPOINT
pg_current_wal_lsn
--------------------
5/EF94B3E8
(1 row)
查看WAL日志量
cpp
[postgres@db2 pg_wal]$ pg_waldump -t 6 --stats=record -s 5/EBE90698 -e 5/EF94B3E8
WAL statistics between 5/EBE90698 and 5/EF94B3E8:
Type N (%) Record size (%) FPI size (%) Combined size (%)
---- - --- ----------- --- -------- --- ------------- ---
XLOG/CHECKPOINT_ONLINE 1 ( 0.00) 114 ( 0.00) 0 ( 0.00) 114 ( 0.00)
Transaction/COMMIT 100017 ( 13.92) 3402514 ( 7.45) 0 ( 0.00) 3402514 ( 5.67)
Storage/TRUNCATE 1 ( 0.00) 46 ( 0.00) 0 ( 0.00) 46 ( 0.00)
CLOG/ZEROPAGE 3 ( 0.00) 90 ( 0.00) 0 ( 0.00) 90 ( 0.00)
Standby/LOCK 1 ( 0.00) 42 ( 0.00) 0 ( 0.00) 42 ( 0.00)
Standby/RUNNING_XACTS 13 ( 0.00) 4994 ( 0.01) 0 ( 0.00) 4994 ( 0.01)
Standby/INVALIDATIONS 5 ( 0.00) 594 ( 0.00) 0 ( 0.00) 594 ( 0.00)
Heap2/PRUNE 78624 ( 10.94) 5189982 ( 11.36) 14040 ( 0.10) 5204022 ( 8.68)
Heap2/VACUUM 279 ( 0.04) 16034 ( 0.04) 0 ( 0.00) 16034 ( 0.03)
Heap2/VISIBLE 1182 ( 0.16) 69753 ( 0.15) 24576 ( 0.17) 94329 ( 0.16)
Heap/INSERT 99359 ( 13.83) 7846936 ( 17.17) 450048 ( 3.15) 8296984 ( 13.83)
Heap/UPDATE 1121 ( 0.16) 98550 ( 0.22) 0 ( 0.00) 98550 ( 0.16)
Heap/HOT_UPDATE 298679 ( 41.58) 21519152 ( 47.09) 13504184 ( 94.58) 35023336 ( 58.39)
Heap/LOCK 136656 ( 19.02) 7379429 ( 16.15) 6904 ( 0.05) 7386333 ( 12.32)
Heap/INPLACE 26 ( 0.00) 5028 ( 0.01) 8984 ( 0.06) 14012 ( 0.02)
Heap/INSERT+INIT 641 ( 0.09) 50639 ( 0.11) 0 ( 0.00) 50639 ( 0.08)
Heap/UPDATE+INIT 250 ( 0.03) 22193 ( 0.05) 0 ( 0.00) 22193 ( 0.04)
Btree/INSERT_LEAF 1371 ( 0.19) 87315 ( 0.19) 269200 ( 1.89) 356515 ( 0.59)
Btree/VACUUM 8 ( 0.00) 3042 ( 0.01) 0 ( 0.00) 3042 ( 0.01)
CommitTs/ZEROPAGE 122 ( 0.02) 3660 ( 0.01) 0 ( 0.00) 3660 ( 0.01)
-------- -------- -------- --------
Total 718359 45700107 [76.19%] 14277936 [23.81%] 59978043 [100%]
次数 | TPS | 非FPI | FPI | FPI占比 | WAL总量 |
---|---|---|---|---|---|
1 | 603 | 45967134 | 45967134 | 25.31% | 61546758 |
2 | 494 | 46059408 | 45700107 | 23.81% | 59978043 |
结论:设置fillfactor=90后,生成的WAL量从61546758减少到59978043 。
3.4.3 设置WAL压缩
修改postgres.conf,开启WAL压缩
cpp
wal_compression = on
再次测试
cpp
postgres@db2 pg_wal]$ psql -c "checkpoint;select pg_current_wal_lsn()" ; pgbench -n -c 100 -j 100 -t 1000 ;psql -c "checkpoint;select pg_current_wal_lsn()"
CHECKPOINT
pg_current_wal_lsn
--------------------
5/EBE90698
(1 row)
pgbench (16.1)
transaction type: <builtin: TPC-B (sort of)>
scaling factor: 1
query mode: simple
number of clients: 100
number of threads: 100
maximum number of tries: 1
number of transactions per client: 1000
number of transactions actually processed: 100000/100000
number of failed transactions: 0 (0.000%)
latency average = 202.112 ms
initial connection time = 281.672 ms
tps = 494.775446 (without initial connection time)
[postgres@db2 pg_wal]$ psql -c "checkpoint;select pg_current_wal_lsn()"
CHECKPOINT
pg_current_wal_lsn
--------------------
5/EF94B3E8
(1 row)
(1 row)
生成的WAL统计如下:
cpp
[postgres@db2 pg_wal]$ pg_waldump -t 6 --stats=record -s 5/F0000188 -e 5/F2FFDB80
WAL statistics between 5/F0000188 and 5/F2FFDB80:
Type N (%) Record size (%) FPI size (%) Combined size (%)
---- - --- ----------- --- -------- --- ------------- ---
XLOG/CHECKPOINT_ONLINE 1 ( 0.00) 114 ( 0.00) 0 ( 0.00) 114 ( 0.00)
Transaction/COMMIT 100012 ( 13.85) 3401832 ( 7.40) 0 ( 0.00) 3401832 ( 6.97)
Storage/TRUNCATE 1 ( 0.00) 46 ( 0.00) 0 ( 0.00) 46 ( 0.00)
CLOG/ZEROPAGE 3 ( 0.00) 90 ( 0.00) 0 ( 0.00) 90 ( 0.00)
Standby/LOCK 1 ( 0.00) 42 ( 0.00) 0 ( 0.00) 42 ( 0.00)
Standby/RUNNING_XACTS 11 ( 0.00) 4248 ( 0.01) 0 ( 0.00) 4248 ( 0.01)
Standby/INVALIDATIONS 3 ( 0.00) 414 ( 0.00) 0 ( 0.00) 414 ( 0.00)
Heap2/PRUNE 82271 ( 11.39) 5479079 ( 11.91) 2397301 ( 85.86) 7876380 ( 16.14)
Heap2/VACUUM 198 ( 0.03) 11880 ( 0.03) 0 ( 0.00) 11880 ( 0.02)
Heap2/VISIBLE 155 ( 0.02) 9155 ( 0.02) 244 ( 0.01) 9399 ( 0.02)
Heap/INSERT 99245 ( 13.75) 7840332 ( 17.05) 957 ( 0.03) 7841289 ( 16.07)
Heap/UPDATE 1199 ( 0.17) 112761 ( 0.25) 0 ( 0.00) 112761 ( 0.23)
Heap/HOT_UPDATE 298653 ( 41.37) 21524549 ( 46.79) 31778 ( 1.14) 21556327 ( 44.18)
Heap/LOCK 137773 ( 19.08) 7439742 ( 16.17) 0 ( 0.00) 7439742 ( 15.25)
Heap/INPLACE 20 ( 0.00) 3740 ( 0.01) 3615 ( 0.13) 7355 ( 0.02)
Heap/INSERT+INIT 755 ( 0.10) 59645 ( 0.13) 0 ( 0.00) 59645 ( 0.12)
Heap/UPDATE+INIT 183 ( 0.03) 16131 ( 0.04) 0 ( 0.00) 16131 ( 0.03)
Btree/INSERT_LEAF 1382 ( 0.19) 87602 ( 0.19) 358051 ( 12.82) 445653 ( 0.91)
Btree/VACUUM 6 ( 0.00) 2676 ( 0.01) 0 ( 0.00) 2676 ( 0.01)
CommitTs/ZEROPAGE 122 ( 0.02) 3660 ( 0.01) 0 ( 0.00) 3660 ( 0.01)
-------- -------- -------- --------
Total 721994 45997738 [94.28%] 2791946 [5.72%] 48789684 [100%]
设置`wal_compression = on后,生成的WAL量从59978043减少到48789684。
四、全页写的适用场景与规避策略
4.1 如何判断是否需要优化WAL?
关于如何判断是否需要优化WAL,可以通过分析WAL,然后检查下面的条件,做一个粗略的判断:
- FPI比例高于70%
- HOT_UPDATE比例低于70%
以上仅仅是粗略的经验值,仅供参考。并且这个FPI比例可能不适用于低写负载的系统,低写负载的系统FPI比例一定非常高,但是,低写负载系统由于写操作很少,因此FPI比例即使高一点也没太大影响。
4.2 优化WAL的副作用
前面用到了3种优化手段,如果设置不当,也会产生副作用,具体如下:
- 延长checkpoint时间间隔
导致crash恢复时间变长。crash恢复时需要回放的WAL日志量一般小于max_wal_size的一半,WAL回放速度(wal_compression=on时)一般是50MB/s~150MB/s之间。可以根据可容忍的最大crash恢复时间,估算出允许的max_wal_size的最大值。
- 调整fillfactor
过小的设置会浪费存储空间,这个不难理解。另外,对于频繁更新的表,即使把fillfactor设成100%,每个page里还是要一部分空间被dead tuple占据,不会比设置成一个合适的稍小的fillfactor更节省空间。
- 设置wal_compression=on
需要额外占用CPU资源进行压缩,但影响不大。
五、总结
全页写机制通过"以空间换安全"的策略,成为PostgreSQL抵御部分页写入风险的终极防线。尽管它会增加WAL日志量和I/O开销,但在大多数生产环境中,开启全页写仍是必要选择。