小心!孤儿归档也可能将数据库整死!
前言
昨天①群有位朋友问了这样一个问题
如果归档进程当前正在处理 1.27 16:00 的日志,我手动把 WAL 日志清理到只保留1.29,归档进程去处理 WAL 日志的时候发现它不存在了会怎么样?它是会报错反复重试被删除的 WAL 日志,还是报错一段时间之后它会跳到 1.29 的 WAL 日志去处理?
当前归档进程处理太慢了,已经落后两天了,想手动清掉一些 WAL 日志,但不确定会有什么影响。
赶巧,今天③群也有位老铁问了个类似的问题:
还没有归档的 WAL 日志被删除了,导致归档一直在找某一个删除了的 WAL,一直归档失败,这种该怎么处理?
本身 PostgreSQL 的归档就由于单进程架构,广为诟病,并且由于各种各样的原因,比如 archive_command 超时,需要遍历目录找到最老的 WAL 日志等等。关于后者,不难想象,如果有成百上千个待归档日志,分分钟就会给你堆积成百上千个进而撑爆磁盘。因此在 15 版本中做了一个优化,将 64 个带归档日志作为一个"批次"存入到一个数组中,处理完这一批之后再去扫描,这样就可以减少遍历目录 archive_status 的次数,同时 15 版本新增了 Archive* 相关的等待事件,进一步提升归档类的可观测性能力。
扯远了,回到群友的问题,如果需要被归档的 WAL 不见了,但是又存在 ready 文件的话,PostgreSQL 会如何处理?
分析
不难想象,如果 PostgreSQL 一直反复重试去归档已经不存在的 WAL,那么按照其逻辑,后续所有正常的 WAL 都无法归档,时间一久,堆积的 WAL 就会越来越多,磁盘就会被撑爆。那社区开发人员有没有考虑到这个问题?
昨天仅仅走马观花,快速浏览了一下相关代码,其中有一段逻辑如下:
注释很清晰
Since archive status files are not removed in a durable manner, a system crash could leave behind .ready files for WAL segments that have already been recycled or removed. In this case, simply remove the orphan status file and move on. unlink() is used here as even on subsequent crashes the same orphan files would get removed, so there is no need to worry about durability.
由于归档状态文件没有以持久的方式被移除,系统崩溃可能会留下已经被回收或移除的 WAL 段的 .ready 文件。在这种情况下,简单地移除孤立的状态文件然后继续执行即可。这里使用unlink(),因为即使在随后的崩溃中,相同的孤立文件也会被移除,所以不需要担心持久性问题。
简而言之,在归档的过程中如果发现了有 ready 文件,但是相应的 WAL 已经被移除了的话,那么会输出一条告警,并移除这个孤儿状态文件。说明社区也考虑到了这么一个场景:
那是什么版本中合入的呢?搜了一下
原来是 12 版本之后才有的逻辑。
复现
现在让我们简单验证一下,方式也很简单,在 archive_command 中指定睡眠一阵即可,通过 GDB 方式较为复杂。
postgres=# show archive_command ;
archive_command
-----------------------------------------------------
date; sleep 60; cp %p /home/postgres/archive_dir/%f
(1 row)postgres=# insert into t1 values(3);
INSERT 0 1
postgres=# select pg_switch_wal();
pg_switch_wal
---------------
0/BA000438
(1 row)
现在已经有了状态文件
[postgres@xiongcc pg_wal]$ ll archive_status/
total 0
-rw------- 1 postgres postgres 0 Jan 30 14:57 0000000100000000000000BA.ready
然后让我们手动移除这个 WAL
[postgres@xiongcc pg_wal]$ mv 0000000100000000000000BA 0000000100000000000000BA_bak
归档进程默认每 60 秒唤醒一次,所以当 60 秒之后,就会发现此孤儿文件,然后将其移除
2024-01-30 14:58:41.608 CST,,,13749,,65b89440.35b5,3,,2024-01-30 14:16:32 CST,,0,LOG,00000,"archive command failed with exit code 1","The failed archive command was: date; sleep 60; cp pg_wal/0000000100000000000000BA /home/postgres/archive_dir/0000000100000000000000BA",,,,,,,,"","archiver",,0
2024-01-30 14:58:42.609 CST,,,13749,,65b89440.35b5,4,,2024-01-30 14:16:32 CST,,0,WARNING,01000,"removed orphan archive status file ""pg_wal/archive_status/0000000100000000000000BA.ready""",,,,,,,,,"","archiver",,0
那么回到这个问题中来,我问了这两位群友,尴尬的是,一个版本是 10,一个版本是 11,都没有这个逻辑...因此都会不断反复重试,直到磁盘撑爆。但是既然我们已经分析清楚了其中逻辑,可以手动 dummy 一个伪日志,让其归档,或者临时修改归档命令,或者移除 ready 和相应的日志,亦或是自己在 archive_command 中使用更为复杂的脚本或命令来判断都可以。
另外一个有趣的问题是,如果我将 ready 文件移除了,会怎样?结果是,PostgreSQL 会正常归档
[postgres@xiongcc pg_wal]$ ll archive_status/
total 0
-rw------- 1 postgres postgres 0 Jan 30 15:02 0000000100000000000000BC.ready_bak
[postgres@xiongcc pg_wal]$ ll ../../archive_dir/
total 32768
-rw------- 1 postgres postgres 16777216 Jan 30 15:00 0000000100000000000000BB
-rw------- 1 postgres postgres 16777216 Jan 30 15:03 0000000100000000000000BC --正常
只不过会打印一条告警 (这是 16 里面的现象)
2024-01-30 15:03:32.635 CST,,,13749,,65b89440.35b5,5,,2024-01-30 14:16:32 CST,,0,WARNING,58P01,"could not rename file ""pg_wal/archive_status/0000000100000000000000BC.ready"" to ""pg_wal/archive_status/0000000100000000000000BC.done"": No such file or directory",,,,,,,,,"","archiver",,0
创建了 ready 文件之后,还会向归档进程发送一个信号以唤醒它 (向归档进程发送一个 SIGUSR1的信号)。现在,归档进程便可以启动并开始处理所有的 .ready 文件,也可能是 15 之后的批次逻辑,已经记录在了数组之中。感兴趣的读者可以研究下这块的代码逻辑。
小结
所以,如果待归档的 WAL 不在了,这个问题怎么解?
12 以后,PostgreSQL 会自动移除相应的孤儿归档状态文件,然后归档下一个 WAL 12 以前,需要人工介入,手动处理,否则可能会将磁盘撑爆
当然,常规的归档失败还是会不断重试。另外,也再次说明了不到万不得已,不要手动去删除 WAL,虽然 PostgreSQL 自作主张给你移除了 ready 文件,"觉得"其是一个孤儿文件,殊不知这是人为操作导致的,但是貌似也确实没有其他更好的方式。
推荐阅读
Feel free to contact me
微信公众号:PostgreSQL学徒 Github:https://github.com/xiongcccc 微信:_xiongcc 知乎:xiongcc 墨天轮:https://www.modb.pro/u/39588