В прошлом уроке мы выяснили: durability в Postgres достигается через WAL. После COMMIT; транзакция уже на disk — но только в журнале, не в основных heap-файлах. Сами страницы остаются dirty в shared_buffers и могут лежать там долго.
Возникает два вопроса. Первый: когда вообще dirty pages сбрасываются на диск, и почему БД работает быстро, если каждый COMMIT не дёргает heap? Второй: что произойдёт после crash, когда мы перезапустим сервер? Будем ли мы реплеить весь WAL с момента initdb? Это очевидно нереально.
На оба вопроса отвечает checkpoint.
Что такое checkpoint
Checkpoint — это момент синхронизации shared_buffers с disk. Конкретная последовательность действий:
- Postgres выбирает текущий LSN —
checkpoint LSN. Это будет «новая точка отсчёта для recovery». - Все страницы в
shared_buffers, которые были dirty на момент начала checkpoint, постепенно сбрасываются на disk черезfsync(). Сброс размазан поcheckpoint_completion_target(по умолчанию 0.9 отcheckpoint_timeout) — чтобы не было всплеска I/O. - В WAL пишется запись
XLOG_CHECKPOINT_ONLINEс информацией о текущем LSN, активных транзакциях, redo-pointer. - Обновляется файл
pg_control— там хранится «последний завершённый checkpoint LSN». Этот файл — критическая метаинформация кластера; его повреждение = БД не запускается.
После checkpoint мы знаем: все изменения, чей LSN ≤ checkpoint LSN, физически на disk в heap-файлах. WAL-записи до этого LSN больше не нужны для recovery — их можно удалять (с поправкой на replication slots и archive — см. дальше).
Поток времени слева направо. Между checkpoints копится WAL и dirty pages. После checkpoint всё, что было dirty, лежит на disk; redo-pointer для recovery = LSN последнего checkpoint.
Когда checkpoint срабатывает
Триггеры checkpoint’а:
- По времени —
checkpoint_timeout(по умолчанию 5 минут). Каждые N минут — checkpoint независимо от объёма. - По объёму WAL —
max_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 мал.
В 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:
- Читает
pg_control, находит LSN последнего успешного checkpoint иredopointer. - Открывает WAL-сегмент, в котором лежит
redoLSN. - Идёт по WAL последовательно. Для каждой WAL-записи: применяет её к соответствующей странице. Если запись — full-page image, страница заменяется целиком. Если delta — применяется поверх.
- Доходит до конца WAL (последняя валидная запись с правильной checksum). На этом моменте recovery завершён, БД переходит в
up-режим. - Перед открытием для клиентов делается
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 сейчас.
Демо: смотрим состояние shared_buffers и checkpoint-нагрузки
В реальной БД полезно мониторить, сколько dirty pages в shared_buffers и кто их сбрасывает. В pglite метрики ограничены, но синтаксис настоящий.
Сколько dirty buffers в shared_buffers сейчас. Через pg_buffercache (extension, нужен на real DB). На production: если dirty постоянно высокая доля от total — checkpoint-burst при следующем срабатывании будет тяжёлым.
Параметры тюнинга
Главные параметры и интуиция:
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(). Лекарство — увеличить интервал.
Стратегия тюнинга на новом сервере:
- Начать с
checkpoint_timeout = 15min,max_wal_size = 4 GiB,completion_target = 0.9. - Под продакшен-нагрузкой посмотреть
pg_stat_bgwriterчерез час — еслиcheckpoints_req≈ 0, всё хорошо. Если >5% от total — подниматьmax_wal_size. - Замерить, сколько времени занимает recovery после kill -9 + restart. Если > RTO, уменьшать
max_wal_size. - Включить
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.
Трое сбрасывают страницы: checkpointer во время checkpoint, bgwriter между ними, backend в крайнем случае. buffers_backend > buffers_clean — bgwriter не справляется, увеличь bgwriter_lru_maxpages.
Что НЕ делает 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-не-кешируй».
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.