Коротко. Каталог pg_wal растёт по трём причинам: сломалась архивация, остался слот выключенной реплики или живая реплика не успевает проигрывать журналы. Первые две проверяются запросами к pg_stat_archiver и pg_replication_slots за пять минут, третья начинается с параметров монтирования. Руками журналы не удалять никогда: по ним база поднимается после сбоя.

Свободное место на сервере базы кончилось за ночь, PostgreSQL перестал принимать запросы, Directum RX лежит. В каталоге pg_wal десятки гигабайт файлов, хотя обычно там пара гигабайт. Соблазн очевиден: удалить лишнее и жить дальше. Так делать нельзя, и ниже объясняем почему, а заодно как за пять минут понять, кто именно держит журналы, и как больше не доводить.

Симптом

Пользователи видят ошибки сохранения и «сервис недоступен». В журнале PostgreSQL появляется запись о невозможности записать файл, дальше сервер уходит в аварийную остановку. Классическая формулировка в логе содержит No space left on device и PANIC.

На диске картина такая: раздел с данными заполнен на сто процентов, а каталог pg_wal внутри PGDATA разросся до десятков гигабайт. При этом сама база по размеру не изменилась, резервные копии по расписанию вроде бы делались.

Почему так

Журнал предзаписи (WAL) это последовательность файлов, в которые PostgreSQL пишет каждое изменение до того, как оно попадёт в основные файлы базы. Такой файл можно удалить только после того, как он больше никому не нужен. «Никому» здесь означает трёх потребителей сразу, и достаточно одного застрявшего, чтобы файлы копились бесконечно.

Первый потребитель, архивация. Если включён режим архивации, PostgreSQL для каждого заполненного файла вызывает заданную команду копирования в архив. Пока команда не вернёт успех, файл остаётся на диске. Команда может падать месяцами молча: кончилось место в приёмнике, сменился пароль, отвалилась сетевая папка, у процесса пропали права после обновления. База при этом работает нормально, никто ничего не замечает, а файлы копятся.

Второй потребитель, слот репликации. Слот существует, чтобы реплика могла отстать и потом догнать, не потеряв данные. Цена этой гарантии в том, что мастер обязан хранить журналы до позиции, которую подтвердила реплика. Реплику выключили, а слот забыли удалить, и мастер честно копит журналы за всё время, пока её нет. Это самая частая причина внезапного роста на контурах, где реплику когда-то поднимали для теста.

Третий, менее опасный, настройка удержания. Параметр, который велит хранить некоторый объём журналов на случай отставания реплики без слота. Он ограничен сверху, поэтому сам по себе диск не съедает, но добавляется к первым двум.

Отдельно стоит случай, который выглядит как исправный контур: реплика включена, слот активен, но журналы всё равно копятся. Значит реплика принимает их быстрее, чем успевает проигрывать. Накат журналов на реплике идёт в один поток, и любая операция, которая делает работу с файлами дороже, замедляет его целиком.

Характерный пример на Astra Linux: у раздела в параметрах монтирования стоит secdel, гарантированное удаление. Эта опция при удалении и усечении файлов синхронно затирает освобождаемые блоки. Для рабочих каталогов это разумная мера, а для раздела с данными PostgreSQL она превращает обычный накат в вечность: однопоточный процесс восстановления ждёт каждое затирание, скорость падает до единиц сегментов в минуту, и отставание растёт быстрее, чем сокращается. На мастере при этом всё выглядит штатно, кроме растущего pg_wal. Проверяется одной командой:

findmnt -no TARGET,OPTIONS /opt
grep -n secdel /etc/fstab

Коварство в том, что параметры монтирования на узлах кластера расходятся незаметно: на одном узле опция есть, на втором нет, и разница вылезает только под нагрузкой.

Важная деталь про Directum RX: система пишет в базу много и постоянно, фоновые задания работают круглосуточно. Поэтому журналы генерируются заметно быстрее, чем на учётной системе со всплесками активности днём, и запас времени от «архив сломался» до «диск кончился» тут короче, чем принято ожидать.

Диагностика

Пять минут и четыре проверки. Начинаем с того, что вообще занимает место.

