В 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: немедленный (startup), помимо consistency и проблемного (exhaust) и дал ценные замечания по логике поиска журнала, который нужно запрашивать.
Также, Асим создал два теста, проявляющие проблему: walreceiver останавливается из-за разрыва соединения и экземпляр реплики перезапускается. Для демонстрации задержки в применении журнальных записей, Асим, использовал параметр recovery_min_apply_delay. Асим отметил, что задержка применения может накапливаться естественным образом, даже если параметр recovery_min_apply_delay не установлен, поскольку генерация WAL на мастере происходит параллельно, а воспроизведение WAL на реплике выполняется только одним процессом startup.

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

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

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

Через 4 года

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

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

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

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

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

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

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

export PGDATA=/var/lib/postgresql/tantor-se-18-replica/data1
pg_ctl stop -D /var/lib/postgresql/tantor-se-18-replica/data1
rm -rf  /var/lib/postgresql/tantor-se-18-replica/data1
psql -qc "select pg_drop_replication_slot('replica1')"
pg_basebackup -D $PGDATA -P -R -C --slot=replica1 -c fast
echo "port=5433" >> $PGDATA/postgresql.auto.conf
echo "cluster_name='replica1'" >> $PGDATA/postgresql.auto.conf
echo "logging_collector = off" >> $PGDATA/postgresql.auto.conf
echo "recovery_min_apply_delay = '5min'" >> $PGDATA/postgresql.auto.conf
pg_ctl start -D $PGDATA
waiting 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/data2
pg_basebackup -p 5433 -D /var/lib/postgresql/tantor-se-18-replica/data2 -P -R -c fast
40190/40190 kB (100%), 1/1 tablespace

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

pg_ctl restart -D /var/lib/postgresql/tantor-se-18-replica/data1
psql -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 SE 1C.

Установим параметр 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/data1
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"

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/data2
pg_basebackup -p 5433 -D /var/lib/postgresql/tantor-se-18-replica/data2 -P -R -c fast
LOG:  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.

Комментарии (0)