WAL перестал архивироваться, а failed_count молчит: как мы это ловим
У одного из наших клиентов на PostgreSQL 18 мониторинг архивирования WAL почти два месяца показывал идиллию: failed_count в pg_stat_archiver держался на нуле, дежурная панель не мигала. При этом каталог pg_wal рос, а на приёмнике архива последний файл не обновлялся уже несколько недель. Причина оказалась банальной и одновременно системной: pg_stat_archiver устроен так, что засчитывает сбой только тогда, когда archive_command явно вернул ненулевой код завершения. Всё, что происходит до этого момента или вместо него, в счётчик не попадает. В этом материале — наш разбор, какие поломки архивирования проходят мимо failed_count, и какую схему контроля мы строим у клиентов, чтобы не зависеть от одного счётчика.
Что именно измеряет pg_stat_archiver — и где проходит граница его компетенции
Начну с определения, а не с догадок. Системное представление pg_stat_archiver в PostgreSQL — это одна строка с накопительными счётчиками: archived_count, last_archived_wal, last_archived_time, failed_count, last_failed_wal, last_failed_time и stats_reset. Представление существует и отдаёт одну строку всегда, когда archive_mode включён (on или always) — вне зависимости от того, успешно ли на самом деле идёт архивирование.
Ключевой момент, который мы объясняем каждому клиенту при внедрении мониторинга: архивер не проверяет результат работы archive_command содержательно. Он смотрит только на код завершения процесса. Официальная документация формулирует это жёстко: команда архивирования должна возвращать нулевой статус завершения тогда и только тогда, когда операция действительно успешна. Получив ноль, PostgreSQL считает WAL-файл заархивированным и помечает его на удаление/переработку. Получив ненулевой статус — засчитывает попытку как неудачную и повторяет её позже.
Отсюда прямое следствие: failed_count — это счётчик "честности" вашей команды архивирования, а не счётчик "здоровья" архивирования как процесса. Если команда врёт про свой результат — неважно, случайно или по недосмотру в скрипте — pg_stat_archiver верит ей безоговорочно. Мы в ITfresh на аудитах инфраструктуры клиентов регулярно видим, что дежурные завязывают алерты только на failed_count > 0, и этого достаточно, чтобы пропустить реальную аварию архива.
| Поле pg_stat_archiver | Что фиксирует | Чего НЕ фиксирует |
|---|---|---|
| archived_count | Число WAL-файлов, для которых archive_command вернул код 0 | Реально ли файл лежит в целевом хранилище и не повреждён |
| last_archived_wal / last_archived_time | Имя и момент последнего успешного (по коду возврата) архивирования | Разрыв между этим WAL и текущим активным сегментом — само по себе поле не покажет отставание |
| failed_count / last_failed_wal | Число и факт последнего явного ненулевого завершения команды | Зависшие без таймаута попытки, отсутствие самого архивера, отключённый archive_mode |
| stats_reset | Время последнего сброса статистики | Не мешает сбросу маскировать реальный инцидент, если сброс сделан вручную или скриптом обслуживания |
Механика архивера: archive_mode, archive_command/archive_library и файлы .ready/.done
Чтобы понимать, где теряются сбои, нужно держать в голове полную цепочку. При переключении сегмента WAL сервер создаёт в каталоге pg_wal/archive_status/ файл-сигнал с расширением .ready для завершённого сегмента. Отдельный процесс-архивер видит эти файлы, по очереди — от старых к новым — запускает для каждого archive_command (либо вызывает archive_library, если она задана — это модуль-разделяемая библиотека, альтернатива shell-командам, появившаяся в актуальных версиях как более надёжный способ архивирования; пример реализации — контрибный модуль basic_archive). После успешного завершения файл переименовывается в .done, а сам WAL-сегмент становится кандидатом на переработку/удаление.
Три параметра, которые управляют этой цепочкой, ведут себя по-разному с точки зрения применения изменений — и это первая точка, где администраторы путаются:
- archive_mode — контекст postmaster, меняется только через перезапуск сервера (не reload). Если после рестарта конфигурация "откатилась" на
off— например, кто-то правил файл конфигурации не тот, что реально подключён, или изменение затёрлось при обновлении пакета — архивер просто не стартует. И это критично: без запущенного архивераpg_stat_archiverне получает новых событий вообще, ни успешных, ни неудачных. Счётчики застывают на последних значениях до сбоя, и внешне это неотличимо от "давно всё было хорошо и продолжает быть хорошо". - archive_command / archive_library — применяются по reload конфигурации, без перезапуска сервера. Это удобно для правок, но также значит, что опечатка в команде подхватится мгновенно после
pg_ctl reloadилиSELECT pg_reload_conf()— без явного предупреждения о синтаксисе. - archive_timeout — тоже применяется по reload, задаёт принудительное переключение сегмента по времени, даже если он не заполнен. Если этот параметр выставлен в 0 (архивирование только по заполнению сегмента) на малонагруженной базе, между .ready-файлами могут быть большие паузы — и разрыв в last_archived_time легко спутать с зависшим архивером, хотя на самом деле просто нет свежих сегментов для отправки.
Отдельно напомню жёсткое ограничение уровня журналирования: archive_mode нельзя включить при wal_level = minimal — этого уровня недостаточно для восстановления по WAL, и сервер с такой комбинацией параметров не запустится. На практике это защищает от совсем грубой ошибки, но не защищает от куда более частого случая — правильного wal_level и полностью сломанной команды архивирования.
Где именно теряются сбои: семантика кода возврата и "тихие" провалы
Здесь — ядро проблемы, вынесенной в заголовок статьи. Мы систематизировали у себя пять паттернов, из-за которых архивирование фактически стоит, а failed_count в pg_stat_archiver не растёт ни на единицу.
1. Команда всегда возвращает 0, независимо от результата. Самый частый случай на практике — обёртка вида test ! -f /archive/%f && cp %p /archive/%f без проверки кода завершения cp, либо конструкция с завершающим || true или ; exit 0, добавленная кем-то "чтобы не спамило ошибками". Формально это archive_command, формально она укладывается в правило "ноль — значит успех", но реального копирования может не произойти — например, целевой каталог смонтирован read-only, диск полон, или путь cd /var/lib/postgresql/data && cp ... ссылается на каталог, который перестал существовать после миграции данных на новый том. Shell в такой конструкции может завершиться нулём на последней команде пайплайна, даже если предыдущая реально провалилась.
2. Команда зависает без таймаута. Если archive_command делает сетевую операцию — scp, rsync через залипший туннель, запрос к object storage без тайм-аута — а сеть "подвешивает" соединение, процесс архивера годами документированного поведения PostgreSQL просто ждёт завершения команды. Она не вернула ни 0, ни ненулевой код — она не вернула ничего. failed_count в таком состоянии не увеличивается, потому что с точки зрения счётчика попытка ещё не завершилась. Тем временем новые .ready-файлы копятся, каталог pg_wal растёт, а last_archived_time просто перестаёт обновляться — и это единственный явный сигнал, но его никто не проверял, потому что мониторинг был настроен на failed_count.
3. archive_mode=off после рестарта. Разобран в предыдущем разделе: без запущенного архивера новых записей в pg_stat_archiver не появляется вовсе — ни успехов, ни провалов. Типичный сценарий: сервер обновили или перезагрузили после патча ОС, а параметр вернулся к значению из postgresql.conf, которое кто-то забыл синхронизировать с боевым postgresql.auto.conf.
4. Сброс статистики через pg_stat_reset_shared. Вызов SELECT pg_stat_reset_shared('archiver'); обнуляет все счётчики и обновляет stats_reset. Это штатная функция, но если она вызывается из скрипта обслуживания (например, при пересборке дашборда мониторинга или рутинной "чистке метрик") в момент, когда уже был сбой, накопленный failed_count исчезает вместе с исторической видимостью проблемы. Отчёт мониторинга покажет чистый ноль там, где секунду назад была авария.
5. Аварийное завершение самой команды сигналом или большим кодом выхода. Документация отдельно описывает случай, когда команда архивирования завершается по сигналу (не SIGTERM в рамках штатного останова сервера) или оболочка возвращает код выхода больше 125 (например, "command not found"): в этой ситуации сам процесс архивера прерывается и перезапускается postmaster-ом. Это фиксируется иначе, чем обычный ненулевой код возврата команды, и в реальных инцидентах у нас были случаи, когда именно эта ветка поведения давала рассинхронизацию между тем, что видно в журнале сервера, и тем, что отражено в pg_stat_archiver на момент проверки.
На объекте, с которого начался этот разбор, реализовался именно первый паттерн: команда архивирования была написана как двухшаговый скрипт копирования на смонтированный по NFS каталог, и после того, как точка монтирования отвалилась при плановом обслуживании сетевого хранилища, команда cp начала завершаться с ошибкой доступа, но обёртка вокруг неё глушила код возврата и завершалась нулём, чтобы "не заваливать журнал сервера сообщениями об ошибке" — так это объяснили нам при разборе истории изменений скрипта. В результате archived_count рос в полном соответствии с частотой переключения WAL-сегментов, а сам архив на хранилище не получал ни одного нового файла несколько недель.
Сводная таблица: сценарий сбоя против того, что видно в мониторинге
Мы собрали для внутреннего чек-листа таблицу соответствия — что реально происходит и что при этом показывают стандартные источники правды. Её же используем на входном аудите PostgreSQL у новых клиентов.
| Сценарий | pg_stat_archiver | Что покажет реальную картину |
|---|---|---|
| archive_command всегда возвращает 0 (true/|| true/битый пайплайн) | archived_count растёт, failed_count = 0 | Файлов нет в целевом каталоге/бакете; растёт задержка на приёмнике архива относительно last_archived_wal |
| Команда зависла без таймаута | Все счётчики застыли, last_archived_time не обновляется | Растёт pg_wal, растёт число .ready-файлов в archive_status |
| archive_mode=off после рестарта | Строка есть (если mode менялся динамически до этого — может и не быть), счётчики не меняются вовсе | В журнале сервера нет записей запуска архивера; SHOW archive_mode показывает off |
| Ручной или скриптовый сброс pg_stat_reset_shared('archiver') | failed_count обнулён, stats_reset обновлён недавно | История инцидента видна только во внешнем логе алертов/тикетов, не в БД |
| Команда завершилась по сигналу / код выхода >125 | Возможна задержка отражения в failed_count из-за перезапуска процесса архивера | Запись о рестарте архивер-процесса в журнале PostgreSQL (server log) |
Общий вывод из таблицы простой и мы его формулируем клиентам без смягчений: failed_count пригоден только для одного типа проверки — "команда архивирования иногда явно фейлится". Для ответа на вопрос "архивирование реально идёт и WAL реально долетает до места назначения целым" нужен отдельный контур, не зависящий от готовности archive_command честно отчитаться о своей неудаче.
Наша методика: сравнение LSN, возраст последнего архива и независимая проверка целостности
В проектах, где мы отвечаем за эксплуатацию PostgreSQL (клиентские инсталляции на 1С-серверах и отдельные аналитические базы), мы строим контроль архивирования на трёх независимых сигналах одновременно, а не на одном failed_count.
Сигнал 1 — отставание по имени файла. Сравниваем имя текущего активного WAL-сегмента с именем последнего успешно заархивированного:
SELECT
pg_walfile_name(pg_current_wal_lsn()) AS current_wal,
last_archived_wal,
last_archived_time,
failed_count,
last_failed_wal,
now() - last_archived_time AS age
FROM pg_stat_archiver;Если current_wal и last_archived_wal расходятся на несколько сегментов дольше, чем занимает штатный цикл переключения на конкретной базе (для активных OLTP-баз клиентов это, по нашей практике, минуты, не часы — но порог мы всегда калибруем по факту под конкретную нагрузку, а не берём universal-число), это тревога независимо от значения failed_count.
Сигнал 2 — возраст last_archived_time. Дежурный алерт срабатывает не на "failed_count > 0", а на "age(last_archived_time) превысил ожидаемый цикл архивирования, умноженный на разумный запас" — с учётом того, что при archive_timeout, выставленном на конкретной базе, разрыв в норме ограничен сверху этим значением. Если age растёт без остановки при живом archive_timeout — это именно зависшая или молчащая команда, а не низкая нагрузка.
Сигнал 3 — количество .ready-файлов, которые не превращаются в .done. Прямая проверка каталога сигналов даёт число реально накопленных, не отправленных сегментов — независимо от того, что говорит статистика:
SELECT count(*) FROM pg_ls_dir('pg_wal/archive_status') f
WHERE f LIKE '%.ready';Рост этого числа при неизменном failed_count — самый надёжный маркер именно "тихого" сбоя из категорий 1 и 2 из предыдущего раздела: команда формально жива, но перестала успевать или перестала действительно копировать файлы.
Дополнительно мы фиксируем в мониторинге момент любого вызова pg_stat_reset_shared отдельным событием (через отслеживание изменения stats_reset между опросами) — чтобы скачок "внезапно всё стало нулём" не читался дежурным как "проблема исчезла", а разбирался как повод перепроверить архив руками.
Второй контур: проверка целостности архива средствами, которые не верят серверу на слово
Даже три сигнала выше опираются на то, что сам PostgreSQL-сервер жив и может отвечать на запросы. Поэтому у клиентов, где архивирование WAL используется как основа для PITR (а не просто "для галочки"), мы добавляем независимую проверку archived-хранилища снаружи СУБД — средствами конкретного backup-инструмента, если он в контуре клиента.
Если резервное копирование организовано через pgBackRest, регулярный прогон команды проверки репозитория — это отдельная от pg_stat_archiver процедура, которая читает и валидирует содержимое самого архива (структуру репозитория, доступность и целостность сегментов), а не верит exit-коду команды, которая туда что-то писала. Если стек — WAL-G, аналогичную роль играет процедура верификации WAL в хранилище: она читает уже сохранённые файлы из целевого расположения и проверяет их валидность как WAL-сегментов, а не полагается на факт, что архивирующий процесс когда-то отчитался кодом 0.
Принцип, который мы закладываем в архитектуру мониторинга у клиентов: проверка успешности архивирования должна быть отделена от системы, которая архивирование выполняет. pg_stat_archiver — это отчёт исполнителя о своей же работе; он полезен для быстрой диагностики, но не может быть единственным источником истины, потому что исполнитель (shell-команда) в принципе способен ошибочно доложить об успехе.
На практике это выливается в простое правило эскалации: если сигналы из предыдущего раздела (расхождение LSN, возраст last_archived_time, число .ready) в норме, а внешняя проверка целостности архива (pgBackRest check или WAL-G wal-verify — что применимо в конкретном стеке) находит дефекты — приоритет отдаётся внешней проверке. Она видит то, что видно только снаружи: физическую доступность и валидность уже сохранённых файлов.
Пошаговое внедрение у клиента: как мы разворачиваем контроль архивирования
Порядок работ, который мы проходим на объекте клиента при постановке PostgreSQL под наблюдение (актуально и для новых инсталляций на PostgreSQL 18, и для миграции контроля со старой версии):
- Аудит текущей archive_command. Читаем буквально, построчно: есть ли в конструкции безусловный
exit 0,|| true, отсутствие проверки кода возврата у внутренних команд пайплайна. Отдельно проверяем, задан ли таймаут на сетевые операции внутри команды. - Оборачиваем команду в контролируемый таймаут, если его не было, например:
archive_command = 'timeout 60 cp %p /archive/%f && test -s /archive/%f'— здесьtimeoutгарантирует, что зависшая операция вернёт ненулевой код вместо бесконечного ожидания, аtest -sпроверяет, что скопированный файл не пустой, прежде чем объявить успех. - Проверяем согласованность archive_mode между работающим инстансом и конфигурационными файлами — сравниваем
SHOW archive_mode;с фактическим содержимымpostgresql.conf/postgresql.auto.conf, чтобы после следующего перезапуска сервер не тихо ушёл в archive_mode=off. - Настраиваем алерты на три независимых сигнала из предыдущего раздела (отставание LSN, возраст last_archived_time сверх ожидаемого цикла, число .ready-файлов), а не только на failed_count.
- Добавляем алерт на сам факт сброса статистики — отслеживаем изменение stats_reset между опросами, чтобы обнуление счётчиков не читалось как "проблема решилась сама".
- Подключаем внешнюю проверку целостности архива — регулярный прогон проверки репозитория тем backup-инструментом, который уже используется в контуре клиента (pgBackRest check или аналог), как источник истины более высокого приоритета, чем внутренние счётчики сервера.
- Прогоняем контролируемый негативный тест — временно подставляем в archive_command заведомо неработающий путь и убеждаемся, что все настроенные сигналы (а не только failed_count) реагируют в течение ожидаемого окна.
Последний пункт мы считаем обязательным: пока алерт не сработал хотя бы раз на управляемо воспроизведённой аварии, доверять ему на боевой системе нельзя — это правило мы применяем к любому мониторингу, не только к PostgreSQL.
Отдельно фиксируем в самой archive_command путь к отдельному лог-файлу операции (например, дублируя stderr во внешний файл через 2>>/var/log/postgresql/archive_command.log), чтобы при разборе инцидента не приходилось восстанавливать картину только по косвенным признакам в pg_stat_archiver — у дежурного должна быть возможность открыть журнал и увидеть, что именно вернула команда на конкретном сегменте, включая случаи, когда код возврата был нулевым, но по существу операция ничего не сделала.
Что это значит для руководителя: риск, который не виден в зелёном дашборде
Для собственника бизнеса или ИТ-директора техническая суть сводится к одному управленческому риску: "зелёный" статус мониторинга резервного архивирования не гарантирует, что при аварии основной базы будет чем восстанавливаться. Разрыв между "счётчик ошибок молчит" и "архив реально пригоден для восстановления на нужный момент времени" может существовать неделями и обнаружиться только в момент реального инцидента — когда цена ошибки максимальна.
Стоимость закрытия этого разрыва для типовой инсталляции — это не новое оборудование и не смена СУБД, а перенастройка мониторинга и однократный аудит команды архивирования: часы, не дни. Мы делаем это как часть регулярного технического обслуживания серверов 1С и отдельных PostgreSQL-баз у клиентов, для которых WAL-архив — это не опциональная настройка, а часть плана восстановления после сбоя.
Практический вывод, который мы формулируем в отчётах для руководства: если ваша команда (внутренняя или подрядчик) отвечает на вопрос "работает ли архивирование" ссылкой на "ошибок нет", а не на "мы сверяли LSN и проверяли целостность архива внешним инструментом в этом месяце" — это повод запросить именно вторую формулировку, до того как она понадобится в реальном восстановлении.
Мы сознательно не приводим здесь универсальную оценку "сколько стоит простой без архива" — это сильно зависит от объёма базы, требований по RPO и договорных обязательств перед конечными пользователями системы. Но общая закономерность, которую мы наблюдаем на практике: чем реже реально проверяется целостность архива независимым инструментом, тем позже обнаруживается разрыв, и тем больше данных оказывается вне зоны восстановления к моменту обнаружения.
Частые вопросы
- Можно ли доверять failed_count в pg_stat_archiver хотя бы как индикатору "нет явных ошибок"?
- Да, но только в узком смысле: он честно показывает, что archive_command хотя бы раз явно вернула ненулевой код. Он не показывает, что архивирование в принципе выполняется, что файлы реально доходят до места назначения целыми и что архивер-процесс вообще запущен после последнего рестарта.
- Как быстро проверить руками, не остановилось ли архивирование, без настройки мониторинга?
- Сравните pg_walfile_name(pg_current_wal_lsn()) с last_archived_wal из pg_stat_archiver и посмотрите на возраст last_archived_time. Если разрыв растёт дольше, чем занимает штатный цикл переключения сегмента на вашей базе, это сигнал независимо от значения failed_count — дополнительно посчитайте .ready-файлы в pg_wal/archive_status.
- Почему архивирование не запускается после планового перезапуска сервера?
- Чаще всего потому, что archive_mode меняется только через перезапуск сервера, а не через reload конфигурации, и после рестарта в силу вступает значение из файла конфигурации, а не то, что было выставлено динамически ранее. Проверяйте SHOW archive_mode после каждого рестарта и сверяйте с postgresql.conf/postgresql.auto.conf.
- Зачем нужна отдельная проверка целостности архива, если pg_stat_archiver и так показывает архивированные файлы?
- Потому что pg_stat_archiver фиксирует только код завершения команды, а не факт того, что файл реально и без повреждений лежит в целевом хранилище. Инструменты вроде проверки репозитория pgBackRest или верификации WAL в WAL-G читают уже сохранённые файлы напрямую из хранилища, независимо от того, что о своём успехе доложила команда архивирования.
- Стоит ли настраивать archive_timeout, если база малонагруженная?
- Как правило да — без него переключение сегмента происходит только по заполнению, и на малонагруженной базе между архивируемыми файлами могут быть большие естественные паузы, которые легко спутать с зависшим архивером. Конкретное значение подбирается под требования по целевой точке восстановления (RPO) конкретной системы, а не берётся универсальным.