df -h /var/lib/postgresql
du -sh /var/lib/postgresql/*/main/pg_wal
ls /var/lib/postgresql/*/main/pg_wal/archive_status/*.ready 2>/dev/null | wc -l

Последняя команда даёт главный ответ. Файлы с расширением .ready ждут архивации. Если их тысячи, архивация сломана, и дальше можно не гадать.

Теперь смотрим, что говорит сама база. Подключаемся и спрашиваем статистику архиватора:

SELECT archived_count, failed_count, last_archived_wal, last_archived_time,
       last_failed_wal, last_failed_time
FROM pg_stat_archiver;

Растущий failed_count вместе со свежим last_failed_time означает, что команда архивации падает прямо сейчас. Имя файла из last_failed_wal подскажет, с какого момента всё встало. Дальше нужно выполнить ту же команду руками от имени пользователя базы и прочитать реальную ошибку: почти всегда это место, права или сеть.

Вторая проверка, слоты:

SELECT slot_name, slot_type, active,
       pg_size_pretty(pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn)) AS udergivaet
FROM pg_replication_slots
ORDER BY pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn) DESC;

Слот со значением active = false и удержанием в десятки гигабайт и есть виновник. У живой реплики значение небольшое и не растёт между замерами.

Что смотрим Чем Норма
Файлы в очереди на архивацию счёт .ready в archive_status единицы, не растёт
Ошибки архивации failed_count в pg_stat_archiver не растёт
Свежесть архива last_archived_time минуты назад
Удержание слотами pg_replication_slots у живых реплик мало и стабильно
Параметры монтирования на узлах СУБД findmnt -no OPTIONS одинаковые на мастере и репликах

Что сделать

Сначала вернуть работу, не удаляя журналы. Если раздел заполнен полностью, PostgreSQL не запустится. Место нужно освободить за счёт чего угодно другого: старые логи, временные файлы, лишние копии. В крайнем случае расширить том. Задача на этом шаге дать базе подняться, а pg_wal не трогать вовсе.

Потом убрать причину. Если сломана архивация, чиним команду и убеждаемся, что очередь .ready начала таять. PostgreSQL сам догонит отставание и удалит отработавшие файлы. Если виноват заброшенный слот, его нужно удалить, предварительно убедившись, что соответствующая реплика действительно не нужна:

SELECT pg_drop_replication_slot('имя_слота');

Удаление слота живой реплики сломает её так, что понадобится пересоздание с нуля, поэтому проверяем дважды.

И сделать так, чтобы в следующий раз узнать заранее. Три правила, которые закрывают этот класс аварий:

  1. Отдельная файловая система под pg_wal. Тогда переполнение журналов не утаскивает за собой весь сервер, и у вас остаётся место для манёвра.
  2. Ограничение на удержание журналов слотами. В современных версиях PostgreSQL есть параметр, задающий предел, после которого слот объявляется недействительным. Реплика при этом потребует пересоздания, зато мастер выживет. Это осознанный размен: лучше чинить реплику, чем поднимать продуктив из копии.
  3. Сверка параметров монтирования на всех узлах СУБД. Они должны совпадать, и на разделе с данными базы не должно быть опций, удорожающих удаление файлов.
  4. Наблюдение по трём метрикам: заполнение раздела с журналами, число ошибок архиватора и возраст последнего успешно заархивированного файла. Первые две ловят поломку, третья ловит случай, когда команда завершается успешно, но никуда ничего не кладёт.

Словами для правил наблюдения: «раздел с журналами заполнен больше чем на восемьдесят процентов», «счётчик ошибок архивации вырос за последний час», «последний файл заархивирован больше часа назад».

Чего не делать

  • Не удалять файлы из pg_wal командой удаления. Это не мусор, а журнал, по которому база восстанавливается после сбоя. Удаление нужного файла превращает штатный перезапуск в восстановление из копии. Если чистка всё же необходима, для этого есть штатная утилита, которая понимает, какие файлы уже не нужны.
  • Не выключать архивацию в панике. Диск освободится, а точка восстановления уедет на момент последней полной копии. Вы поменяете аварию на час на потерю данных за сутки.
  • Не удалять слоты списком. Сначала выяснить, какой реплике каждый принадлежит и жива ли она.
  • Не списывать отставание реплики на «слабое железо», не посмотрев параметры монтирования. Диск может быть быстрым, а каждая операция удаления дорогой из-за настроек файловой системы.
  • Не считать снимок виртуальной машины заменой архиву. Снимок отдаёт базу в состоянии «как при выключении питания», и восстановиться на произвольный момент по нему нельзя.