工作笔记-一次PG WAL爆盘的处理

300 阅读6分钟

概述

笔者最近在工作中,遇到了一次服务宕机的故障。

宕机的原因,和应用系统本身没有直接的关系,而是Postgres数据库服务停机,导致应用系统出现错误。而数据库宕机的原因,也非常简单: 磁盘空间满了。

这是一个不是很重要和很关键的应用系统,只是一个普通的业务应用管理系统,所以分配的资源也不是那么充裕,只是一般的可用的水平。而且当时在资源规划的时候,磁盘的规划也不甚合理。这是一个虚拟机系统,磁盘分配了大约70G,但在安装操作系统的时候,还分为了两个分区40G(/)+30G(Home)。数据库系统的主数据目录在主分区中,其实只有40G左右的工作空间,而且还需要和操作系统共享。剩余的30G空间,除了分给作为用户工作目录之外,其实没有实际的用途。

所幸的是,虽然数据库服务被迫停机,但操作系统仍然是可用并且能够正常访问的,笔者可以通过SSH进行远程的管理和操作。经过简单的排查(使用DF和DU命令),笔者看到主分区,已经被 /pgwal 文件夹占满(30G),另外在主数据文件夹中,还有一些数据库备份的文件(大约6G)。

作为应急措施,笔者先将数据库备份文件,移动到 /home 分区当中,这样在主系统分区中,腾出了6G的可用空间。然后可以正常的启动Postgres服务,先让生产系统上线提供服务。但显然,这只是一个临时的处理方式,事后也证明了这一判断。随后两天,笔者通过跟踪监控系统运行状态,发现每天的主数据分区磁盘占用都会增加200M左右,这样6G的空间显然是不敷使用的。需要寻找一个更稳定长久的处理方案。

所以,现在的问题是:

1 为什么会出现WAL文件爆盘的情况

2 如何解决或者缓解这个问题

WAL

WAL (Write-Ahead Logging,预写日志) 是PostgreSQL中的一个重要核心的数据管理和操作机制,它用于确保数据库的可靠性和持久性。它的定义和名称体现了其核心思想,就是在对数据文件进行实际修改之前,必须先将修改的内容记录到日志中。这里,日志的本质,就是一系列严格有序的操作和数据记录。理论上而言,可以基于一个初始状态,通过回放日志操作,就可以到达一个确定的数据状态。

这样,通过日志系统,数据库就可以提供诸多有益的特性,包括:

  • 崩溃恢复:如果物理损坏或者数据库崩溃,PostgreSQL 可以通过回放 WAL 日志来恢复到崩溃前的状态
  • 数据一致性:确保即使在系统崩溃的情况下也能保持数据的一致性
  • 支持复制:WAL日志可以被发送到备用服务器,然后通过重放日志来实现数据库复制,这是实现主从模式和数据库群集的重要基础
  • 性能优化:将随机写转换为顺序写,来大幅提高IO效率
  • 事务支持:事务提交时,变更批量写入磁盘
  • 数据备份和迁移:可以用于增量备份和还原

关于WAL的细节原理和实现,不是本文重点要讨论的内容。这里读者只需要理解,WAL是现代关系数据库系统的一个重要的功能特性,并且它会影响到数据库系统的正常运行。

问题和检查

前面的章节,我们已经了解到,在数据库系统运行的过程中,会不断的生成日志记录。但是在正常的情况下,这并不是一个太大的问题,因为它提供了一种归档机制,当WAL文件被正常处理之后,系统会对其进行清理,来保证系统的正常运行。

笔者的系统并不是一个很大的系统,真正的业务数据每年也就是几个G,正常运行情况下,当前的配置不会有太大的限制。在笔者的系统中,WAL的归档模式是打开的,也就是说,它应当进行归档操作。但不知道什么原因,这个系统的归档操作出现了问题,WAL文件就开始堆积,加上WAL写放大的效应,在差不多一年左右的时间内,WAL目录增长到差不多30G左右,终于撑爆了系统磁盘空间。这就是我们看到pgwal文件夹异常大的原因。

在看到了这个情况之后,笔者进一步分析了造成这个状况的原因。使用了如下工具和命令:

// 查询 postgres 主数据文件夹
-bash-4.2$ pwd
/pgsql_data/data

// 查询数据和WAL磁盘占用 (清理后的)
-bash-4.2$ du -h -d 1
4.1G	./pg_wal
608K	./global
...
4.7G	./base
...

// 查看postgres系统日志信息
-bash-4.2$ cat log/postgresql-Sun.log 
The failed archive command was: cp pg_wal/0000000100000029000000B4 /pgsql_data/backups/xlogs/0000000100000029000000B4


// 查询归档模式和命令
postgres=# show archive_mode;
 archive_mode 
--------------
 on
(1 行记录)


postgres=# show archive_command;
          archive_command           
------------------------------------
 cp %p /pgsql_data/backups/xlogs/%f

// 查询归档失败信息
postgres=# select * from pg_stat_archiver;
 archived_count |    last_archived_wal     |      last_archived_time       | failed_count |     last_failed_wal      |       last_failed_time 
       |          stats_reset          
----------------+--------------------------+-------------------------------+--------------+--------------------------+------------------------
-------+-------------------------------
           1292 | 000000010000003000000064 | 2025-01-13 15:59:36.222835+08 |         9378 | 000000010000002B00000075 | 2025-01-13 15:56:06.300
459+08 | 2025-01-11 10:11:57.674069+08
(1 行记录)

// 查询当前WAL编号
postgres=# select pg_walfile_name(pg_current_wal_lsn());
     pg_walfile_name      
--------------------------
 0000000100000033000000E9

