Learning Platform
Урок 14.02 · 23 мин
Продвинутый
CheckpointRecoveryWALDurabilityTuning

В прошлом уроке мы выяснили: durability в Postgres достигается через WAL. После COMMIT; транзакция уже на disk — но только в журнале, не в основных heap-файлах. Сами страницы остаются dirty в shared_buffers и могут лежать там долго.

Возникает два вопроса. Первый: когда вообще dirty pages сбрасываются на диск, и почему БД работает быстро, если каждый COMMIT не дёргает heap? Второй: что произойдёт после crash, когда мы перезапустим сервер? Будем ли мы реплеить весь WAL с момента initdb? Это очевидно нереально.

На оба вопроса отвечает checkpoint.

Что такое checkpoint

Checkpoint — это момент синхронизации shared_buffers с disk. Конкретная последовательность действий:

  1. Postgres выбирает текущий LSN — checkpoint LSN. Это будет «новая точка отсчёта для recovery».
  2. Все страницы в shared_buffers, которые были dirty на момент начала checkpoint, постепенно сбрасываются на disk через fsync(). Сброс размазан по checkpoint_completion_target (по умолчанию 0.9 от checkpoint_timeout) — чтобы не было всплеска I/O.
  3. В WAL пишется запись XLOG_CHECKPOINT_ONLINE с информацией о текущем LSN, активных транзакциях, redo-pointer.
  4. Обновляется файл pg_control — там хранится «последний завершённый checkpoint LSN». Этот файл — критическая метаинформация кластера; его повреждение = БД не запускается.

После checkpoint мы знаем: все изменения, чей LSN ≤ checkpoint LSN, физически на disk в heap-файлах. WAL-записи до этого LSN больше не нужны для recovery — их можно удалять (с поправкой на replication slots и archive — см. дальше).

Lifecycle WAL и checkpoint

Поток времени слева направо. Между checkpoints копится WAL и dirty pages. После checkpoint всё, что было dirty, лежит на disk; redo-pointer для recovery = LSN последнего checkpoint.

Время →WAL пишется непрерывно, checkpoint срабатывает по таймеру или объёму
Checkpoint ALSN 0/1000000
WAL накапливаетсяUPDATE/INSERT идут в журнал; dirty pages в shared_buffers
Checkpoint BLSN 0/2000000
WAL копится дальше...
CrashLSN 0/2800000
Recovery после crash: redo с LSN(Checkpoint B) = 0/2000000 до конца WAL = 0/2800000реплеить нужно ~8 MiB WAL — не весь журнал с initdb

Когда checkpoint срабатывает

Триггеры checkpoint’а:

  • По времениcheckpoint_timeout (по умолчанию 5 минут). Каждые N минут — checkpoint независимо от объёма.
  • По объёму WALmax_wal_size (по умолчанию 1 GiB в 14+, было 16 MiB в старых версиях). Если с последнего checkpoint накопилось ≥ max_wal_size WAL — checkpoint срабатывает.
  • ЯвноCHECKPOINT; или pg_ctl stop -m fast.
  • Перед бэкапомpg_start_backup() форсирует checkpoint, чтобы база backup’а была консистентной.

В нормальной работающей системе хочется, чтобы checkpoint срабатывал по таймеру, а не по объёму. Если checkpoint срабатывает по max_wal_size — это признак, что таймер слишком долгий или WAL генерируется быстрее, чем ожидалось. Это видно в pg_stat_bgwriter:

Статистика checkpoint'ов: сколько по таймеру, сколько по объёму, сколько dirty buffers сброшено. checkpoints_req > checkpoints_timed → max_wal_size мал.

PostgreSQL

В pglite эта вьюшка тоже доступна, но цифры там фиктивные. На реальной БД смотрим на отношение checkpoints_req / (checkpoints_timed + checkpoints_req): если > 10%, поднимай max_wal_size или checkpoint_timeout.

Что копится в WAL между checkpoints

Между двумя checkpoints в WAL копятся все изменения данных: INSERT, UPDATE, DELETE, DDL, TOAST, индексные операции, freeze marks от VACUUM. Сюда же — full-page images при первой модификации каждой страницы после checkpoint.

Это важное место для тюнинга. Чем чаще checkpoints — тем больше FPI попадает в WAL (каждый checkpoint «обнуляет» bit «страница модифицирована после checkpoint»). Чем реже — тем дольше recovery после crash, потому что больше WAL нужно реплеить.

Типичный баланс: checkpoint_timeout = 15min, max_wal_size = 4 GiB-16 GiB, checkpoint_completion_target = 0.9. Это держит recovery time в пределах минут и при этом не плодит лишние FPI.

Crash recovery: что делает Postgres при старте

Когда Postgres стартует и видит, что прошлый shutdown был нечистый (флаг в pg_control), он входит в recovery mode:

  1. Читает pg_control, находит LSN последнего успешного checkpoint и redo pointer.
  2. Открывает WAL-сегмент, в котором лежит redo LSN.
  3. Идёт по WAL последовательно. Для каждой WAL-записи: применяет её к соответствующей странице. Если запись — full-page image, страница заменяется целиком. Если delta — применяется поверх.
  4. Доходит до конца WAL (последняя валидная запись с правильной checksum). На этом моменте recovery завершён, БД переходит в up-режим.
  5. Перед открытием для клиентов делается restart-after-recovery checkpoint.

