Представьте, что сервер postgresql выключили рывком - выдернули питание прямо посреди транзакции. Откуда база знает, что уже зафиксировано, а что нет, и почему данные на диске не превращаются в кашу? Ответ - журнал предзаписи WAL (Write-Ahead Log). Это сердце надёжности PostgreSQL. В этом уроке разберём, зачем нужен WAL, как он обеспечивает краш-рекавери, репликацию и восстановление на момент времени (PITR), что такое контрольные точки (checkpoint) и процесс checkpointer, и как параметры full_page_writes, fsync, synchronous_commit и wal_level балансируют между долговечностью и скоростью записи.

Как это работает
Главная идея одной фразой: прежде чем менять страницу данных, запиши намерение в журнал. Когда транзакция что-то правит, PostgreSQL не бежит сразу переписывать табличные файлы на диске. Сначала он формирует компактную запись об изменении (WAL-запись) и кладёт её в журнал. Слово "предзапись" буквально означает: журнал пишется ВПЕРЁД самих данных.
Это даёт фокус. Сами страницы таблиц и индексов живут в разделяемой памяти (буферном кэше) и считаются грязными, пока их не сбросили на диск. На диск их можно отправить лениво, потом, пачкой. А вот WAL при коммите обязан физически дойти до диска. Получается, что вместо множества случайных записей в разные файлы мы делаем одну последовательную дозапись в конец журнала - а последовательная запись на любом носителе в разы быстрее случайной.
Долговечность (буква D в ACID) обеспечивается именно так: в момент COMMIT процесс backend вызывает сброс журнала и ждёт подтверждения, что WAL до этого места лёг на диск. Если сразу после этого сервер падает, при следующем старте PostgreSQL входит в восстановление: читает журнал и заново проигрывает (накатывает) все изменения, которые не успели попасть в файлы данных. Это и есть краш-рекавери. Зафиксированное не теряется, незавершённое откатывается.
Контрольная точка (checkpoint) - это гарантия, с которой можно начинать восстановление. В этот момент фоновый процесс checkpointer сбрасывает на диск все грязные страницы, накопившиеся до определённой позиции в журнале. После успешного checkpoint всё, что было раньше, уже надёжно в файлах данных, и старые сегменты WAL можно переиспользовать или удалить. Поэтому восстановление после краха начинается не с начала времён, а с последней контрольной точки - чем чаще checkpoint, тем быстрее старт после сбоя, но тем выше нагрузка на диск в обычной работе. Это классический компромисс, которым вы управляете через checkpoint_timeout и max_wal_size.
Зачем full_page_writes. Запись страницы на диск не атомарна: типичная страница PostgreSQL 8 КБ, а сектор диска 512 байт или 4 КБ. При сбое питания страница может оказаться записанной наполовину (torn page, разорванная запись). Чтобы такую полусломанную страницу можно было починить, после каждого checkpoint при первом изменении страницы PostgreSQL пишет в журнал её полный образ. Это раздувает WAL, зато гарантирует, что восстановление получит целую страницу, поверх которой накатит остальные изменения.
SQL и примеры
Посмотрим ключевые параметры долговечности. Демобаза "Авиаперевозки" (схема bookings) подойдёт, чтобы погонять реальную запись.
Код: Выделить всё
-- какой уровень журналирования и режимы безопасности заданы
SHOW wal_level; -- minimal | replica | logical
SHOW fsync; -- on - синхронизировать запись с диском
SHOW synchronous_commit; -- on | off | local | remote_apply ...
SHOW full_page_writes; -- on - писать полные образы страниц
-- сразу группой, с единицами измерения
SELECT name, setting, unit
FROM pg_settings
WHERE name IN ('wal_level','fsync','synchronous_commit',
'full_page_writes','checkpoint_timeout','max_wal_size');
Код: Выделить всё
-- текущая точка записи в журнал (LSN, Log Sequence Number)
SELECT pg_current_wal_lsn();
-- зафиксируем LSN, выполним массовую запись, посчитаем прирост журнала
SELECT pg_current_wal_lsn() AS before \gset
-- нагружаем таблицу бронирований из демобазы
UPDATE bookings.bookings
SET total_amount = total_amount + 0
WHERE book_date > '2017-07-01';
SELECT pg_size_pretty(
pg_wal_lsn_diff(pg_current_wal_lsn(), :'before')
) AS wal_written;
Принудительно запустить контрольную точку и посмотреть статистику:
Код: Выделить всё
CHECKPOINT; -- требует прав суперпользователя
-- статистика контрольных точек (в PG 15-16)
SELECT checkpoints_timed, checkpoints_req,
buffers_checkpoint, buffers_clean
FROM pg_stat_bgwriter;
Безопасное ускорение записи без потери долговечности - сжатие полных образов страниц:
Код: Выделить всё
-- сжимать full-page образы в журнале (с PG 15 поддержаны lz4 и zstd)
ALTER SYSTEM SET wal_compression = 'zstd';
SELECT pg_reload_conf();
- Выключают fsync ради скорости и забывают вернуть. При любом сбое питания база может стать нечитаемой без шанса на восстановление. fsync=off допустим только на одноразовой загрузочной инстанции, которую не жалко пересоздать.
- Путают synchronous_commit=off с потерей целостности. На самом деле база останется консистентной после краха, но несколько последних подтверждённых транзакций могут пропасть. Для денег это недопустимо, для логов и аналитики - часто приемлемо.
- Думают, что wal_level=minimal ускорит всё и везде. Он экономит журнал на массовых загрузках в одной транзакции (COPY в только что созданную таблицу), но полностью ломает физическую реплику и PITR - архивировать с него нечего.
- Ставят слишком большой max_wal_size, радуются редким checkpoint, а потом в восстановлении после сбоя сервер стартует минутами, потому что накопилось много неприменённых изменений.
- Отключают full_page_writes на обычном железе. Это приглашение к разорванным страницам при сбое питания. Отключать его можно лишь там, где атомарность 8 КБ гарантирует сама подсистема хранения (некоторые ФС с CoW, отдельные массивы).
- Кладут pg_wal на тот же раздел, что и данные, и при всплеске WAL диск переполняется - сервер останавливается целиком. WAL любит отдельный быстрый том.
- Для PostgreSQL под 1С забывают, что тяжёлые регламентные операции дают шквал журнала: без выделенного диска под WAL и адекватного max_wal_size checkpoint начинает штормить и проседает отклик.
- Подключитесь к своему серверу: psql -d demo. Выполните SHOW wal_level; и группу параметров через запрос к pg_settings из урока. Запишите текущие значения.
- Зафиксируйте LSN: SELECT pg_current_wal_lsn() \gset как before.
- Сгенерируйте нагрузку: сделайте UPDATE по таблице bookings.ticket_flights, меняющий какое-нибудь поле у тысяч строк.
- Посчитайте прирост журнала через pg_wal_lsn_diff и pg_size_pretty. Запомните число.
- Выполните CHECKPOINT; затем сразу повторите тот же UPDATE и снова замерьте прирост WAL. Сравните с шагом 4 и объясните разницу (подсказка: full_page_writes после контрольной точки).
- Включите wal_compression = 'zstd' через ALTER SYSTEM и pg_reload_conf(), повторите замер. Оцените, насколько уменьшился объём журнала.
- Посмотрите счётчики в pg_stat_bgwriter (или pg_stat_checkpointer на PG 17): сколько контрольных точек прошло по таймеру, а сколько по требованию.
- Почему запись в WAL быстрее, чем прямая запись изменений в табличные файлы, хотя данные в итоге всё равно окажутся на диске?
- Что именно происходит при краш-рекавери и с какой позиции журнала PostgreSQL начинает накат изменений?
- Какую проблему решает full_page_writes и почему первое изменение страницы после checkpoint дороже остальных?
- Чем грозит synchronous_commit=off и в каких сценариях это допустимый компромисс?
- Какие значения wal_level существуют и какой минимально нужен для физической реплики и для PITR?
- Как checkpoint_timeout и max_wal_size влияют на скорость восстановления после сбоя и на нагрузку в обычной работе?