Репликационный слотобъект на primary, который удерживает WAL, пока его потребитель не подтвердит получение — это обещание не перерабатывать WAL раньше времени. Обещание безусловное: слот не различает, отстал ли standby на десять секунд или apply worker подписчика падает с ошибкой с прошлого вторника. Пока удержание ничем не ограничено, pg_wal растёт, пока не заполнит том, и тогда primary падает — вместе с репликой, которую он так старательно защищал. Параметр max_slot_wal_keep_size вводит верхнюю границу этого удержания, но устроен он не так, как подсказывает интуиция: это не бюджет диска, а расстояние, и измеряется оно только в моменты контрольных точек.
Что именно защищает параметр
Неактивный слот удерживает на primary сегменты WAL, которые ещё не получил его потребитель, и расходует место в pg_wal. При max_slot_wal_keep_size = -1 верхней границы нет — значение по умолчанию означает отсутствие предела, поэтому pg_wal может расти, пока не заполнит том, после чего primary упадёт.
Ключевая деталь: значение параметра — это расстояние, а не общий объём. На каждом checkpointмомент, когда PostgreSQL сбрасывает накопленные изменения на диск и фиксирует согласованную точку восстановления берётся самый старый restart_lsnпозиция в WAL, с которой слоту ещё потребуется WAL для продолжения репликации среди всех слотов, и если эта точка отстаёт от текущей позиции записи больше чем на заданное число мегабайт, горизонт удержания сдвигается вперёд ровно на это расстояние, а слоты с более старым restart_lsn инвалидируются. Пока параметр равен -1, измерять нечего: столбец safe_wal_sizeполе в pg_replication_slots, показывающее, сколько байт ещё можно записать в WAL, прежде чем слот окажется под угрозой потери имеет значение NULL и не может подсказать, насколько близок зависший слот к заполнению диска.
Как параметр соотносится с wal_keep_size и max_wal_size
wal_keep_size задаёт минимум старых WAL-файлов, сохраняемых в pg_wal для потоковой репликации, и работает как «пол»: WAL, удерживаемый для него, сохраняется для всех, включая слоты. max_slot_wal_keep_size — «потолок»: максимум WAL, который слоты репликации могут удерживать в pg_wal на момент контрольной точки. Если restart_lsn слота отстаёт от текущего LSN больше чем на заданный размер, слот может стать непригодным из-за удаления нужных WAL.
Эти два параметра не конкурируют. Слот аннулируется только если его restart_lsn оказывается за горизонтом после применения wal_keep_size, а при одновременной установке эффективный лимит слота — больший из двух. max_wal_size — это порог, после которого WAL может удаляться, но файлы всё ещё удерживаются слотом или wal_keep_size; такое состояние отражается значением extended.
Как работает механизм пошагово
Кто и когда проверяет слоты
Проверка слотов репликации выполняется во время контрольной точки: система обходит все слоты и для каждого решает, нельзя ли признать его устаревшим и освободить удерживаемый им WAL. Именно на контрольной точке удобно пересчитать горизонт удержания WAL и заодно избавиться от слотов, которые этот горизонт переросли, не трогая горячий путь записи WAL. Для каждого слота определяется причина, по которой он может быть признан устаревшим; если слот требует WAL, который уже удалён, его restart_lsn оказывается позади границы oldestLSN — точки, до которой WAL разрешено сохранять. Всё, что старше неё, будет удалено, поэтому слот, чей restart_lsn оказался позади этой границы, уже не сможет получить нужные ему файлы. При срабатывании этой причины формируется сообщение с подсказкой увеличить max_slot_wal_keep_size. В контексте restartpoint (recovery) проверка выполняется для двух причин сразу: удаление нужного слоту WAL и простой слота дольше настроенного idle_replication_slot_timeout.
Как вычисляется порог
Порог вычисляется в функции KeepLogSeg: она отступает *logSegNo к последнему сегменту, который нужно сохранить из-за wal_keep_size или слотов репликации. Сначала берётся минимум LSN по слотам через XLogGetReplicationSlotMinimumLSN, и если он валиден и меньше recptr, номер сегмента пересчитывается по этому LSN. Затем учитывается max_slot_wal_keep_size_mb: если текущий сегмент отстоит от полученного больше чем на slot_keep_segs, порог подтягивается вперёд ровно на это число сегментов. После этого, если wal_keep_size_mb > 0, порог дополнительно отступает на keep_segs, вычисляемый из wal_keep_size_mb. CheckPointSegments в этой формуле не участвует — он используется в XLogCheckpointNeeded для решения о необходимости чекпоинта. Итоговое значение — это segno, полученный последовательным применением ограничений от слотов, max_slot_wal_keep_size и wal_keep_size.
Что происходит при инвалидации
Инвалидация выполняется перебором слотов под общей блокировкой ReplicationSlotControlLock в режиме LW_SHARED. Сохранение слотов на диск делает CheckPointReplicationSlots, удерживая ReplicationSlotAllocationLock в режиме LW_SHARED: для каждого используемого слота строится путь в PG_REPLSLOT_DIR и вызывается SaveSlotToPath. Поля restart_lsn и last_saved_restart_lsn читаются под спинлоком слота; для персистентного слота при restart_lsn > last_saved_restart_lsn берётся last_saved_restart_lsn. ReportSlotInvalidation только формирует и логирует сообщение о причине инвалидации, не меняя restart_lsn.
Связь с другими причинами инвалидации
Проверка принимает маску причин и за один проход пытается инвалидировать слот по нескольким возможным причинам сразу, минимизируя лишние итерации. Причина, связанная с удалением нужного слоту WAL, связана с max_slot_wal_keep_size через подсказку увеличить этот параметр. Причина, связанная с простоем слота дольше настроенного idle_replication_slot_timeout, обрабатывается в той же функции, но её применимость ограничена: слот должен иметь валидный restart_lsn, ненулевое время простоя и не быть синхронизированным слотом в процессе recovery. Порядок проверки причин последовательный, и возвращается первая подходящая: сначала причина, связанная с удалением нужного слоту WAL, затем конфликт с xid horizon, затем требование к wal_level, затем простой слота.
Пороги и значения по умолчанию
По умолчанию max_slot_wal_keep_size равно -1, что означает отсутствие ограничения. Единица измерения — мегабайты, но разрешение — целый WAL-сегмент: значение переводится в целые сегменты целочисленным делением, поэтому всё меньше wal_segment_size (16 МБ, если не менялось) округляется вниз до 0. При этом 0 — это не «выключено»: единственное написание «без ограничения» — это -1. При -1 столбец safe_wal_size равен NULL. Параметр имеет контекст sighup, то есть задаётся без перезапуска.
Соотношение с wal_keep_size на примере
Если заданы оба параметра, эффективный предел для слота — больший из них. На версии 18.6 при wal_keep_size = 256MB и max_slot_wal_keep_size = 96MB слот, отставший на 184 MB, оставался в состоянии extendedслот удерживает WAL-файлы сверх max_wal_size, но они ещё не удалены через контрольную точку; как только wal_keep_size был сброшен, следующая контрольная точка сделала слот недействительным. Состояние extended отличается от reservedудерживаемые файлы укладываются в max_wal_size тем, что max_wal_size уже превышен, и от unreservedслот больше не удерживает требуемые WAL-файлы, часть будет удалена на следующей контрольной точке тем, что файлы ещё на месте.
Задержка между превышением порога и инвалидацией
Инвалидация по простою происходит во время контрольной точки, поэтому между превышением idle_replication_slot_timeout и фактической инвалидацией есть задержка, зависящая от интервала checkpoint_timeout. Чтобы избежать такой задержки, можно принудительно выполнить контрольную точку. Для инвалидации по WAL-лимиту проверка также привязана к контрольной точке.
Состояние wal_status показывает близость к инвалидации: unreserved означает, что слот больше не удерживает требуемые WAL-файлы и часть из них будет удалена на следующей контрольной точке. В примере с max_wal_size = 48MB и max_slot_wal_keep_size = 96MB слот был инвалидирован первой контрольной точкой после 110 MB отставания, а весь процесс занял около секунды, потому что генерация 100 MB WAL занимает примерно секунду.
Что видит клиент и мониторинг
Значения wal_status
Столбец wal_status в pg_replication_slots показывает доступность WAL-файлов, удерживаемых слотом:
reserved— удерживаемые файлы находятся в пределахmax_wal_size;extended—max_wal_sizeпревышен, но файлы всё ещё удерживаются слотом илиwal_keep_size;unreserved— слот больше не удерживает требуемые WAL-файлы, часть будет удалена при следующей контрольной точке; типично при неотрицательномmax_slot_wal_keep_size, состояние может вернуться вreservedилиextended;lost— слот больше не пригоден для использования.
Как читать safe_wal_size
safe_wal_size вычисляется только для слотов, которые не потеряны, и только при заданном максимальном размере. Если слот потерян или max_slot_wal_keep_size равен -1, в столбец записывается NULL. Значение вычисляется как разница между LSN, при достижении которого слот потеряет свой сегмент, и текущим LSN:
failSeg = targetSeg + Max(slotKeepSegs, keepSegs) + 1;
values[i++] = Int64GetDatum(failLSN - currlsn);Единица измерения — байты. Значение может становиться отрицательным в состоянии unreserved.
Сообщения в логе и ошибки клиента
При инвалидации слота primary пишет в лог одно из двух сообщений: если слот неактивен — «invalidating obsolete replication slot "%s"», а если к слоту подключён walsender — сначала «terminating process %d to release replication slot "%s"». В обоих случаях добавляется DETAIL, зависящий от причины:
- для
RS_INVAL_WAL_REMOVED— «The slot's restart_lsn %X/%08X exceeds the limit by %" PRIu64 " bytes.» и HINT «You might need to increase "max_slot_wal_keep_size".»; - для
RS_INVAL_HORIZON— «The slot conflicted with xid horizon %u.»; - для
RS_INVAL_WAL_LEVEL— «Logical decoding on standby requires the primary server to either set "wal_level" >= "logical" or have at least one logical slot when "wal_level" = "replica".»; - для
RS_INVAL_IDLE_TIMEOUT— «The slot's idle time of %lds exceeds the configured "idle_replication_slot_timeout" duration of %ds.» и HINT «You might need to increase "idle_replication_slot_timeout".».
Клиент, подключённый к walsender, при принудительном завершении процесса видит «terminating connection due to administrator command». После инвалидации слот остаётся в pg_replication_slots с wal_status = 'lost', invalidation_reasonстолбец, показывающий причину, по которой слот был инвалидирован = 'wal_removed' (в версии 17 и новее), NULL restart_lsn и замороженным inactive_since. Любая попытка использовать такой слот приводит к ошибке «ERROR: can no longer access replication slot "sub"» с DETAIL «This replication slot has been invalidated due to "wal_removed".».
Пограничные случаи
Активный walsender в момент инвалидации
Когда слот с подключённым walsender становится недействительным, checkpointer сначала завершает этот процесс: в лог пишется сообщение вида «terminating process 277 to release replication slot "stall"», а клиент видит «terminating connection due to administrator command». Затем слот инвалидируется и WAL удаляется. Слот остаётся в pg_replication_slots с wal_status = 'lost', invalidation_reason = 'wal_removed', NULL restart_lsn и замороженным inactive_since, продолжая занимать один из max_replication_slots.
Возврат старого standby
При возврате старого standby после инвалидации слот уже помечен как недействительный, и продолжить с того же restart_lsn нельзя. В функции pg_get_replication_slots при invalidated != RS_INVAL_NONE состояние WAL принудительно устанавливается в WALAVAIL_REMOVED, а затем в зависимости от наличия активного процесса выводится либо «unreserved», либо «lost». Причина RS_INVAL_WAL_REMOVED фиксируется, когда restart_lsn слота меньше oldestLSN, то есть требуемый слоту WAL уже удалён.
Сетевой разрыв и конкуренция слотов
При сетевом разрыве, когда standby не читает WAL дольше, чем допускает max_slot_wal_keep_size, слот в конечном итоге инвалидируется: checkpointer при очередном checkpoint сравнивает самый старый restart_lsn среди всех слотов с текущей позицией записи и, если отставание превышает лимит, сдвигает горизонт удержания WAL вперёд и инвалидирует слоты, чей restart_lsn оказался позади.
Отличие при нескольких слотах в том, что WAL удерживается один раз, а не по разу на слот, поэтому десять слотов стоят столько же диска, сколько один, и параметр ограничивает лишь то, насколько далеко может отстать самый медленный слот. Конкуренция слотов за один и тот же WAL не увеличивает расход диска, но именно самый отставший слот определяет, когда горизонт удержания будет сдвинут и слоты позади него инвалидированы.
Почему сделано именно так
Инвалидация на checkpoint, а не при записи WAL
Инвалидация слотов выполняется на checkpoint, потому что сама функция инвалидации запускается как часть контрольной точки. Это даёт компромисс: порог по WAL проверяется не в момент записи WAL, а на checkpoint, поэтому возможна задержка между превышением порога и фактической инвалидацией. Для idle-таймаута это прямо описано: инвалидация происходит во время checkpoint, и между превышением idle_replication_slot_timeout и инвалидацией есть задержка, зависящая от интервала checkpoint_timeout. Пользователь может уменьшить эту задержку, принудительно вызвав checkpoint.
Для ограничения по WAL на каждом checkpoint берётся самый старый restart_lsn среди всех слотов, и если он отстаёт больше чем на max_slot_wal_keep_size, горизонт удержания сдвигается вперёд и слоты с более старым restart_lsn инвалидируются. Накладные расходы на горячем пути записи WAL снижаются за счёт того, что проверка и удаление WAL выполняются в checkpoint, а не при каждой записи; при этом WAL удерживается один раз, а не по разу на слот.
Инвалидировать, а не удалять
Слот не удаляется автоматически, а помечается недействительным, чтобы администратор мог увидеть причину и решить, что делать. Причина RS_INVAL_WAL_REMOVED означает, что restart_lsn слота отстал от oldestLSN; в лог пишется «invalidating obsolete replication slot "%s"» с деталью о превышении лимита в байтах и подсказкой увеличить max_slot_wal_keep_size. Причина RS_INVAL_IDLE_TIMEOUT срабатывает, когда слот простаивает дольше заданного idle_replication_slot_timeout, с деталью о времени простоя и подсказкой увеличить параметр. Причина RS_INVAL_HORIZON означает конфликт с xid horizon. Причина RS_INVAL_WAL_LEVEL означает, что логический слот на стендбае требует wal_level >= logical или хотя бы одного логического слота при wal_level = replica. Если недействительным стал последний логический слот, запрашивается отключение логического декодирования.
Что из этого следует на практике
- Значение по умолчанию
-1— это не безопасно, а безлимитно. Пока параметр равен-1,pg_walрастёт, пока не заполнит том, иsafe_wal_sizeничего не показывает. Если на primary есть репликационные слоты, отсутствие лимита — это отложенный отказ диска. - Параметр задаёт расстояние, а не бюджет. Он ограничивает, насколько далеко может отстать самый медленный слот, а не суммарный объём WAL. Десять слотов стоят столько же диска, сколько один.
- Проверка происходит только на checkpoint. Между превышением порога и инвалидацией есть задержка, зависящая от
checkpoint_timeout. Если нужно инвалидировать слот немедленно, поможет принудительная контрольная точка. wal_keep_size— пол, а не конкурент. WAL, удерживаемый для него, сохраняется для всех, включая слоты, и при одновременной установке эффективный лимит слота — больший из двух. Сбросwal_keep_sizeможет немедленно привести к инвалидации слота на следующей контрольной точке.- 0 — это не «выключено». Единственное написание «без ограничения» —
-1. Ноль означает отсутствие удержания сверх того, что контрольная точка хранит для собственного восстановления после сбоя. - Инвалидированный слот остаётся в
pg_replication_slotsсwal_status = 'lost', NULLrestart_lsnи замороженнымinactive_since, продолжая занимать место вmax_replication_slots. Старый standby не сможет продолжить с того жеrestart_lsn— этот WAL утрачен. safe_wal_size— рабочий инструмент мониторинга, но только при заданном лимите. Он показывает, сколько байт ещё можно записать до опасности перехода слота вlost, и уходит в минус в состоянииunreserved.