“写笔记”支持四种格式——Word 文档、Excel 表格、Markdown、纯文本,起稿或二次编辑时都能随时切换,同一篇笔记想用哪种形态来记,都由你说了算。
md、txt、csv、json 这类纯文本则原样载入,不做多余加工。拿一张现成的表倒进来、改几笔、再导出去,等于白用一台免费的格式转换器。
要带走就在右上角点“下载”,可导出 PDF、Word、Markdown、Excel、TXT 等格式;列表卡片“⋯”菜单里,也有同样的下载入口。
在“工具”页点“+ 上传工具”即可发布:填好名称与链接,再用 Markdown 把使用方法写清楚——能解决什么问题、怎么装、怎么用,比堆介绍实在。
要分发安装包就一并上传压缩包(ZIP、RAR、7Z、TAR.GZ,最大 35MB),别人在详情页一键下载;只放链接不带附件也可以。
工具按大家的收藏热度排序,好用的自然会被顶上来。发布后可在详情页或卡片菜单里编辑、下架。
写笔记时勾上“隐藏”,这篇就只存在于你自己的账号里:不进列表、不进搜索、不上首页精选,也不会出现在任何公开的页面,链接发给别人同样打不开。
适合放密码、草稿、日记这类只给自己看的内容;想公开,去“发布”打开它,把“隐藏”的勾去掉再保存,之后编辑会默认保持原状态,不会悄悄变回公开。
你的内容会同时保存在多个副本上,系统定期做备份与完整性校验,再配合异地容灾机制:就算某台机器出问题,数据也不会丢,可以长期放心存放;特别重要的资料,仍建议你另外再留一份备份。
全站跑在容器化、模块化的现代架构上,更新、部署、回滚都很快,扩展性和稳定性都按长期运营的标准来设计(Built for reliability, designed to scale)。
这个网站最早只是一个人的笔记仓库,后来慢慢长成现在的知识中枢。设计上很克制——没有广告、没有追踪、没有推荐算法,只是干干净净地存放一些东西;既然做好了,就公开出来,万一有人用得上呢。
不做大而全,不做平台梦,保持简单、保持克制、保持好奇。所有内容都由用户贡献、由用户维护:不会突然冒出付费墙,不会在角落塞广告位,也不会把你的数据卖给第三方。
产品会持续迭代,站内日志页记录着每一次改动,改了什么都有迹可循;想了解这个站是怎么一步步走到今天的,翻翻日志就能看到来龙去脉。
如果在这里看到涉嫌违规的内容,点对应卡片右侧的“举报”按钮就能提交,我们会尽快核实处理;也谢谢你花一点时间,一起把这里维护干净。
预写式日志 — WAL 基础原理
前言:为什么我要写这篇笔记
最近跟几个同事排查线上问题,发现大家对 WAL(Write-Ahead Log)的理解其实挺模糊的。有人觉得它就是 Postgres 的“日志”,有人以为它跟 MySQL 的 binlog 差不多。直到有一次 pg_wal 把磁盘撑爆了,整个集群直接 PANIC 停摆,大家才开始认真研究这东西到底是怎么工作的。
我决定把 WAL 的来龙去脉完整写下来,不是为了背文档,而是希望读完这篇的人能真正理解:Postgres 为什么要先写日志再写数据文件?为什么 pg_wal 满了会导致集群拒绝写入?以及那些常见的“磁盘爆了”的坑,到底是怎么踩进去的。
这篇文章会从原理讲起,然后深入每个细节,最后给出生产环境里常见的故障模式和排查方法。内容有点长,但每个字都是有用的。
一、WAL 是什么?核心原则
WAL 的全称是 Write-Ahead Log,中文叫“预写式日志”。它是 Postgres 保证数据持久性(Durability,ACID 里的 D)的核心机制。
黄金法则:日志先于数据
Postgres 的一条铁律是:任何对堆表(heap)、索引(index)、空闲空间映射(free-space map)、可见性映射(visibility map)的修改,都必须先写入 WAL 并调用 fsync 刷到磁盘,之后对应的数据文件才能被允许刷盘。
这句话是整篇文章的基石,我建议你多读两遍。
为什么要有这个规则?因为如果先写数据文件,系统崩溃了,数据文件可能只写了一半,或者写了错的数据,这时候你没法知道哪些是有效的、哪些是垃圾。但如果 WAL 先写好了,崩溃后可以重新播放 WAL 里的记录,把数据库恢复到崩溃前的状态。
Postgres 官方文档在“Reliability and the Write-Ahead Log”这一章里讲得很清楚。正是这个机制,让一个已经 COMMIT 的事务,即使操作系统立刻崩溃或者断电,也能保证数据不丢。
开发人员什么时候会碰到 WAL?
说实话,大部分开发者平时写 SQL 根本不需要关心 WAL。你写 INSERT、UPDATE、DELETE,Postgres 在背后默默处理好了。但一旦出了故障,WAL 就成了你第一个要查的东西。
最常见的场景:
- pg_wal/ 目录磁盘满了:导致 Postgres 直接 PANIC,拒绝所有写入操作,错误信息是
PANIC: could not write to file ... No space left on device - 崩溃恢复慢:OOM 导致 Postgres 挂了,重启后 recovery 跑了十几分钟,健康检查失败,负载均衡器把流量切走了
- 复制延迟:主库写入正常,从库一直追不上,查了半天发现 WAL 传输有瓶颈
这些都不是“语法错误”那种简单问题,而是需要理解 WAL 内部机制才能解决的。
二、WAL 的工作机制:从 INSERT 到磁盘的完整旅程
这一节我会用一个 INSERT 语句的例子,把 WAL 的整个流程串起来。
2.1 数据页在内存中
Postgres 不会每次 INSERT/UPDATE 都直接写数据文件。数据文件被组织成 8KB 的页面(heap page、index page 等),这些页面平时活在 shared_buffers 里——这是 Postgres 在内存中的一个共享缓冲区池。
当你执行:
INSERT INTO users (name) VALUES ('Alice');
Postgres 会做这些事:
- 在 shared_buffers 里找到(或分配)一个 8KB 的页面,把新数据写进去
- 同时生成一条 WAL record,描述这次修改的细节:record type(比如
XLOG_HEAP_INSERT)、relfilenode(对应哪个表文件)、block number(哪个页面)、payload(具体修改了什么) - 这条 WAL record 被追加到 wal_buffers 里——这是一个独立的共享内存区域,专门用来暂存 WAL 记录
2.2 COMMIT 时发生了什么
最关键的时刻来了。当你执行 COMMIT 时,Postgres 不会立刻把 shared_buffers 里的脏页刷到数据文件。它做的是:
- 调用
XLogFlush()函数,把 wal_buffers 里所有内容(一直到包含 commit 记录的那个字节)write + fsync 到磁盘的 WAL 文件 - 只有 fsync 成功返回后,Postgres 才会在
pg_xact里标记事务已提交 - 然后才给客户端返回“COMMIT OK”
注意顺序:先 fsync WAL,再标记提交,最后回复客户端。这意味着如果 fsync 失败了,客户端会收到错误,事务不会被认为已提交。
2.3 数据页什么时候刷到数据文件?
脏数据页(dirty pages)继续留在 shared_buffers 里,由 checkpointer 进程负责刷盘。checkpointer 是一个后台进程,它周期性地把所有脏页写回数据文件。这个周期和单个事务的提交没有直接关系。
所以 COMMIT 很快——只需要 fsync 一条 WAL 记录,而不是刷整个数据页。数据页的刷盘可以慢慢来,由 checkpointer 在后台处理。
2.4 WAL 的物理组织
WAL 被组织成一系列固定大小的 segment 文件,放在 $PGDATA/pg_wal/ 目录下。默认每个 segment 是 16MB(可以在 initdb 时用 --wal-segsize 参数修改)。
文件名像这样:000000010000000000000001
每个 segment 文件内部是一串连续的 WAL records。Postgres 用 LSN(Log Sequence Number) 来定位 WAL 中的位置。LSN 是一个 64 位整数,显示的时候通常格式化成 XXXX/XXXXXXXX,实际上就是从集群启动开始算的字节偏移量。
LSN 单调递增,是 Postgres 内部唯一信任的“时钟”,用于确定写入顺序。
2.5 查看 LSN 的实用 SQL
-- 当前写入位置
SELECT pg_current_wal_lsn();
-- 示例输出: 0/1A2B3C40
-- 执行一些写入操作
INSERT INTO t SELECT g FROM generate_series(1, 1000) g;
-- 再看 LSN 已经前进
SELECT pg_current_wal_lsn();
-- 示例输出: 0/1A2BE018
-- 计算两次 LSN 之间产生了多少字节的 WAL
SELECT pg_wal_lsn_diff('0/1A2BE018', '0/1A2B3C40') AS bytes_written;
-- 输出: 这个差值就是那 1000 行 INSERT 产生的 WAL 大小
-- 查看当前 LSN 在哪个 segment 文件里
SELECT pg_walfile_name(pg_current_wal_lsn());
-- 示例输出: 000000010000000000000001
2.6 崩溃恢复:WAL 的终极使命
假设 Postgres 突然崩溃了(比如 OOM 被 kill,或者断电)。重启时,Postgres 进入 recovery mode:
- 读取
pg_control文件,找到 redo point——这是最近一次完成的 checkpoint 的 LSN 位置 - 从 redo point 开始,按顺序重放(replay)每一条 WAL record,应用到 shared buffers 里,重建内存中的状态
- 一直重放到 WAL 的末尾(所有有效的 WAL 记录)
结果是:
- 所有在崩溃前已经 COMMIT 的事务,它们的 WAL record 已经被 fsync 到磁盘了,所以会被完整恢复
- 所有还没来得及写 WAL 的修改(比如崩溃时正在执行但未提交的事务),全部丢失
这就是 WAL 的“持久性契约”:只要 WAL record 被 fsync 成功了,数据就安全了。数据文件本身可能还是旧的,但没关系,WAL 里有完整的修改记录,恢复时会重放。
2.7 全页写入(Full-Page Write)
这是一个重要的细节。默认情况下,full_page_writes = on。
为什么需要全页写入?
想象一下:一个 8KB 的数据页,操作系统在刷盘时可能只写了 4KB 就断电了——这叫 torn write(撕裂写入)。这时候磁盘上的页面是一半旧数据、一半新数据,完全是个乱码。
如果 WAL 里只记录了这次修改的 delta(比如“把第 100 字节改成 X”),恢复时在 torn page 上应用 delta,结果就是垃圾。
全页写入的机制是:
- 每次 checkpoint 之后,每个数据页第一次被修改时,Postgres 会把整个 8KB 页面(修改前的完整内容)写入 WAL,而不是只写 delta
- 这样恢复时,如果遇到 torn page,可以直接用 WAL 里的完整页面覆盖掉坏的页面,保证安全
后果:WAL 的体积会在 checkpoint 之后突然增大(因为很多页面第一次修改都要写全页),然后随着页面变“热”(已经写过一次全页了),后续只写 delta,WAL 体积回落。所以你在监控里会看到 WAL 写入速率有周期性波动,跟 checkpoint 频率相关。
三、生产环境里最常见的故障模式
理论知识说完了,现在聊聊我亲眼见过(或者同事踩过)的坑。
故障模式 1:pg_wal/ 被 replication slot 撑爆
这是最常见、也最坑的一个。
背景:Replication slot(无论是逻辑复制还是物理复制)会强制 Postgres 保留从 slot 的 restart_lsn 开始的所有 WAL segment,直到 consumer 确认已经消费完。如果 consumer 挂了、或者你创建了一个 slot 但从来没消费、或者 Debezium connector 被暂停了无限期——WAL 就会一直堆积。
max_wal_size 参数在这种情况下是 软限制(soft target),Postgres 不会因为 WAL 超过了这个值就强制删除——因为 slot 在 pin 着这些 WAL,删了 consumer 就没法恢复了。
最终结果:pg_wal/ 目录越来越大,直到磁盘空间用完。Postgres 会 PANIC 并 shutdown,而且因为磁盘满了,重启都起不来。
错误的做法:
-- 为了测试创建了一个 slot,用完忘了删
SELECT pg_create_logical_replication_slot('debug_cdc', 'pgoutput');
-- consumer 跑了几分钟就停了,但这个 slot 永远 pin 着 WAL
正确的做法:
首先,检查当前所有 slot 的状态:
SELECT slot_name, active, restart_lsn,
pg_size_pretty(pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn)) AS retained
FROM pg_replication_slots;
active 为 false 的 slot 就是潜在的风险点。retained 列显示这个 slot 导致 WAL 保留了多少数据。
其次,Postgres 13+ 提供了救命参数——max_slot_wal_keep_size:
-- 设置 slot 最大保留 WAL 大小为 20GB,超过这个值的 slot 会被自动 invalidate
ALTER SYSTEM SET max_slot_wal_keep_size = '20GB';
SELECT pg_reload_conf();
这个参数的意思是:如果一个 replication slot 导致 WAL 保留超过 20GB,Postgres 会把这个 slot 标记为 invalid,然后释放 WAL 空间。代价是那个 consumer 需要重新同步(resync),但总比整个集群挂掉好。
我的建议:只要用了 replication slot,就一定要设置 max_slot_wal_keep_size。宁可丢一个 consumer,不能丢整个集群。
故障模式 2:archive_command 静默失败
如果你开启了 WAL 归档(archive_mode = on),Postgres 会在 WAL segment 写满后,调用 archive_command 把它复制到远程存储(比如 AWS S3、wal-g 等)。Postgres 只有在 archive_command 返回退出码 0 时,才会认为这个 segment 已经归档成功,然后可以安全地回收它。
问题来了:如果 archive_command 一直失败(比如 AWS 凭证过期、bucket 权限变更、网络不通),那些 WAL segment 就会一直留在 pg_wal/ 目录里。Postgres 本身不会因为这个停止服务,它只是在日志里反复打印 archive command failed with exit code N。
但如果没人看日志,或者告警没配好,磁盘最终会被撑满。这时候就跟故障模式 1 一样了——集群 PANIC。
GitLab 2017 年那次著名的数据库 outage,postmortem 里就专门提到了这个问题:多个保护机制(备份、复制、WAL 归档)同时静默失效,没有任何告警触发。最后磁盘满了,数据库挂了。
教训:监控 archive_command 的失败次数,设置告警。不要假设归档永远正常工作。
故障模式 3:误解“Postgres 不写数据文件”
我见过有开发用 strace 跟踪 Postgres 进程,发现数据文件很少被写入,就得出结论:“Postgres 太懒了,数据根本没持久化。”
这是完全错误的。持久化靠的是 WAL 的 fsync,不是数据文件的 fsync。数据文件由 checkpointer 在后台刷盘,频率低得多。如果只看数据文件的写入频率来判断持久性,你会得出相反的结论。
更危险的是,有人会因此“优化”——比如关掉 fsync、关掉 full_page_writes、或者把 synchronous_commit 设为 off,以为能提升性能。结果一次断电,数据损坏到无法恢复。
Postgres 文档对 fsync = off 的警告原文是:“may result in unrecoverable data corruption”。这不是吓唬人,是真的会。
故障模式 4:Checkpoint 后的 WAL 尖峰导致复制延迟
前面说了,checkpoint 之后,每个“热”页面第一次被修改都会触发全页写入(FPW),产生 8KB 的 WAL 记录。如果一个表正在被大量 UPDATE,checkpoint 后的几十秒内,WAL 的产生速率可能飙升好几倍。
这个尖峰传到网络层面,会导致主库到从库的 WAL 传输出现瓶颈,复制延迟(replication lag)瞬间跳高。
如果你的应用依赖读副本来做“写后读”(比如用户下单后立即刷新页面看到订单),那每隔几分钟(跟 checkpoint 周期一致)就会有一批用户看到“订单不见了”。
解决方法:
- 把
checkpoint_timeout调大(比如从默认的 5 分钟改成 30 分钟甚至 1 小时),减少 checkpoint 频率 - 增大
max_wal_size,让 checkpoint 不要因为 WAL 大小而被触发 - 开启
wal_compression,全页写入的内容可以被压缩,减少 WAL 体积
故障模式 5:崩溃恢复时间过长
崩溃恢复需要从最近的 redo point 开始,重放所有 WAL record。如果两次 checkpoint 之间的 WAL 量很大(比如你为了减少 FPW 尖峰把 checkpoint_timeout 调大了,但写入负载又很重),那崩溃后 recovery 可能要 replay 好几个 GB 的 WAL。
这就意味着:几分钟到十几分钟的服务不可用。健康检查会失败,负载均衡器会切流量,如果用的是 Kubernetes,Pod 会被重启,然后陷入“启动→恢复中→超时→重启”的死循环。
这是一个直接的 tradeoff:
- 频繁 checkpoint → FPW 增多,WAL 写入放大,复制延迟风险增加
- 不频繁 checkpoint → 恢复时间(RTO)变长
没有完美的设置,只能根据业务需求权衡。
四、如何监控和诊断 WAL 相关问题
4.1 该看什么症状
- pg_wal/ 目录持续增大,远超 max_wal_size → 检查 replication slot 和 archive 状态
- 日志里有
archive command failed→ 归档链路有问题 - 日志里有
checkpoints are occurring too frequently→ checkpoint 太频繁,可能导致 FPW 放大 - 复制延迟周期性跳变,周期跟 checkpoint_timeout 吻合 → 大概率是 FPW 尖峰导致
- 崩溃后启动日志显示
redo starts at ...到redo done at ...间隔很长 → RTO 太长,需要优化 checkpoint 策略
4.2 核心监控 SQL
-- 查看当前 WAL 写入、刷盘、插入的位置
-- insert_lsn: 当前正在写入 WAL 的位置
-- flush_lsn: 已经 fsync 到磁盘的位置
-- write_lsn: 已经写入 OS 缓存但还没 fsync 的位置
SELECT pg_current_wal_lsn() AS insert_lsn,
pg_current_wal_flush_lsn() AS flush_lsn,
pg_current_wal_insert_lsn() AS write_lsn;
-- 计算 WAL 产生速率(bytes/sec)
-- 先记下 lsn1 和 t1,过一段时间再记 lsn2 和 t2
SELECT pg_wal_lsn_diff(:lsn2, :lsn1) / EXTRACT(EPOCH FROM (:t2 - :t1)) AS bytes_per_sec;
-- 查看 pg_stat_wal(PostgreSQL 14+)
-- 这个视图提供了很多 WAL 相关的统计信息
SELECT * FROM pg_stat_wal;
-- 查看 checkpoint 相关的统计
SELECT * FROM pg_stat_bgwriter;
4.3 几个实用参数速查
| 参数 | 默认值 | 说明 |
|---|---|---|
wal_level |
replica | 决定 WAL 包含多少信息。minimal 不支持归档和复制 |
fsync |
on | 关掉它你就等着数据损坏吧 |
synchronous_commit |
on | off 会更快但可能丢最近的事务 |
full_page_writes |
on | 防止 torn write 导致的数据损坏 |
wal_compression |
off | 建议开启,减少 WAL 体积 |
checkpoint_timeout |
5min | checkpoint 间隔时间 |
max_wal_size |
1GB | 软限制,WAL 超过这个值会触发 checkpoint |
min_wal_size |
80MB | WAL 保留的最小值 |
max_slot_wal_keep_size |
-1 (无限制) | 每个 slot 最大保留 WAL 大小,13+ 可用 |
wal_buffers |
-1 (自动) | WAL 缓冲区大小,写入频繁可适当调大 |
五、总结
WAL 是 Postgres 持久性的基石。理解它的工作原理,能帮你:
- 避免磁盘爆满导致集群停摆
- 正确评估 checkpoint 频率和恢复时间的关系
- 排查复制延迟问题
- 不被“Postgres 不写数据文件”这种误解带偏
记住最核心的一句话:WAL 先 fsync 成功,数据才算安全。数据文件可以慢点刷,但 WAL 必须立刻刷。
如果你在生产环境遇到过其他 WAL 相关的坑,欢迎分享。我也还在不断学习中。