База выросла вдвое за ночь: забытый слот репликации не давал чистить WAL
Дежурный инженер получает алерт в три часа ночи: диск под PostgreSQL за несколько часов вырос почти вдвое. Ни один деплой не катился, ни один бэкап не запускался руками, объём данных в таблицах не изменился — а свободного места на разделе с pg_wal вдруг не осталось. Разбираем историю целиком: что видели на графиках, какие версии отбрасывали одну за другой, и почему виноватым оказался слот репликации, про который все давно забыли.
Содержание
- Первый звонок: место на диске тает быстрее, чем должно
- Куда посмотрели дальше — и куда сначала не заглянули
- Гипотезы, которые отбросили одну за другой
- Слот репликации, который никто не трогал три недели
- Почему неактивный слот замораживает очистку WAL
- Что сделали сразу и как закрыли инцидент
- Что изменили в процессах после разбора
Первый звонок: место на диске тает быстрее, чем должно
Мониторинг диска сработал стандартно — порог в 85% занятого места на разделе с данными PostgreSQL. Дежурный открыл графики: рост не был плавным, как обычно бывает при накоплении данных. Кривая пошла вверх резко, почти вертикально, начиная с определённого часа ночи, когда по расписанию отрабатывал ночной пакет обновлений — большая ETL-задача, которая раз в сутки перекладывает данные из основных таблиц в витрины для отчётов.
Первая мысль была прямолинейной: задача стала писать больше данных, чем раньше, и таблицы просто разрослись. Проверили pg_database_size и размеры основных таблиц через pg_total_relation_size — рост был, но не такой, чтобы объяснить скачок занятого места на диске. Таблицы прибавили мегабайты, а диск потерял гигабайты.
Тогда посмотрели, где именно растёт занятое место — не в самих файлах данных, а в каталоге pg_wal внутри PGDATA:
du -sh /var/lib/postgresql/16/main/pg_wal
ls /var/lib/postgresql/16/main/pg_wal | wc -l
Количество файлов WAL было заметно выше нормы для этой инсталляции — вместо привычных полусотни сегментов их скопилось несколько сотен. Каждый сегмент по умолчанию занимает 16 МБ, так что счёт быстро идёт на гигабайты. Стало ясно: причина не в данных, а в том, что WAL перестал вовремя удаляться.
Куда посмотрели дальше — и куда сначала не заглянули
Первым делом проверили настройки, которые прямо управляют объёмом хранимого WAL. wal_keep_size в конфиге стоял на разумном значении, max_wal_size тоже не был раздут — то есть штатный механизм ограничения роста WAL не был отключён руками. Значит, что-то держало WAL насильно, в обход этих настроек.
Дальше проверили архивацию: параметр archive_mode был включён, а archive_command указывал на скрипт, копирующий сегменты в бакет объектного хранилища для бэкапов. PostgreSQL не удаляет сегмент WAL, пока не убедится, что архивная команда успешно его забрала — это штатное поведение, и первая версия была: скрипт архивации сломался, команда падает с ошибкой, WAL копится.
Проверили логи PostgreSQL на предмет ошибок архивации:
grep -i "archive" /var/log/postgresql/postgresql-16-main.log | tail -100
И тут гипотеза не подтвердилась: архивация отрабатывала штатно, ошибок не было, pg_stat_archiver показывал последнюю успешную выгрузку буквально несколько минут назад. Значит, дело не в архиве.
Нужен сервер под эту задачу?
Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.
Арендовать серверГипотезы, которые отбросили одну за другой
К этому моменту список подозреваемых выглядел так:
- Сломанная архивация — отбросили:
pg_stat_archiver.last_archived_timeобновлялся вовремя,archive_commandзавершалась с кодом 0. - Слишком большая транзакция в ночном ETL — отбросили: посмотрели
pg_stat_activityво время следующего запуска задачи, длинных транзакций не увидели, ETL коммитил пачками, как и должен был. - Ручной
VACUUM FULLилиREINDEX, забытый кем-то в cron — отбросили: проверилиcrontab -lпод всеми сервисными пользователями и системныйsystemdтаймеры, ничего похожего не нашли. - Утечка на уровне файловой системы, "фантомные" файлы после удаления — отбросили:
lsofне показывал держащихся дескрипторов на удалённые файлы, аduиdfсходились друг с другом. - Резкий всплеск нагрузки на запись из-за индекса, который недавно перестроили — тема резонная (похожая история уже разбиралась в статье про то, как индекс ускоряет запрос и когда замедляет), но конкретно здесь свежих изменений в индексах за последние недели не было.
Отбросив пять правдоподобных версий, вернулись к самому WAL — не к тому, почему он *генерируется*, а к тому, почему он не *удаляется*. Это два разных вопроса, и именно смешение одного с другим отняло больше всего времени на старте разбора.
Слот репликации, который никто не трогал три недели
PostgreSQL не удаляет сегмент WAL, если он ещё нужен хотя бы одному потребителю — физической реплике, логическому подписчику или инструменту вроде pg_receivewal. Договорённость между сервером и таким потребителем оформляется через слот репликации (replication slot): пока слот существует, сервер обязан хранить WAL начиная с позиции, которую подтвердил этот слот, даже если сам потребитель давно отключился.
Проверили список слотов:
SELECT slot_name, slot_type, active, restart_lsn,
pg_size_pretty(pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn)) AS retained_wal
FROM pg_replication_slots;
И вот он, виновник — физический слот с именем вроде old_analytics_replica, active = f, а restart_lsn замер на позиции трёхнедельной давности. Колонка retained_wal показывала объём WAL, который PostgreSQL был обязан хранить только ради этого слота — и это была основная масса накопленных сегментов.
История слота восстановилась быстро: три недели назад была развёрнута временная read-реплика для миграции аналитики на отдельный сервер. Реплику подняли, слот на мастере создали командой pg_basebackup --slot=old_analytics_replica --create-slot, миграцию довели до конца, а саму реплику после этого выключили и удалили из инфраструктуры как ненужную. А вот команду SELECT pg_drop_replication_slot('old_analytics_replica'); на мастере никто не выполнил — слот остался висеть, продолжая требовать WAL, который никто больше не читал.
Три недели диск рос медленно и незаметно — свободного места хватало с запасом, и обычный мониторинг диска не бил тревогу, реагируя только на процент заполнения, а не на скорость роста. Ночной ETL с интенсивной записью просто оказался той нагрузкой, которая за несколько часов дописала оставшийся запас — отсюда и ощущение, что база "выросла вдвое за одну ночь", хотя на самом деле бомба была заложена тремя неделями раньше.
Почему неактивный слот замораживает очистку WAL
Механика простая, но неочевидная, если не сталкивался с ней раньше. У каждого слота репликации есть restart_lsn — позиция в потоке WAL, с которой сервер обязан начинать отдавать данные, если подписчик когда-нибудь снова подключится. Пока слот существует, PostgreSQL не имеет права удалить ни один сегмент WAL новее этой позиции, потому что теоретически подписчик может опять запросить чтение именно с неё.
Если подписчик отключился навсегда, но слот не удалили — restart_lsn просто перестаёт двигаться и замирает в той точке, где подписчик последний раз подтвердил получение данных. Все новые сегменты WAL, которые сервер продолжает писать, накапливаются поверх этой замороженной позиции и не могут быть переработаны, пока слот жив. Разница между текущей позицией записи и restart_lsn растёт линейно с объёмом операций записи в базу — и чем активнее база, тем быстрее заканчивается место.
Отдельная неприятность в том, что до PostgreSQL 13 не было встроенного предохранителя от этого сценария: забытый слот мог заполнить диск полностью, если никто не проверял pg_replication_slots руками. Начиная с 13 версии появился параметр max_slot_wal_keep_size, который можно использовать как страховку — но по умолчанию он выключен (-1), и в этой инсталляции его тоже никто не настраивал. Об устройстве WAL в целом и о том, зачем PostgreSQL вообще пишет данные дважды, есть отдельный разбор — что такое WAL и зачем писать дважды; а более широкий список причин, из-за которых pg_wal может расти, разобран в статье PostgreSQL: растёт pg_wal — причины и решение.
Что сделали сразу и как закрыли инцидент
Сначала — снять острую фазу, не разбираясь в причинах на живом инстансе с почти заполненным диском. Убедились, что слот действительно никем не используется:
SELECT slot_name, active, active_pid FROM pg_replication_slots
WHERE slot_name = 'old_analytics_replica';
active было f, active_pid — NULL, то есть ни один процесс к слоту не подключён прямо сейчас. Дополнительно проверили по логам и по списку известных IP-адресов, что сервер, который когда-то использовал этот слот, действительно выведен из эксплуатации — рисковать и удалять слот, к которому кто-то ещё мог обращаться, нельзя: физическая реплика в таком случае потеряет синхронизацию и потребует полного пересоздания через pg_basebackup.
Убедившись, что слот действительно мёртв, его удалили:
SELECT pg_drop_replication_slot('old_analytics_replica');
WAL, накопленный только ради этого слота, PostgreSQL начал вычищать в фоне почти сразу — свободное место возвращалось постепенно, в течение нескольких контрольных точек (checkpoint), а не мгновенно. Дальше приняли три постоянных изменения, чтобы не повторять историю:
- Выставили
max_slot_wal_keep_sizeв конфиге на разумный предел (конкретное значение подбирается под объём операций записи и доступное место на конкретном сервере — универсального числа тут нет, это всегда компромисс между запасом на переподключение реплики и риском переполнения диска). - Добавили в мониторинг отдельный алерт именно по
pg_replication_slots, а не только по проценту занятого диска — оповещение срабатывает, если у неактивного слота объём удерживаемого WAL превышает пороговое значение, а не когда диск уже почти полон. Это отдельная метрика от общего мониторинга диска на сервере, про типовые ошибки настройки которого есть отдельный разбор — мониторинг диска на сервере: частые ошибки и решения. - Внесли пункт в чек-лист вывода реплик из эксплуатации: удаление слота репликации на мастере теперь идёт первым шагом, а не последним, и без него задача на списание сервера не считается закрытой.
Что изменили в процессах после разбора
Главный процессный вывод: слоты репликации создаются в момент запуска реплики одной командой, а вот их удаление никогда не происходит автоматически — PostgreSQL специально спроектирован консервативно в этом месте, чтобы не потерять данные для потенциально живого подписчика. Ответственность за симметричность операции целиком лежит на людях и процессах вокруг базы, а не на самой СУБД.
Из этого вытекли конкретные шаги, которые применили не только к аналитической реплике, но и ко всей инфраструктуре репликации:
- Раз в неделю по расписанию стал выполняться простой запрос-аудит, который выводит все слоты с
active = falseи возрастомrestart_lsnбольше суток — такие слоты почти всегда означают забытого или упавшего подписчика. - В runbook по развёртыванию временных реплик (для миграций, для аналитики, для тестов на проде) добавили обязательный шаг "удалить слот" со ссылкой на конкретную команду и на то, как убедиться, что слот действительно не используется, прежде чем его удалять.
- Отдельно обсудили логическую репликацию: у логических слотов та же проблема стоит даже острее, потому что кроме WAL там ещё удерживаются системные каталоги для декодирования изменений — если в инфраструктуре есть подписчики через
pglogicalили встроенную логическую репликацию PostgreSQL, аудит неактивных слотов особенно важен именно там. Общее устройство репликации и природу отставания реплик разбирали отдельно в статье как работает репликация и отставание реплики. - Договорились, что любой новый сервис, который создаёт слот репликации программно (а не через ручной
pg_basebackup), обязан регистрировать его в общем реестре слотов с указанием владельца — чтобы при инциденте не тратить время на детективную работу по логам и IP-адресам, а сразу знать, кто отвечает за конкретный слот.
Нужен сервер под эту задачу?
Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.
Арендовать серверНужны сами нейросети для контента?
Генерируйте изображения, видео и озвучку нейросетями на falapi.io — десятки моделей в одном окне. Оплата картой РФ и по СБП.
Частые вопросы
Как быстро понять, что причина роста диска именно в слоте репликации, а не в чём-то другом?
Первым делом смотрите не на общий размер pg_wal, а на вывод SELECT slot_name, active, pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn) FROM pg_replication_slots ORDER BY 3 DESC;. Если у неактивного слота эта разница сопоставима с объёмом накопленного WAL — вот и причина, дальше можно не искать.
Можно ли просто удалять все неактивные слоты, не разбираясь?
Нет. Неактивный слот не всегда мёртвый — реплика или логический подписчик могут быть временно выключены на обслуживание и переподключиться позже. Перед удалением нужно убедиться, что сервер, использовавший слот, действительно выведен из эксплуатации навсегда, иначе придётся пересоздавать реплику с нуля через полный бэкап.
Спасёт ли max_slot_wal_keep_size от подобной ситуации полностью?
Он ограничивает максимальный объём WAL, удерживаемый ради слотов, но ценой этого ограничения реплика, которая отстала больше лимита, теряет возможность продолжить репликацию инкрементально и требует пересоздания. Это защита диска, а не защита от самого факта забытого слота — правильный процесс важнее одной настройки.
Почему обычный мониторинг диска по проценту заполнения не поймал проблему раньше?
Потому что он реагирует на абсолютный уровень занятого места, а не на скорость и характер роста. Метрика вроде "процент неактивных слотов с растущим retained_wal" ловит проблему на раннем этапе — именно поэтому её стоит выносить в мониторинг отдельно, а не полагаться только на общий алерт по диску.
Актуально ли это для логической репликации так же, как для физической?
Да, причём логические слоты требуют даже больше внимания: помимо WAL они удерживают ресурсы для декодирования изменений (wal_level = logical обязателен для всей базы, а не только для слота), и забытый логический слот в нагруженной базе может забить диск быстрее физического.
Обсудить статью, задать вопрос или начать новую тему
Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.
Перейти в сообщество →