Назад к блогу

max_slot_wal_keep_size: как не дать репликационным слотам забить диск и уронить primary

max_slot_wal_keep_size: как не дать репликационным слотам забить диск и уронить primary

Параметр max_slot_wal_keep_size ограничивает объём WAL, который могут удерживать репликационные слоты, предотвращая переполнение диска на primary. Разбираем, как именно работает этот механизм, почему его значение — это расстояние, а не бюджет, и как он взаимодействует с wal_keep_size и max_wal_size.

Репликационный слот — это обещание не перерабатывать 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 берётся самый старый restart_lsn среди всех слотов, и если эта точка отстаёт от текущей позиции записи больше чем на заданное число мегабайт, горизонт удержания сдвигается вперёд ровно на это расстояние, а слоты с более старым restart_lsn инвалидируются. Пока параметр равен -1, измерять нечего: столбец safe_wal_size имеет значение 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_keep_size был сброшен, следующая контрольная точка сделала слот недействительным. Состояние extended отличается от reserved тем, что max_wal_size уже превышен, и от unreserved тем, что файлы ещё на месте.

Задержка между превышением порога и инвалидацией

Инвалидация по простою происходит во время контрольной точки, поэтому между превышением 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', NULL restart_lsn и замороженным inactive_since, продолжая занимать место в max_replication_slots. Старый standby не сможет продолжить с того же restart_lsn — этот WAL утрачен.
  • safe_wal_size — рабочий инструмент мониторинга, но только при заданном лимите. Он показывает, сколько байт ещё можно записать до опасности перехода слота в lost, и уходит в минус в состоянии unreserved.

Источники

Похожее