经过以上的操作和分析,笔者能够得出一个初步的结论。就是通过查看日志文件,发现Archive复制命令出现了错误。导致系统一直在重试这个命令,就是说,系统的归档操作被卡在一个文件的复制操作异常之上。然后查看为什么这个文件的操作会失败,有点无语,就是这个文件是不存在的?!由于这个错误,归档操作不能顺利进行,所有的WAL文件都被累积下来,占满了磁盘空间。

处理方式

明确了上述的错误原因后,就可以着手处理问题的。但其实问题并没有那么简单。

首先是,这个文件去哪里了? 答案是: 不知道。笔者接手这个项目的时候,状态就是这样的。其实不光是这个文件,后面我们会看到是一系列文件。

其次,当前可以看到,当前的WAL文件编号,很多WAL文件都比它旧,就是理论上可以清除的。正规的处理方式是找到当前的WAL文件,然后使用pg_cleanup命令,来清理比较旧的文件。但笔者的系统是PG10,没有这个配套的清理命令。

最后,就是这种情况,在丢失WAL文件的情况下,可能需要重置WAL状态,这需要停止和重启PG系统...

而且,原有的设计归档方式,归档文件夹和pgwal文件夹是在同一个分区当中,所以这样的复制,对于磁盘占用是没有什么帮助的。

所以,在正式开始之前,笔者需要重新规划一个归档文件夹。笔者在另一个磁盘分区中,创建了一个xlogs文件夹,作为归档文件夹,然后将归档命令中,原来的目标文件夹通过软链接指向了这个文件夹。这样表面上还是在pg数据文件夹中,实际的存储已经在另一个磁盘分区中了。然后笔者模拟执行了PG中定义的复制命令行操作,保证命令能够正确执行。

在定义好目标归档文件夹之后,就可以着手处理实际的归档操作的文件错误的问题了。又经过深入的分析和查询,笔者了解到,其实这个archive操作,可能并没有我们认为的那么复杂。按照技术文档的说明,它会执行这个归档命令操作,其实就是一个简单的shell复制命令,如果有一个正常的操作结果,就会认为归档操作是成功的。也就是说,它其实并不关心这个文件的实质内容是什么。而失败的时候,它会获得一个错误的系统返回状态,它会记录这个错误,并在后面进行重新尝试。

为了验证这个想法,笔者做了一个简单的实验,就是新创建了一个同名的空文件,放在了pgwal文件夹中。等到了一会儿,再去查看这个文件,发现它确实被复制到归档目标文件夹中了。而且再次检查归档状态,发现原有的错误确实已经不在了。当然,由于笔者的环境中,其实是缺了很多的WAL文件的,系统内部的错误提示,就变成了一个新的文件错误。

这里简单的解释一下WAL文件的命名规则,可以帮助读者更好的理解这个问题。通常情况下,一个WAL的文件名称是这样的:

000000010000002B00000075

看起来挺复杂,其实有很强的规律。它其实是三个组成部分。00000001,是第一部分;0000002B,是第二部分;00000075是第三部分。查看所有WAL文件会发现,第三部分其实没有完全用到,它固定的是从00到FF。每个WAL的大小,在正常的时候,也是相同的,默认情况下是16M。这样,结合第二第三部分,就构成了一个日志文件序列。

根据这个规律,笔者想到可能可以通过构造符合规则的空文件,来“欺骗”一下数据库系统,让它以为可以正常的进行归档操作,以继续下一个日志归档的流程。这看起来是一个办法,但由于缺失的文件太多,相关的操作工作量比较大,所以可以作为备用方案。随后,笔者尝试找到了更好的处理方法。

笔者发现,在pgwal文件夹中,除了这些WAL文件之外,还有一个 "archive_status" 文件夹。这里面也有很多类似命名规则的文件,但和前面的WAL文件不同,它们都是“空的”,另外,它们的扩展名不同,要么是.ready,要么是.done。稍微对比分析不难发现,这应当是系统生成的,用来指示和标识归档状态的临时文件。ready文件是待处理的文件,done文件是已处理文件。

为了验证这个想法,笔者做了另外一个实验。首先找到当前卡住的,无法继续的ready文件,将其名称改为done。过了几分钟,确实发现归档状态跳过了这个文件,卡在了下一个文件当中。说明这个想法应当是可行的。所以笔者打算采用批量重命名的方式来处理这个问题。为此,笔者编写了一个简单的shell脚本,可以批量的对reade文件进行改名(一次修改一个号段的文件)。这里要特别注意,改名的文件,必须是真实在WAL文件夹中不存在的文件(真实的文件,系统会自行处理,我们在后面会看到)。


#!/bin/bash

for file in 000000010000002B000000*.ready; do
    mv "$file" "${file%.ready}.done"
done
~       

笔者的环境中,丢失的WAL文件可能有几百个。当所有的文件都标识为DONE后,过了一段时间,可以明显感觉到,pg就进入了一个正常归档处理的状态(通过查询归档状态,查看当前处理的WAL编号)。这个过程是比较快的,后面差不多半个小时左右,WAL的标识就进展到最新的ID。这些WAL文件,都被复制到了另一个磁盘分区中的归档文件夹中,而且系统也进行了正常的清理,数据目录中的WAL文件,从原来的30多G,下降到了4G左右,有效的释放了磁盘空间。而且考虑到WAL文件的处理已经走上了正轨,按照现有系统的运行和使用状态,在其他一切正常的情况下,可以持续运行数年之久。而且整个过程,基本上没有对系统的正常运行造成太大影响,这样的结果显然是比较令人满意的。

小结

本文记录了一次Postgres数据库WAL文件清理的过程。包括系统故障的产生,问题和现象分析,具体的处理方式,使用的工具和命令。直到最后问题解决的现象和效果等等。