Реплики с задержкой в применении WAL

от автора

В PostgreSQL процесс получения журналов walreceiver запускается на реплике только после того, как процесс startup применит все накопившиеся WAL. Это не создаёт особых проблем, если процесс startup успевает накатывать полученные журналы. Проблема проявляется, если используется реплика с отложенным применением журнальных записей, например, на сутки. На мастере сутки будут копиться журналы и места может не хватить.

Если процесс walreceiver остановится, то он не запустится до тех пор, пока не пройдёт время задержки, установленное параметром recovery_min_apply_delay. Команд ручного запуска процесса walreceiver нет. Получится, что сутки мастер копит журналы, только потом запускается walreceiver и начинает вытягивать журнальные файлы. Такое поведение нелогично, но его задокументировали: «When the standby is started and primary_conninfo is set correctly, the standby will connect to the primary after replaying all WAL files available in the archive. If the connection is established successfully, you will see a walreceiver in the standby, and a corresponding walsender process in the primary.»

Всё время, до запуска walreceiver, слот репликации на мастере удерживает файлы журналов. На мастере скопится много журналов и, если не хватит места, то экземпляр мастера подвиснет по нехватке места в директории pg_wal (или слот инвалидируется по параметру max_slot_wal_keep_size и реплику придётся пересоздать).

Что приводит к остановке walreceiver? Это не только перезапуск экземпляра реплики. Достаточно разрыва сокета между wareceiver и walsender из-за сетевых проблем, перезапуск walsender, перезапуск экземпляра мастера.

То, что walreceiver долго не стартует, не даёт использовать реплики с отложенным применением журналов, как аналог постоянно обновляемых (in place) бэкапов. Утилита pg_combinebackup не может применять инкрементальные бэкапы in-place (она копирует полный бэкап, сливая его с инкрементальными, в новую директорию) и использование реплик с отложенным применением имеет преимущества. Реплику с отложенным применением WAL можно было бы использовать для случаев, когда на мастере возникнет повреждение. При появлении повреждения, можно приостановить применение журналов, сделать бэкап реплики и экспериментировать с репликой (или с бэкапом), докатывая журналы до момента сбоя. Если момент сбоя будет определён неверно, и применятся лишние журналы (позже точки повреждения), то откатить их нельзя, для такого случая и понадобится сделанный вначале бэкап с реплики (или реплика, если экспериментировали с бэкапом), чтобы повторить всё заново.

История проблемы

Впервые, на проблему обратил внимание Константин Книжник в 2019 году. Альваро Эррера подтвердил, что он тоже столкнулся с этой проблемой. Томас Манро, как менеджер коммитфеста, запросил патч с тестом, чтобы патч прошел проверку коммитфест бота. Коммитер Майкл Пакьер счёл логику патча убогой (kind of ugly) и предложил ввести параметр конфигурации, чтобы DBA могли выбирать, когда им нужно запускать walreceiver. Константин добавил параметр. За приятной беседой пролетел год.

Проблема не отпускала пользователей PostgreSQL. Следующим, столкнувшимся с проблемой, и решившимся написать в pg-hackers, был Асим Правен. Он независимо от Константина создал  патч и качественно описал как и когда проявляется проблема. Например, указал, что журнальные записи создаются большим числом серверных процессов, а применяет записи один процесс — startup и, при реальных нагрузках, отставание реплики от мастера будет обычным делом. Также указал, что при реальной работе, с использованием синхронного коммита, поведение synchronous_commit в значении remote_write и on становится эквивалентным remote_apply до тех пор, пока startup не применит накопившиеся журналы и не запустит walreceiver. Такое поведение делает использование синхронной фиксации опасным для мастера.

Майкл отметил, что Асим Правен ввел трети вариант запуска walreceiver: немедленный и дал ценные замечания по логике поиска журнала, который нужно запрашивать.

Асим создал два теста, проявляющие проблему:

walreceiver останавливается из-за разрыва соединения и экземпляр реплики перезапускается. Для демонстрации задержки в применении журнальных записей Асим, использовал параметр recovery_min_apply_delay. Асим отметил, что задержка применения может накапливаться естественным образом, даже если параметр recovery_min_apply_delay
не установлен, поскольку генерация WAL на главном сервере происходит параллельно, а
воспроизведение WAL на резервном сервере выполняется одним процессом.

Асим создал улучшенную логику сканирования файлов журнала реплики для определения того, какой журнал запрашивать у процесса walsender.

