PostgreSQL WAL 幂等性详解
1. WAL简介
WAL(Write-Ahead Logging)是PostgreSQL的核心机制之一。其基本理念是:在修改数据库数据页之前,必须先将这次修改操作写入到WAL日志中。
这确保了即使发生崩溃,数据库也可以根据WAL日志进行恢复。
恢复的核心流程就是不断**Replay(重做)**WAL记录,把数据库恢复到一致状态。
2. 为什么需要幂等性?
故障恢复可能发生在任意时刻,且可能反复进行。为了确保数据一致性,必须保证:
- WAL重放(replay)是幂等的:即多次重做同样的WAL记录,最终结果与只做一次是一样的。
- 防止重复执行逻辑修改,避免出现脏数据(如重复插入、更新错误等)。
没有幂等性,PostgreSQL的可靠性和数据完整性就无从谈起。
3. PostgreSQL 如何实现 WAL 的幂等性?
分情况讨论:
3.1 物理日志(Full Page Write, FPW)
- 当首次修改某个page时,如果遇到检查点之后第一次修改,或者出现了部分写(partial write)风险时,PostgreSQL会将整个page的物理镜像写入WAL。
- 恢复时直接拷贝覆盖,无需考虑page上的状态。
- 因为是整页覆盖,所以天然是幂等的。重复应用多次,结果不会变。
注:FPW是恢复的"大杀器",确保即使发生中间页损坏,也能恢复。
3.2 逻辑日志(Insert/Update/Delete操作记录)
- 日志并不是直接记录"修改了page的哪一块物理位置",而是记录在什么page上执行了什么逻辑操作。
- 例如:在某个page上插入一条tuple "X",而不是"在page偏移offset=32的地方插入"。
- 如果简单地无脑重做,恢复两遍就会出现重复记录。
为避免此问题,PostgreSQL每个page在物理结构中维护了一个字段:pd_lsn,即Page LSN。
- 当一个page被修改时,它的pd_lsn被更新为当前WAL record的LSN。
- 恢复时,每次要重放一个WAL record之前,PostgreSQL会先检查:
只有当 WAL record 的 LSN > page 的 pd_lsn 时,才进行重做,否则跳过。
3.3 流程示意图
恢复 -> 读取WAL record -> 找到要修改的Page ->
比较 (WAL record LSN vs Page pd_lsn)
-> 如果 WAL LSN <= Page LSN, 说明已经做过,跳过
-> 如果 WAL LSN > Page LSN, 执行修改,更新Page LSN
这种机制可以保证:即使崩溃后在同一个地方反复重做日志,也不会对数据造成破坏。
4. 实验验证 PostgreSQL WAL 幂等性
下面用一个小实验验证pg的WAL幂等性:
4.1 实验环境准备
initdb -D /tmp/pgwaltest
pg_ctl -D /tmp/pgwaltest -l logfile start
psql
4.2 创建表并插入数据
CREATE TABLE test_wal (id serial PRIMARY KEY, val text);
INSERT INTO test_wal(val) VALUES('first insert');
CHECKPOINT; -- 强制生成检查点,防止混入无关WAL
这时test_wal表中有一条数据,系统生成了新的检查点。
4.3 查看Page LSN
PostgreSQL提供了扩展 pageinspect 来查看物理Page的信息。
安装并使用:
CREATE EXTENSION pageinspect;
-- 找出表的物理文件
SELECT relfilenode FROM pg_class WHERE relname = 'test_wal';
-- 假设relfilenode是 16384
-- 查看page的LSN(第一页)
SELECT * FROM page_header(get_raw_page('test_wal', 0));
会返回:
lsn | checksum | flags | lower | upper | special | pagesize | version | prune_xid
------+----------+-------+-------+-------+---------+----------+---------+-----------
0/1500720 | ... | | ... | ... | ... | 8192 | 4 | ...
记录下当前Page的LSN,比如是0/1500720。
4.4 模拟崩溃后重复恢复
手动重放WAL当然很复杂,但可以模拟:
- 手动修改page LSN,使其低于WAL日志的LSN;
- 重启数据库,触发recovery。
不过这样太麻烦,我们可以简单理解为:
- 如果page LSN已经大于WAL record的LSN,那么即使WAL被Replay,也不会改动页面。
4.5 直接实验重复写入一条相同数据:
-- 模拟意外重复插入
INSERT INTO test_wal(val) VALUES('first insert');
-- 应该报错,因为违反唯一约束(id列),而不是"无脑"插两遍
说明PostgreSQL在逻辑层也有幂等保护,比如:
- 主键/唯一约束
- page LSN校验
- XID(事务ID)检测
5. 注意事项和补充
- FPW(Full Page Writes)必须打开,否则遇到partial write会导致崩溃后无法恢复。
- 极端情况下,日志丢失(比如WAL损坏)仍然会导致恢复失败,因此建议使用流复制+归档备份双保险。
- WAL日志管理工具(如pg_waldump)可以辅助查看日志内容,帮助理解。
示例:
pg_waldump /tmp/pgwaltest/pg_wal/
可以看到诸如:
rmgr: Heap len (rec/tot): 54/ 54, tx: 490, lsn: 0/01500280, desc: INSERT off 2
显示了一个Heap表上的insert动作及其LSN。
总结
- PostgreSQL WAL重放过程是严格幂等的。
- 物理日志天然幂等,逻辑日志依赖于Page LSN机制保证幂等。
- 实现细节涵盖了page级别和事务级别的双重校验。
- 实验验证了幂等机制的有效性。
PostgreSQL通过这些设计,确保即使在最坏情况下崩溃恢复,也能保证数据一致性和正确性。