ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

PostgreSQL WAL文件膨胀排查:归档失败与僵尸复制槽的处置指南

PostgreSQL WAL文件膨胀排查:归档失败与僵尸复制槽的处置指南 深夜的磁盘告警永远是数据库运维最不想收到的消息。前阵子值班一台PostgreSQL实例的数据盘使用率三小时内从70%冲到95%监控指向的罪魁祸首是pg_wal目录。WAL文件大小失控这件事几乎每个PG运维都会遇到一次而且绝大多数时候问题根源并不在参数配置上而是整个保留链路的某个环节悄悄断掉了。WAL的全称是Write-Ahead Logging预写日志。它存在的意义很朴素事务真正修改数据页之前必须先把变更写入日志文件。这个机制让数据库可以延迟刷数据页而不用牺牲持久性——崩溃时只要从检查点开始重放WAL就能把所有已提交事务找回来。正因为WAL承载着崩溃恢复、流复制、时间点恢复这三条命脉它的保留和清理规则远不是写满就删那么简单。文件该留多少、什么时候删、谁在拦着不让删每一环都可能出问题。这篇文章我打算从WAL的运行机制讲起把影响文件大小的所有参数逐个拆开再把我亲身经历的一次WAL把磁盘写满的完整排查过程原样写出来最后给你一套能直接抄走的监控巡检方案。刚接触PG的读者可以先看前两节已经背过不少参数的老手可以直接跳到第三节和第四节里面那些坑你大概率也会踩到。1. WAL文件的运行机制为什么pg_wal目录会自己膨胀1.1 WAL在PostgreSQL里的真实角色WAL可以类比成店铺的收银小票数据文件则是总账本。每一笔交易发生时先把小票写好放好回头再誊账本。如果誊到一半停电了第二天拿着小票就能把所有账目重新誊一遍不会漏账也不会重复记账。PostgreSQL在事务提交时并不需要把数据页立刻刷到磁盘只要确保对应的WAL记录落盘就行这就是它能在普通硬件上做到较高写入吞吐量的根本原因。崩溃恢复时数据库从最近一次检查点留下的重放位置开始把WAL里的变更逐条重放到数据页这个过程叫前滚。在PG 10之前WAL目录叫pg_xlogPG 10之后改成了pg_wal。你翻老博客如果看到pg_xlog注意这是版本差异。这个目录默认在数据目录下里面放的是连续的WAL段文件每个段默认16MB。段文件命名规则是24位十六进制比如000000010000000000000001前8位是时间线timeline中间8位是逻辑日志ID最后8位是段ID。排查哪些WAL比较旧的时候先看懂这个命名规则能省不少时间。1.2 16MB的段文件是怎么循环利用的PostgreSQL的WAL不是写一个无限大的文件而是一段一段推进默认每段16MB。这里有个关键认知对运维来说真正会变的是段文件的数量而不是单个文件的大小。单个文件大小在initdb初始化时就被定死了运行中无法修改这个后面细说。正常状态下pg_wal目录会保持一定数量的段文件形成一个动态平衡。数据库后台的行为可以理解成一个水盆检查点之后系统会把WAL总量尽量收缩到min_wal_size附近当累积WAL量超过max_wal_size时触发一次及时检查点检查点完成后不再被引用的旧段会被回收要么删除要么改名复用。所以在负载平稳的实例上pg_wal目录大小通常是在一个区间内反复波动的不是恒定不变。只有负载突增、检查点跟不上、或者有外部因素一直引用WAL不让清理时目录大小才会连续爬升爬到让你后背发凉。1.3 哪些操作最容易让WAL总量失控结合我见过的案例WAL暴涨基本逃不出下面几类大批量更新或删除。一次UPDATE影响几百万行、重建索引、大表ALTER都会产生大量WAL记录。长时间不结束的事务。事务开着不提交数据页的修改对应WAL会一直处于需要保留的状态同时会拖住最老的快照和重放位置间接导致检查点无法推进、旧WAL无法清理。检查点过于频繁或迟迟完不成。检查点本身要刷大量脏页如果磁盘I/O能力不足检查点迟迟不结束LSN还在不断往前走WAL量自然越积越多。归档失败或复制槽不消费。这是最容易被忽略的原因后面我用一整节讲。full_page_writes带来的整页镜像。每次检查点之后每个数据页第一次被修改时PG会把整个8KB页面写入WAL。检查点越频繁整页镜像越多WAL总量越大。这里有个反直觉的结论max_wal_size设得太小反而可能让WAL总量变大。因为检查点频繁触发full_page_writes产生的整页镜像变多写放大更严重。很多人以为把max_wal_size调小一点WAL文件就不会占那么多空间这基本是南辕北辙。这个原理我在第二节展开讲。1.4 一个健康的pg_wal目录应该长什么样没有任何复制、归档、长事务的纯开发库里pg_wal目录一般只有几个到几十个段文件总体积在min_wal_size默认80MB到max_wal_size默认1GB之间波动。如果你打开目录看到几百上千个WAL文件而且时间戳横跨好几天甚至几周基本可以断定有东西在钉住这些文件不让清理。这时候排查引用者比删除文件重要得多。2. 决定WAL文件大小的参数以及它们真正的作用边界2.1 先分清单个文件大小和目录总大小经常有人问WAL文件大小怎么设置这个问题其实分两层单个WAL文件的大小由wal_segment_size决定默认16MB在initdb时固定。pg_wal目录的总大小由检查点机制希望保留多少和外部依赖强制保留多少共同决定对应参数是max_wal_size、min_wal_size、wal_keep_size以及归档、复制槽、备份进程等运行时状态。搞清楚这个区别再看下面的参数表就不会混。2.2 影响WAL文件数量的参数清单参数默认值作用修改方式wal_segment_size16MB单个WAL段文件的大小initdb时指定运行中不可改min_wal_size80MB检查点后WAL回收的目标下限动态修改max_wal_size1GB触发及时检查点的软阈值检查点后期望控制的上限动态修改checkpoint_timeout300s两次检查点的最大间隔动态修改checkpoint_completion_target0.9检查点目标完成时间占总间隔的比例动态修改wal_keep_size0MBPG15即使没有备库连接也强制保留的WAL量动态修改旧版本为wal_keep_segmentsarchive_modeoff是否开启WAL归档修改后需重启archive_command空归档命令归档成功才会清理被归档的段动态修改wal_levelreplica控制WAL中记录的冗余信息量修改后需重启full_page_writeson检查点后首次修改的数据页是否整页写入WAL动态修改一般不建议关闭wal_level有三个级别minimal、replica、logical。如果开启逻辑复制或使用了某些逻辑解码工具WAL里要记录更多元数据和变更信息总量会比replica更高。生产环境一般至少要设为replica因为流复制依赖它。minimal级别下某些操作甚至可以跳过WAL但代价是无法做流复制和某些恢复操作一般只在初始化或某些特殊场景下用。2.3 max_wal_size为什么是软限制max_wal_size的官方定义是在检查点之间允许WAL增长到的软限制。实际机制是两次检查点之间累积的WAL量超过了这个值系统会提前触发一次及时检查点。注意软这个字——如果磁盘I/O跟不上检查点完成需要很长时间WAL会继续增长超过max_wal_size是完全正常的现象。很多人把这个参数当成硬上限一看pg_wal目录超过了1GB就紧张其实未必有问题。真正需要担心的是持续不回落而不是瞬间超过。在I/O能力足够的机器上用更小的max_wal_size来控制WAL文件总量是个错误方向因为检查点越频繁full_page_writes产生的整页镜像越多写放大越严重WAL总量反而可能更高。理解这个反馈回路比死记参数值重要得多。2.4 wal_keep_size一个容易被配大的保留参数PG 15之前这个参数叫wal_keep_segments单位是段数PG 15开始改成wal_keep_size单位是MB语义也更直白不管你系统里有没有备库连上来都会在pg_wal里强制保留这么多量的WAL文件。默认是0表示不做额外保留。它的本意是给偶尔断连的备库留出追赶空间。但很多人配置时习惯写个偏大的值比如wal_keep_size1024意味着pg_wal里永远至少有1GB的WAL不会被清理。如果磁盘本来就紧张或者备库长期断开这个善意保留就会变成磁盘压力来源。我在生产里见过一台主库因为历史原因配了wal_keep_size5120配合逻辑复制槽和归档pg_wal常年稳定在10GB以上。后来确认没有备库依赖把它调成0同时清理了失效复制槽pg_wal直接缩到几百MB。检查配置的时候这类保留型参数值得多看一眼。2.5 归档和复制槽才是最终决定者真正决定WAL能不能被清理的是引用链。归档进程会引用尚未成功归档的WAL段复制槽会引用restart_lsn之后的WAL段正在运行的pg_basebackup会在backup_label里记录起始LSN也需要保留相应范围的WAL。所以哪怕你把max_wal_size调得再小只要archive_command一直失败或者复制槽长时间不被消费pg_wal里的旧文件依然不会回收。这就像你把水龙头的出水量调小了但下水道堵了水池还是会满出来。第三节的排查案例就是归档失败和僵尸复制槽同时出问题的典型场景。3. 一次真实的WAL把磁盘写满排查过程3.1 第一层目录浏览先分清新文件和旧文件那次告警的数据盘使用率已经到92%还在往上走。我第一件事不是去调参而是先看清楚pg_wal里到底堆了什么东西。du -sh $PGDATA/pg_wal ls -lt $PGDATA/pg_wal | head -20输出显示pg_wal已经58GB文件数量接近3700个按16MB一个段算数字对得上。更关键的是目录里文件的修改时间跨度拉到了3天以上。正常负载平稳的实例pg_wal里的文件时间跨度应该以小时计跨度超过一天说明有大量旧文件在排队等待清理。接着查了当前WAL位置确认写入侧没有异常SELECT pg_current_wal_lsn(), pg_walfile_name(pg_current_wal_lsn());LSN推进正常说明数据库写入没有问题问题出在清理这一侧。到这里方向已经收敛到谁在引用旧WAL。3.2 第二层归档状态一眼看出命令是不是假归档我打开了归档统计视图SELECT archived_count, failed_count, last_archived_time, last_failed_time, last_failed_reason FROM pg_stat_archiver;结果很扎眼archived_count停留在很久之前的数字failed_count却在持续增加last_failed_reason显示归档命令退出非零。再看配置SHOW archive_command;命令指向了一个已经失效的备份脚本路径等于每个WAL归档都在失败。归档失败意味着什么PostgreSQL必须在某一段WAL被成功归档之后才允许把这一个段从pg_wal回收。命令一直失败旧文件就被全部挂在那里越积越多。这里有个容易忽略的点archive_command失败不会让数据库停摆也不会往应用侧报错只在postgresql日志里反复输出归档失败信息。很多环境根本没看日志这个问题可以潜伏非常久。另外要注意last_failed_reason字段是PG 12才加入的老版本只有失败计数没有失败原因排查起来更费劲。3.3 第三层复制槽比归档更隐蔽的钉子户修归档之前我顺手查了复制槽因为归档和复制槽经常同时出问题SELECT slot_name, slot_type, active, pg_size_pretty(pg_current_wal_lsn() - coalesce(restart_lsn, 0/0)) AS lag, restart_lsn, pg_current_wal_lsn() FROM pg_replication_slots;结果确实有一条老备用复制槽activefalserestart_lsn停在好几天前的LSN和当前LSN的差距换算下来超过20GB。这是个典型的僵尸复制槽——对应备库早就删了但主库上的slot记录没清理。PostgreSQL会一直认为可能有一个备库还需要这些WAL所以restart_lsn之后的段全部保留。复制槽比归档更隐蔽的地方在于归档失败至少在日志里有痕迹僵尸复制槽几乎完全静默。你不主动去查pg_replication_slots它就在那里安安静静地让磁盘慢慢满掉。它和归档失败叠加的效果就是旧WAL既不能因为归档成功被清理也不能因为槽消费完被清理只能无限堆积。3.4 第四层长事务和检查点排掉最后几个可能继续排查长事务和检查点是为了避免遗漏其他引用者SELECT pid, state, now() - xact_start AS xact_age, left(query, 100) AS query FROM pg_stat_activity WHERE xact_start IS NOT NULL AND state idle ORDER BY xact_start LIMIT 10;没有超过半小时的长事务。接着看检查点压力SELECT checkpoints_timed, checkpoints_req, buffers_checkpoint FROM pg_stat_bgwriter;checkpoints_req并不异常说明检查点不是主要矛盾。到这里根因基本锁定归档失败加上僵尸复制槽两个问题叠加导致WAL只增不减。3.5 处置顺序先解引用再让检查点回收修复思路是先解除引用再触发清理顺序不能反。第一步修正或临时变更archive_command。当时我一时无法确认正式的备份脚本是否恢复又确认这台实例没有备库依赖归档先把命令改为ALTER SYSTEM SET archive_command TO /bin/true; SELECT pg_reload_conf();这样归档链路立刻成功等磁盘水位降下来再恢复真正的归档命令。生产中这样做要谨慎先记录原值确认临时变更的影响范围。第二步确认僵尸复制槽没有客户端使用后删除SELECT client_addr, state FROM pg_stat_replication;确认列表里没有使用这个槽的备库然后执行SELECT pg_drop_replication_slot(old_standby_slot);第三步让数据库做一次检查点开始回收CHECKPOINT;这一步不会瞬间删除所有文件但会让系统重新评估引用边界。大约几分钟后pg_wal目录开始明显下降半小时内从58GB降到不到2GB。第四步把archive_command恢复为正式命令继续观察归档成功数是否递增。整个处置过程大约40分钟最花时间的其实是定位——归档失败在日志里有痕迹复制槽却一点报错都没有不去主动查很难发现。4. 清理WAL的正确姿势为什么直接删文件是事故的开始4.1 删pg_wal文件可能引发的连锁反应我见过不止一个团队在磁盘告警的紧急状态下直接rm $PGDATA/pg_wal/00000001...删掉一批看起来很久的WAL文件。出事的场景基本是这几种主库崩溃后启动PG发现需要重放某个段但段文件已被删除直接报requested WAL segment ... has already been removed实例无法完成恢复。备库正在追赶进度主库把备库还没消费的WAL删了备库断流只能重新做全量备份。删除文件时恰好在检查点前后pg_control里的重放位置和实际文件不一致数据页出现部分更新逻辑上已经损坏。为什么后果这么严重因为WAL不是普通历史日志它是数据库完整性的证据链。PostgreSQL的恢复过程按段号严格顺序读取缺中间任何一段都没法重放到最新状态。你删掉的不是没用的旧文件而是可能正要被重放的起点。这也是为什么PG官方从没提供过手动删除WAL的工具一切回收都交给检查点机制、归档状态、复制槽状态协同完成。4.2 谁在钉住WAL不让清理理解为什么系统没有自己清理比学会手动清理更有用。正常情况下检查点完成之后旧段文件会被回收但如果有以下引用者存在对应范围的WAL段会被强制保留归档进程每一段WAL都需要被archive_command成功处理一次。复制槽物理或逻辑复制槽的restart_lsn之后的WAL段都不得删除。正在进行的备份pg_basebackup从开始到结束期间需要的WAL段。长事务事务未结束最老的快照和回放位置可能被钉住。最新检查点重放位置redo point之后需要保留redo point之前的段才有资格进入回收评估。逐个排查这些引用者才是解决WAL文件大小的正道。直接删文件等于绕过数据库协议去篡改证据链风险完全不可控。4.3 正确的清理流程按顺序操作如果磁盘真的告急我的建议顺序是这样先看有没有活动备份或备库追赶检查pg_stat_replication和pg_stat_archiver确认有没有恢复中的备库在等待WAL。处理归档失败修复archive_command让它能成功归档实在不行先临时用/bin/true但要记得恢复。清理失效复制槽确认没有备库在用后用pg_drop_replication_slot删除。处理长事务联系应用提交或终止空闲事务。再执行CHECKPOINT;让系统按最新边界回收。观察pg_wal目录是否回落到正常区间。如果上述都处理完目录仍然没降再考虑在维护窗口重启实例让PG在启动时按当前redo point重新评估旧段。但重启是最后手段不是在告警时第一时间做的事。注意任何情况下都不要直接在文件系统层删除pg_wal里的段文件。如果数据库已经因为WAL文件缺失或损坏起不来唯一稳妥的路径是重新做全量备份并恢复。4.4 一个不得已的应急场景如果你面对的是纯开发或测试库没有备库、没有归档、没有PITR需求磁盘又真的要被灌满了有没有更快的方法有但同样要按流程走先确认没有长事务再干净停库然后启动。其实正常停库再启动一次PG本身就会把大量不再需要的WAL段清理掉。这么做比重启风险小也更符合数据库的运行逻辑——它自己知道哪些文件没人要了。还要强调一句数据库恢复或备份进程正在跑的时候不要去动pg_wal。你看着某个文件时间戳很早但它可能恰好是pg_basebackup在备份期间需要保留的起点。动之前用pg_stat_activity和pg_stat_progress_basebackup看一下几十秒就能确认。5. 把WAL文件大小纳入日常巡检和监控5.1 直接抄走的巡检SQL组合日常巡检不需要打开一堆视图把下面几条SQL按顺序跑一遍基本能覆盖WAL健康度的关键指标。当前WAL目录总大小和文件数SELECT pg_size_pretty(sum(size)) AS total_wal_size, count(*) AS wal_file_count FROM pg_ls_waldir();注意pg_ls_waldir()需要superuser或pg_monitor角色权限。给监控账号授权时建议直接赋予pg_monitor不要给superuser。归档状态SELECT archived_count, failed_count, last_archived_time, last_failed_time, last_failed_reason FROM pg_stat_archiver;重点观察failed_count是否在多次巡检之间持续增加last_archived_time是否长时间不更新。复制槽健康度SELECT slot_name, slot_type, active, pg_size_pretty(pg_current_wal_lsn() - coalesce(restart_lsn, 0/0)) AS lag, restart_lsn, pg_current_wal_lsn() FROM pg_replication_slots;lag越大说明这个槽要求保留的WAL越多。lag超过一定阈值就要人工确认。长事务SELECT pid, state, now() - xact_start AS xact_age, left(query, 80) AS query FROM pg_stat_activity WHERE xact_start IS NOT NULL AND state idle ORDER BY xact_start LIMIT 10;检查点统计SELECT checkpoints_timed, checkpoints_req, checkpoint_write_time, checkpoint_sync_time, buffers_checkpoint FROM pg_stat_bgwriter;checkpoints_req持续增长说明经常因为达到max_wal_size而触发及时检查点可以结合WAL总量判断是否需要调整参数。PG 14之后可以看pg_stat_checkpointer视图字段更细包含num_timed和num_requested。5.2 命令行和监控系统侧的快速定位如果你在SSH终端里应急几条命令就够了du -sh $PGDATA/pg_wal find $PGDATA/pg_wal -name 0* -mtime 1 | wc -l第二行统计超过1天没被回收的段文件数量。正常情况下这个数字应该非常小如果数量很大基本可以确定存在引用者问题。在有监控系统的环境里建议把这些指标接入告警pg_wal目录大小或占数据盘比例。archive_command失败次数的增量。复制槽restart_lsn与当前LSN的滞后字节数。pg_stat_replication中statestreaming的备库数量突然变0要引起注意。最老活动事务年龄超过阈值要告警。5.3 告警阈值和调参建议告警阈值没有统一标准我这边常用的做法pg_wal目录超过max_wal_size的1.5倍并持续30分钟以上说明可能有堆积。archive failed_count在轮询周期内有增长立即告警。复制槽lag超过max_wal_size的2倍或者超过2GB需要人工确认。数据盘使用率超过80%进入重点观察超过90%要准备应急方案。如果经过排查确认没有引用问题只是单纯写负载高、检查点跟不上再考虑调参。顺序是先确保归档和复制槽正常再调整checkpoint_timeout或max_wal_size每次调整幅度不要太大观察几小时。max_wal_size从1GB调到8GB跨度看起来不大但会让崩溃恢复时间明显变长。务必先在测试环境做一次崩溃恢复演练再上生产别拿生产环境做实验。最后说点个人体会。我做PG运维这些年WAL文件大小报警十次里有八次不是配置问题而是某条链路断了——归档命令改了没生效、复制槽建了忘删、备份脚本挂了没人知道。如果你只盯着max_wal_size去调等于看着压力表去调温度旋钮方向从一开始就错了。先把归档、复制、备份这些引用者捋顺再回来看参数你会少熬很多夜。另外每隔一段时间清一次僵尸复制槽和失效归档命令比任何花哨的监控配置都管用。
返回列表