Асим написал гораздо более быстрое и менее инвазивное
решение для определения начальной точки. Сканируется первый блок каждого
WAL-файла, начиная с того, который в данный момент читается для наката. Если первый блок валидный, переходим к следующему WAL-файлу проверяем его первый блок. Продолжаем это по одному WAL-файлу за раз, пока файлы не закончатся или блок будет невалидным. Начальной точкой, в ​​этом случае, будет являться начало предыдущего WAL-файла. Нет необходимости читать WAL-файл до конца, по одной записи за раз, как это делалось в патче Константина. Последний полученный LSN, при этом, может не сохраниться на диске и это не будет проблемой.

Патч пообсуждали и улучшили. В августе 2020 года Масахико Савада нашел ошибку. В ноябре 2021 года Борат Рупиредди дал много полезных комментариев по коду патча. За патч взялся Сумьядип Чакраборти и обновил его с учетом давних замечаний Майкла Пакье. Борат нашел в обновленном патче утечку памяти. Казалось бы патч можно коммитить, но…

Киотаро Хоригучи (NTT) попробовал патч и нашел, что патч не работал в простейшем случае. На этом обсуждение патча закончили.

Через 4 года

Через 4 года, в 2025 году у Сунил С. (Broadcom) появилось время, и он взялся за доработку патча. Как выяснил Сунил, Киотаро Хоригучи натолкнулся на проблему, которую породило исправление, внесенное в код PostgreSQL для устранения race condition между архиватором и checkpointer.

  Патч сменил 10 версий и был номинирован на коммитфест 19 версии в конце 2025 года со статусом Ready for Committer, но не нашёл коммитера. Что неудивительно, так как в патче есть строки FIXME.

Третий пост о проблеме появился в апреле 2026 года с заголовком: «баг это или фича реквест??» где справедливо отмечен отложенный сюрприз, который таит нерешенная проблема: первоначальная настройка репликации работает идеально, усыпляя бдительность администратора, а спустя недели/месяцы, как только произойдет перезагрузка экземпляра реплики,  репликация незаметно нарушается. Было отмечено, что в PostgreSQL не существует способа вручную запустить walreceiver и предложено хотя бы дать такую возможность.

Воспроизведение проблемы

Запустим нагрузку:

pgbench -ipgbench -T 6000 -R 700 -P 10

Создадим реплику с отложенным применением журналов:

export PGDATA=/var/lib/postgresql/tantor-se-18-replica/data1pg_ctl stop -D /var/lib/postgresql/tantor-se-18-replica/data1rm -rf  /var/lib/postgresql/tantor-se-18-replica/data1psql -qc "select pg_drop_replication_slot('replica1')"pg_basebackup -D $PGDATA -P -R -C --slot=replica1 -c fastecho "port=5433" >> $PGDATA/postgresql.auto.confecho "cluster_name='replica1'" >> $PGDATA/postgresql.auto.confecho "logging_collector = off" >> $PGDATA/postgresql.auto.confecho "recovery_min_apply_delay = '5min'" >> $PGDATA/postgresql.auto.confpg_ctl start -D $PGDATAwaiting for server to start....done server started

Репликация идёт, walreceiver работает, журналы применяются через 5 минут.

psql -p 5433 -qc "select pg_get_wal_replay_pause_state() state, pg_is_wal_replay_paused() p, pg_last_wal_replay_lsn() replay_lsn, pg_last_wal_receive_lsn() receive_lsn, pg_last_xact_replay_timestamp();"psql -qc "select application_name, state, flush_lsn, replay_lsn, replay_lag from pg_stat_replication"  state    | p | replay_lsn | receive_lsn | pg_last_xact_replay_timestamp  -----------+---+------------+-------------+------------------------------- not paused | f | 0/4000130  | 0/671A360   | 21:25:19.866532+03 (1 row) application_name |   state   | flush_lsn | replay_lsn |   replay_lag     -----------------+-----------+-----------+------------+----------------- replica1         | streaming | 0/671B3D0 | 0/4000130  | 00:01:30.018386 (1 row)

Даже бэкап с реплики выполняется:

rm -rf /var/lib/postgresql/tantor-se-18-replica/data2pg_basebackup -p 5433 -D /var/lib/postgresql/tantor-se-18-replica/data2 -P -R -c fast40190/40190 kB (100%), 1/1 tablespace

Перезапустим экземпляр мастера или реплики:

pg_ctl restart -D /var/lib/postgresql/tantor-se-18-replica/data1psql -qc "select application_name, state, flush_lsn, replay_lsn, replay_lag from pg_stat_replication"psql -p 5433 -qc "select pg_get_wal_replay_pause_state() state, pg_is_wal_replay_paused() p,pg_last_wal_replay_lsn() replay_lsn, pg_last_wal_receive_lsn() receive_lsn, pg_last_xact_replay_timestamp();"waiting for server to shut down....server stopped waiting for server to start....LOG:  entering standby mode LOG:  redo starts at 0/3000080 LOG:  consistent recovery state reached at 0/4000130 LOG:  database system is ready to accept read-only connections done server started application_name | state | flush_lsn | replay_lsn | replay_lag  -----------------+-------+-----------+------------+------------ (0 rows)   state    | p | replay_lsn | receive_lsn | pg_last_xact_replay_timestamp  -----------+---+------------+-------------+------------------------------- not paused | f | 0/4000130  |             | 21:25:19.866532+03 (1 row)

walreceiver не запустился. Он запустится только через recovery_min_apply_delay. А этот параметр может быть выставлен в несколько суток. Просто так поменять его нельзя, так как применятся изменения, и реплику не удастся использовать для восстановления на момент в прошлом.

Решение проблемы задержки с запуском walreceiver

В Tantor Postgres, начиная с версии 17.9 есть параметр wal_receiver_start_at, который устраняет проблему: walreceiver можно запускать сразу после запуска экземпляра или по достижению согласованного состояния. Параметр был добавлен техподдержкой Тантор, по запросу администратора небольшой компании, купившей Tantor Postgres.

Установим параметр wal_receiver_start_at на реплике и перезапустим экземпляр:

psql -p 5433 -qc "alter system set wal_receiver_start_at=startup"pg_ctl restart -D $HOME/tantor-se-18-replica/data1psql -p 5433 -qc "select pg_get_wal_replay_pause_state() state, pg_is_wal_replay_paused() p,pg_last_wal_replay_lsn() replay_lsn, pg_last_wal_receive_lsn() receive_lsn, pg_last_xact_replay_timestamp();" psql -qc "select application_name, state, flush_lsn, replay_lsn, replay_lag from pg_stat_replication"ALTER SYSTEM LOG:  received fast shutdown request waiting for server to shut down....done server stopped waiting for server to start....LOG:  database system was shut down in recovery at 21:31:49 MSK LOG:  requesting stream from beginning of: "000000010000000000000009" LOG:  entering standby mode LOG:  redo starts at 0/3000080 LOG:  started streaming WAL from primary at 0/9000000 on timeline 1 LOG:  consistent recovery state reached at 0/66C92B8 LOG:  database system is ready to accept read-only connections done server started   state    | p | replay_lsn | receive_lsn | pg_last_xact_replay_timestamp  -----------+---+------------+-------------+------------------------------- not paused | f | 0/66FE800  | 0/C899BE8   | 21:26:50.104791+03 (1 row) 21:31:50.113 MSK [92940] LOG:  autoprewarm successfully prewarmed 2550 of 2550 previously-loaded blocks application_name |   state   | flush_lsn | replay_lsn |   replay_lag     -----------------+-----------+-----------+------------+----------------- replica1         | streaming | 0/C89A6E8 | 0/66FFC18  | 00:00:00.574187 (1 row)

Процесс walreceiver запустился сразу после запуска экземпляра реплики.

Защита мастера от повреждений репликой с отложенным применением журналов

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

psql -p 5433 -qc "select pg_wal_replay_pause()" rm -rf /var/lib/postgresql/tantor-se-18-replica/data2pg_basebackup -p 5433 -D /var/lib/postgresql/tantor-se-18-replica/data2 -P -R -c fastLOG:  recovery has paused HINT:  Execute pg_wal_replay_resume() to continue. pg_wal_replay_pause  ---------------------  (1 row)54422/54422 kB (100%), 1/1 tablespace

Примеры повреждений мастера: удаление базы данных или важных объектов по ошибке. Вместо реплики с отложенным применением можно использовать бэкапы. Они могут быть хорошим решением, если размер кластера небольшой. В 17 версии появились инкрементальные бэкапы, но есть недостаток: их нельзя накладывать на директорию с полным бэкапом (in-place).

Заключение

Параметр wal_receiver_start_at позволяет запускать walreceiver на реплике сразу или по достижению согласованного состояния файлов. При перезапуске экземпляра реплики или процесса walreceiver, процесс walreceiver будет немедленно запускаться и продолжать вытягивать журналы без задержки.

Это позволяет использовать реплику с отложенным применением журналов для защиты мастера от повреждений, как постоянно обновляемый (in place) бэкап. С реплики с отложенным применением журналов можно делать бэкапы.

Без параметра wal_receiver_start_at, реплика не может принимать журнальные файлы до истечения recovery_min_apply_delay.

ссылка на оригинал статьи https://habr.com/ru/articles/1041890/