Длительность recovery зависит от объёма WAL между last checkpoint и crash, и от того, насколько медленный disk. На современном NVMe реальная скорость replay — десятки MiB/s. То есть 1 GiB WAL — это минута recovery. Это и есть причина, почему max_wal_size ограничивает recovery time.

На реальной БД после рестарта можно посмотреть в системные view recovery-state. В pglite recovery не происходит (она in-memory), но проверим, что мы не в recovery сейчас.

PostgreSQL

Демо: смотрим состояние shared_buffers и checkpoint-нагрузки

В реальной БД полезно мониторить, сколько dirty pages в shared_buffers и кто их сбрасывает. В pglite метрики ограничены, но синтаксис настоящий.

Сколько dirty buffers в shared_buffers сейчас. Через pg_buffercache (extension, нужен на real DB). На production: если dirty постоянно высокая доля от total — checkpoint-burst при следующем срабатывании будет тяжёлым.

PostgreSQL

Параметры тюнинга

Главные параметры и интуиция:

  • checkpoint_timeout (1min — 1day, default 5min) — максимальный интервал между checkpoint’ами. Подними до 15-30 минут, если IO позволяет. Это уменьшит FPI-overhead.
  • max_wal_size (default 1 GiB) — мягкий верхний предел WAL между checkpoint’ами. «Мягкий», потому что Postgres не блокирует запись при превышении — он просто триггерит checkpoint. Подними до 4-16 GiB на больших OLTP.
  • min_wal_size (default 80 MiB) — сколько WAL держать неудалённым для переиспользования (rename вместо unlink+create). Помогает на write-bursts.
  • checkpoint_completion_target (0.0-1.0, default 0.9) — какую долю интервала растягивать запись dirty pages. Высокое значение = меньше пиковый IOPS, дольше окно слабого throughput.

Антипаттерн: checkpoint_timeout = 1min на write-heavy БД. Каждые 60 секунд все «горячие» страницы получают FPI, WAL раздувается в 5-10 раз, а disk занят бесконечным fsync(). Лекарство — увеличить интервал.

Стратегия тюнинга на новом сервере:

  1. Начать с checkpoint_timeout = 15min, max_wal_size = 4 GiB, completion_target = 0.9.
  2. Под продакшен-нагрузкой посмотреть pg_stat_bgwriter через час — если checkpoints_req ≈ 0, всё хорошо. Если >5% от total — поднимать max_wal_size.
  3. Замерить, сколько времени занимает recovery после kill -9 + restart. Если > RTO, уменьшать max_wal_size.
  4. Включить wal_compression = lz4 — почти всегда чистый профит.

Какие процессы участвуют в checkpoint

В исходниках Postgres за checkpoint отвечают:

  • checkpointer — отдельный backend-процесс, единственный. Запускает sequence и сбрасывает dirty pages.
  • bgwriter — фоновый писатель, который между checkpoint’ами постепенно сбрасывает dirty pages «по горячности» (LRU). Это снижает пик IO во время checkpoint и уменьшает шанс, что backend-у придётся самому ждать flush’а страницы.
  • walwriter — отдельный процесс для flush WAL (не путать с checkpointer).
  • backend’ы — сами могут писать dirty pages при BufferAlloc, если в shared_buffers нет свободного слота. Это худший сценарий: запрос ждёт IO. Метрика buffers_backend в pg_stat_bgwriter должна быть много меньше buffers_checkpoint + buffers_clean.
Кто пишет dirty pages на disk

Трое сбрасывают страницы: checkpointer во время checkpoint, bgwriter между ними, backend в крайнем случае. buffers_backend > buffers_clean — bgwriter не справляется, увеличь bgwriter_lru_maxpages.

checkpointerperiodically: flush all dirty as of checkpoint LSN
bgwritercontinuously: flush hot LRU dirty pages
backend (худший случай)на BufferAlloc, если нет свободного слота

Что НЕ делает checkpoint

  • Не блокирует запись и чтение. Lazy checkpoint берёт минимальный lock, не конфликтует с обычными запросами.
  • Не очищает WAL-файлы. Удаление старых сегментов делает следующий checkpoint и фоновый процесс walwriter/archive. Если есть replication slot с устаревшим restart_lsn или archive_command ломается — WAL-файлы не удаляются, и pg_wal/ может расти бесконечно. Это частая авария: место на disk заканчивается из-за повешенного slot’а.
  • Не уменьшает recovery time, если ты крашнулся прямо в момент checkpoint. Half-completed checkpoint остаётся как обычные WAL-записи; recovery от него восстановит как с предыдущего полного.

Checkpoint и репликация: что увидит standby

