第一拍:源头封口(产生物理载荷)
- 物理现场:业务在疯狂写入,
pg_wal目录下的一个文件(比如叫000000010000000A000000FF)刚好写到了第 16777216 个字节。 - 物理动作:负责写日志的
WAL Writer进程或者前台业务进程,调用底层的close()函数,把这个 16MB 的文件彻底封口。从此以后,这个文件变成了**“只读的物理标本”**。
🎬 第二拍:生成“发车信号”(极其关键的防呆设计)
- 前置逻辑:文件封口了,怎么通知归档进程去干活?如果用内存发消息,万一瞬间断电,消息就丢了,这个文件就永远不会被归档了。
- 物理动作:PG 内核极其老辣地采用了**“落盘即真理”**的法则。内核直接在
pg_wal/archive_status/这个子目录下,创建一个大小为 0 字节的空文件,名字叫做:000000010000000A000000FF.ready - 定性:这个
.ready文件就是一张“押运传票”。只要它存在于硬盘上,就代表对应的 16MB 日志需要被运走。断电重启也不怕,因为传票在硬盘上。
🎬 第三拍:归档进程苏醒(Archiver 介入)
- 物理现场:后台一直处于休眠(Sleep)状态的
Archiver(归档进程),被内核的内部信号唤醒,或者它自己按周期醒来了。 - 物理动作:它第一件事根本不是去看日志文件,而是直接去扫描
archive_status/目录。 - 数据得出:它一眼就看到了那个闪闪发光的
.ready文件。它立刻知道:“来活了,目标是000000010000000A000000FF。”
🎬 第四拍:拼装押运指令(执行 archive_command)
- 前置逻辑:归档进程自己是不会搬运文件的,它必须依靠你(DBA)写在
postgresql.conf里的archive_command。 - 物理动作:假设你写的命令是
cp %p /mnt/nfs_backup/%f。 归档进程开始做底层的字符串替换(Macro Substitution):- 它把
%p替换成这个日志的绝对物理路径(如/var/lib/pgsql/data/pg_wal/00000001...)。 - 它把
%f替换成这个日志的纯文件名(如00000001...)。
- 它把
- 执行:拼装完成后,归档进程向 Linux 操作系统发起
system()调用,把这串拼好的命令扔给操作系统去执行。
🎬 第五拍:生与死的审判(死等操作系统的 Return Code)
- 前置逻辑:命令扔出去了,归档进程怎么知道运成功没有?它绝对不看目标端到底有没有这个文件,它只认死理——看 Linux 返回的退出状态码(Exit Code/Return Code)。
- 物理动作(分岔路口):归档进程死死盯着刚才执行的那个
cp命令的返回值。- 分支 A(物理大圆满 – 返回值是 0): 如果 Linux 告诉它命令执行成功(返回 0)。归档进程立刻掏出
rename()函数,把那个 0 字节的.ready文件,重命名为.done(000000010000000A000000FF.done)。 此时,归档的物理闭环彻底宣告完成! - 分支 B(死亡螺旋 – 返回值非 0): 如果目标 NAS 满了,或者网络断了,
cp命令报错了(返回非 0)。 归档进程会立刻在数据库的错误日志里大吼一声(报错),然后它会绝不妥协地进行无限次重试! 只要没成功,那个.ready文件就绝对不会变成.done。
- 分支 A(物理大圆满 – 返回值是 0): 如果 Linux 告诉它命令执行成功(返回 0)。归档进程立刻掏出
🎬 第六拍:闭环与交接(Checkpointer 的入场)
- 前置逻辑:归档进程运完了,但它没有权力去删除原来的 16MB 文件。它只负责把
.ready变成.done。 - 物理动作:就像我们上一局推演的,等到
Checkpointer(检查点进程)来清理磁盘时,它看到了这个.done文件,它才知道:“哦,归档兄弟已经运走了,这个 16MB 的实体文件我可以安全地拿去**回收(Recycle)或者删除(Unlink)**了。”
架构级物理闭环总结
你看到了吗?没有任何思维跳跃。 生成 16MB 载荷 -> 产生 .ready 信号 -> 唤醒归档进程 -> 替换 %p %f 执行外部命令 -> 强校验退出码为 0 -> 重命名为 .done -> 最终交给检查点进程去回收物理空间。
🧬 终极补充一:架构的物理跃迁 —— 抛弃 archive_command,拥抱 archive_library (PG 15+ 革命)
我们刚才推演第四拍时提到,归档进程会拼装一个命令,然后调用操作系统的 system() 去执行(比如 cp)。
- 传统架构的致命瓶颈(物理开销): 在 Linux 底层,调用
system()意味着必须执行fork()和exec()。也就是说,每归档一个 16MB 的文件,操作系统都要去克隆一个进程、分配独立的内存空间、加载 Shell 环境。如果高并发下每秒产生 10 个 WAL 文件,这种疯狂的fork()会极其严重地消耗 CPU 算力,甚至引发系统的 Context Switch(上下文切换)风暴! - 新世代的降维打击(
archive_library): 从 PostgreSQL 15 开始,内核研发团队彻底忍不了这种粗暴的玩法了。他们引入了archive_library。 物理逻辑:你不再需要写那些笨重的 Shell 脚本了。原厂架构师可以直接用 C 语言写一个归档模块,编译成动态链接库(.so文件)。PG 启动时,直接把这个.so加载到归档进程的同一块内存地址空间里! 当.ready文件生成时,归档进程直接在内存里调用 C 函数指针,把 16MB 的二进制流通过内部 API 直接推送到灾备端(比如 S3 对象存储 API)。 - 定性:零
fork()开销,没有跨进程通信损耗! 这是从“外部挂载武器”到“内置原生主炮”的绝对物理进化!
📊 终极补充二:内存级的法医探针 —— pg_stat_archiver 视图的底层映射
考官一定会问:“你刚才说归档失败了会无限重试,那作为 DBA,我怎么在它把磁盘撑爆之前,精准狙击到这个故障?”
- 常规 DBA 的土办法:写个 Shell 脚本去
pg_wal/archive_status目录下ls | grep .ready | wc -l,看堆积了多少个文件。这极其业余且消耗 I/O。 - 你的高阶玩法(内存直读): “我会直接查询系统内部的
pg_stat_archiver内存视图! 在 C 语言底层,PG 在共享内存里维护了一个叫做PgStat_MsgArchiver的结构体。归档进程每成功一次,就会在内存里把archived_count加 1,并记录last_archived_wal;只要失败一次,哪怕只失败了半秒,就会立刻把failed_count加 1,并刻下last_failed_wal及其时间戳。 定性:作为高阶架构师,排障绝对不依赖外部的、滞后的文件系统扫描,而是直接读取共享内存中被内核实时更新的状态机统计量!”
了解 www.876873.xyz 的更多信息
订阅后即可通过电子邮件收到最新文章。
wal日志的归档进程 工作原理,逻辑条理 的推演:等您坐沙发呢!