WAL — журнал упреждающей записи
Предпосылки: страницы и кортежи.
← Физическая структура хранения | Буферный кеш →
Страницы сделали обновления управляемыми, но не дали гарантию «COMMIT переживёт падение». Durability в PostgreSQL строится на журнале, который можно проиграть при восстановлении.
Проблема: записывать на диск при каждом COMMIT слишком дорого
Страницы таблиц разбросаны по диску. Если пытаться «сохранить всё прямо на COMMIT», придётся подтверждать запись набора страниц в разные места файлов данных. Это множество независимых операций ввода‑вывода: случайный I/O (random I/O) (HDD vs SSD).
На HDD одна случайная запись страницы может занимать около 10 мс (см. оценку в страницах и кортежах). Транзакция, изменившая 10 разных страниц, превращает COMMIT в порядок 100 мс только на записи этих страниц.
Если транзакция изменила данные на нескольких страницах, наивный подход заставляет COMMIT ждать диск: пока все эти страницы не будут надёжно записаны. При параллельной нагрузке это быстро превращается в узкое место и делает время COMMIT непредсказуемым.
Альтернатива: держать изменения в памяти, писать на диск позже пачкой. Но при сбое всё, что в памяти, теряется. Нарушение Durability.
Решение: Write-Ahead Log (WAL)
WAL (журнал упреждающей записи) — последовательный лог всех изменений.
Почему «упреждающий» (write-ahead)? Запись в лог происходит до того, как изменения попадают в основные файлы данных. Сначала лог, потом данные.
Механизм:
- Транзакция меняет страницы в памяти
- Каждое изменение записывается в WAL-буфер
- При COMMIT: WAL-буфер сбрасывается на диск (
fsync— принудительно довести запись до диска) - Клиент получает «OK»
- Изменённые страницы остаются в памяти, записываются на диск позже
Dirty page (грязная страница) — страница в памяти, которая изменена, но ещё не записана на диск. Название буквальное: страница «испачкана» изменениями, которые ещё не сохранены.
Почему WAL быстрее:
WAL — один поток: запись в него почти всегда последовательная (append‑only). Последовательная запись устройствам хранения даётся намного дешевле, чем «скакать» по разным страницам в файлах таблиц.
Обычно WAL хранит описание изменения (дельту), а не полный образ страницы. Полный образ страницы тоже бывает нужен — например, как защита от torn page после контрольной точки (см. Full Page Writes в буферном кеше).
Checkpoint: когда записывать dirty pages на диск
Dirty pages копятся в памяти. Когда их записывать в основные файлы?
Checkpoint (контрольная точка) — момент, когда PostgreSQL записывает все dirty pages на диск и делает отметку: «все изменения до этой точки сохранены».
WAL timeline:
[records][records][CHECKPOINT][records][records][сейчас]
↑
все dirty pages записаныЗачем нужен checkpoint:
WAL растёт бесконечно. После checkpoint’а старые WAL-записи можно удалить — изменения уже в основных файлах, WAL для них не нужен.
Проблема checkpoint’а: Нужно записать все dirty pages. Если их много — всплеск I/O, база «подвисает».
Чтобы не создавать резких всплесков I/O, PostgreSQL старается распределять запись контрольной точки во времени: часть dirty pages записывается заранее, а не «одним залпом».
Trade-off checkpoint’ов:
Частые checkpoint’ы → меньше WAL нужно проигрывать при восстановлении, но больше записи в WAL из-за Full Page Writes. После каждого checkpoint’а первое изменение страницы записывает её полный образ (8 КБ), а не только дельту. Чем чаще checkpoint’ы — тем чаще страницы «сбрасывают» флаг FPW и записывают полные образы снова. Это write amplification: объём записи в WAL превышает объём фактических изменений данных.
Редкие checkpoint’ы → меньше write amplification, но дольше восстановление при сбое: нужно проигрывать больший хвост WAL.
Recovery: как WAL спасает данные при сбое
Сервер упал. В памяти были dirty pages, которые не успели записаться на диск. Данные потеряны?
Нет. При старте PostgreSQL:
- Находит последний checkpoint — точку, где все изменения были сохранены
- Читает WAL-записи от checkpoint’а до конца
- «Проигрывает» изменения заново (redo)
- Данные восстановлены
Это работает, потому что WAL — упорядоченный поток записей, и у каждой записи есть позиция LSN (Log Sequence Number). Recovery применяет WAL‑записи по LSN и не «переигрывает» то, что уже находится на диске. Каждая страница хранит pd_lsn — LSN последнего изменения страницы. При redo PostgreSQL сравнивает LSN записи с pd_lsn страницы: если страница уже содержит это изменение (или более новое) — запись пропускается. Поэтому redo можно безопасно повторять: запись либо применится один раз, либо будет проигнорирована.
WAL гарантирует, что COMMIT переживёт сбой. Но данные при этом остаются в памяти как dirty pages — буферный кеш управляет этой памятью и координирует запись на диск.
Sources
- PostgreSQL Documentation (пример: v16): Write-Ahead Logging (WAL) и Checkpoints. https://www.postgresql.org/docs/16/wal.html, https://www.postgresql.org/docs/16/runtime-config-wal.html