Когда на primary происходит checkpoint, в WAL пишется запись XLOG_CHECKPOINT_ONLINE. Эта запись передаётся по replication-стриму и применяется на каждой replica. Что делает replica с ней:

  • Обновляет свой pg_control, чтобы знать «последний checkpoint LSN от primary».
  • НЕ делает свой собственный checkpoint автоматически. Standby выполняет restartpoint — аналог checkpoint, но привязанный к LSN полученного checkpoint от primary. Restartpoint срабатывает раз в checkpoint_timeout (то же время, что на primary), но привязан к last replayed checkpoint LSN.

Это важно для standby crash recovery: если standby сам упал, при рестарте он начинает replay не с самого начала входящего WAL, а с last restartpoint. Без restartpoint reboot replica замусоривал бы CPU на повторном применении уже applied изменений.

Параметр checkpoint_flush_after (по умолчанию 256 KiB) — сколько байт между явными posix_fadvise(POSIX_FADV_DONTNEED)-вызовами на checkpoint. Это смягчает IO-burst на disk: вместо «огромный fsync в конце» получается «писать-постепенно-говорить-OS-не-кешируй».

Проверка знанийKnowledge check
У вас Postgres, checkpoint_timeout = 5min, max_wal_size = 1 GiB. В pg_stat_bgwriter: checkpoints_timed = 12, checkpoints_req = 240 за последний час. Что не так и как это исправить?
ОтветAnswer
checkpoints_req сильно больше checkpoints_timed (240 vs 12) означает: checkpoint'ы триггерятся по объёму WAL (max_wal_size), а не по таймеру. То есть за час было ~240 checkpoint'ов по 1 GiB WAL каждый — суммарно 240 GiB WAL. Каждый checkpoint порождает FPI для всех горячих страниц, что раздувает WAL ещё больше — это самоподдерживающийся feedback loop. Лекарство: подними max_wal_size до 8-16 GiB. Тогда checkpoint начнёт срабатывать по таймеру (раз в 5 минут), FPI-overhead упадёт, общий WAL throughput может уменьшиться в 2-3 раза. Дополнительно — checkpoint_timeout до 15min, completion_target оставить 0.9 (или поднять до 0.95). Главное: следить, чтобы recovery time (max_wal_size / WAL replay rate) оставался в приемлемых рамках.

Spread checkpoint vs immediate

Параметр checkpoint_completion_target управляет тем, насколько checkpoint «растянут» во времени:

  • 0.0 — immediate checkpoint. Все dirty pages пишутся максимально быстро, занимая весь IO. Используется только при shutdown через pg_ctl stop -m fast или явной CHECKPOINT;.
  • 0.5 — spread на 50% от checkpoint_timeout. То есть если timeout = 10 минут, dirty pages размазаны на 5 минут.
  • 0.9 (default) — spread на 90%. Самое мягкое для disk: IO почти равномерно по интервалу.

На write-heavy системах поднимают до 0.95. Дальше нельзя — checkpointer должен успеть до начала следующего checkpoint, и слишком близкий к 1.0 даёт риск «не успели завершить, а уже надо начинать».

Симптом плохого тюнинга — пилообразный график disk write IOPS: всплеск каждые checkpoint_timeout минут. Это означает «checkpoint всё пишет в конце» — completion_target слишком мал или bgwriter ленив.

Restart-after-recovery checkpoint

После crash recovery первый шаг open’а БД — выполнение restart checkpoint. Это нужно, чтобы:

  • Зафиксировать новое consistent состояние shared_buffers.
  • Освободить место в pg_wal/: до этого checkpoint мы держали все WAL после last checkpoint pre-crash; после — можно начать удалять.
  • Создать новую точку отсчёта для возможного следующего crash.

Restart checkpoint поэтому может быть «тяжёлым» по IO — он скидывает все dirty pages, скопившиеся за время replay. Если у тебя машина после crash долго грузится — большая часть времени это recovery WAL + restart checkpoint.

Чек-лист

  • Checkpoint — момент, когда все dirty pages сброшены на disk и в WAL записана XLOG_CHECKPOINT_ONLINE отметка. После checkpoint WAL до этого LSN не нужен для recovery.
  • Триггеры: checkpoint_timeout (default 5min), max_wal_size (default 1 GiB), явный CHECKPOINT;, перед бэкапом.
  • Хочется чтобы checkpoint срабатывал по таймеру, не по объёму. checkpoints_req / total > 10% — повод поднять max_wal_size.
  • Между checkpoints в WAL копятся все изменения + FPI первой модификации каждой страницы.
  • Crash recovery = read pg_control → найти last checkpoint LSN → replay WAL до конца. Время recovery ≈ объём WAL / replay rate.
  • Старые WAL не удаляются автоматически, если есть зависший replication slot или ломается archive_command. pg_wal/ может разорвать disk.
Inodes — метаданные файлов и жизнь без имени ReplicatedMergeTree и ClickHouse Keeper

Проверьте понимание

Результат: 0 из 0
Концептуальный
Вопрос 1 из 4. Что именно делает checkpoint в PostgreSQL?

Закончили урок?

Отметьте его как пройденный, чтобы отслеживать свой прогресс

Войдите чтобы оценить урок

Прогресс модуля
0 из 5