PostgreSQL
WAL: Write-Ahead Log и durability
Каждый раз когда PostgreSQL говорит 'COMMIT' - данные уже на диске. Не в памяти, не 'скоро запишутся' - именно на диске. Это гарантия durability, буква D в ACID. Механизм который это обеспечивает - WAL. Без понимания WAL невозможно правильно настроить производительность, репликацию или понять почему база восстановилась после краша быстро.
- **Stripe (2022):** при переходе с HDD на NVMe SSD главным выигрышем стало снижение WAL fsync latency с 3-5ms до 0.15ms. Throughput вырос на 40% без изменения кода приложения - просто WAL стал быстрее.
- **Heroku PostgreSQL:** обещает crash recovery < 10 минут на инстансах до 500GB за счёт оптимизированного WAL replay на NVMe. Это SLA-гарантия, которую делает возможной архитектура WAL.
- **Citus (распределённый PostgreSQL):** при добавлении шардов использует WAL-репликацию для синхронизации. wal_level = logical необходим для logical replication между нодами - настройка WAL напрямую влияет на возможности горизонтального масштабирования.
Что такое WAL и зачем он нужен
Write-Ahead Log (WAL) - фундаментальный механизм durability в PostgreSQL. Правило одно: перед изменением любых данных на диске - сначала запись в WAL. Если сервер упадёт в любой момент, WAL содержит достаточно информации для полного восстановления.
WAL позволяет PostgreSQL откладывать запись грязных страниц из shared_buffers на диск - достаточно что WAL уже на диске. Это кардинально ускоряет COMMIT: последовательная запись в WAL-файл быстрее случайной записи в heap-файлы таблиц.
Stripe перешёл на NVMe SSD именно ради WAL: на SATA SSD fsync WAL занимал 2-5ms, на NVMe - 0.1-0.3ms. При 5000 TPS это разница между 99th percentile в 8ms и в 50ms для транзакционных запросов.
Почему COMMIT возвращает успех клиенту только после записи WAL на диск, но не ждёт записи страниц таблицы?
WAL segments и LSN
WAL хранится как последовательность сегментов по 16MB (по умолчанию) в каталоге $PGDATA/pg_wal/. Каждая запись в WAL имеет адрес - LSN (Log Sequence Number), 64-битное монотонно возрастающее значение.
PostgreSQL pre-allocates несколько WAL-сегментов вперёд (wal_keep_size или min_wal_size). Количество WAL-файлов регулируется параметром max_wal_size (default 1GB). При превышении начинается checkpoint или старые файлы переиспользуются.
Что такое LSN (Log Sequence Number)?
Checkpoints: синхронизация WAL и heap
Checkpoint - момент, когда PostgreSQL гарантирует что все грязные страницы из shared_buffers записаны на диск. После checkpoint при crash recovery достаточно воспроизвести только WAL после этой точки - не весь WAL с начала времён.
checkpoint_completion_target = 0.9 означает: растянуть запись грязных страниц на 90% времени между checkpoint'ами. Это сглаживает I/O нагрузку. Если checkpoints_req >> checkpoints_timed в pg_stat_bgwriter - max_wal_size слишком мал и checkpoint'ы происходят слишком часто из-за переполнения WAL.
Что означает высокое значение checkpoints_req в pg_stat_bgwriter?
Full Page Writes: защита от torn pages
Операционная система и диск используют 512B или 4KB блоки, а PostgreSQL - 8KB страницы. При сбое питания запись 8KB страницы может прерваться на середине - половина страницы будет старой, половина новой. Это называется torn page.
CloudNativePG (оператор Kubernetes для PostgreSQL) всегда оставляет full_page_writes = on даже на enterprise SSD с battery backup. Причина: защита нужна не только от power failure, но и от багов в драйверах и firmware диска.
Зачем PostgreSQL записывает full page image в WAL только при первом изменении страницы после checkpoint, а не при каждом?
Crash Recovery: от LSN к живой базе
При старте после краша PostgreSQL автоматически запускает recovery. Находит последний checkpoint, читает WAL с этой точки и replay каждую запись. Транзакции без COMMIT откатываются. Процесс полностью автоматический.
Heroku PostgreSQL гарантирует RTO (Recovery Time Objective) < 10 минут на инстансах до 500GB. Это возможно именно потому что crash recovery - replay WAL - работает с максимальной скоростью последовательного чтения диска, что на NVMe даёт 3-5 GB/s.
synchronous_commit = off опасен для данных и его нельзя использовать
synchronous_commit = off жертвует durability последних ~200ms транзакций (wal_writer_delay) в обмен на снижение latency. Это приемлемо для некритичных данных: счётчики просмотров, аналитические события, логи. Транзакция не будет потеряна частично - потеряется целиком или не потеряется.
При synchronous_commit = off COMMIT возвращает успех до fsync WAL. Если сервер упадёт в этот момент, последние транзакции (за несколько сотен миллисекунд) будут потеряны. Но они потеряются полностью - WAL не содержит их совсем. Это отличается от data corruption - данные согласованны, просто часть операций откатится при recovery.
Откуда PostgreSQL узнаёт, с какого LSN начинать replay WAL при crash recovery?
Итоги
- **WAL = durability:** запись в WAL предшествует любому изменению данных на диске. COMMIT возвращает успех только после fsync WAL - это гарантия D в ACID
- **Checkpoint - граница recovery:** при crash восстанавливается только WAL после последнего checkpoint. Частые checkpoint'ы ускоряют recovery но увеличивают I/O
- **full_page_writes защищает от torn pages:** первое изменение страницы после checkpoint записывает полную 8KB копию в WAL, чтобы дельты можно было применить даже после частичной записи страницы
Связанные темы
WAL - основа многих механизмов PostgreSQL:
- Репликация (streaming replication) — Standby серверы получают те же WAL-записи и replay их - репликация работает поверх WAL-механизма
- Shared Buffers и Buffer Cache — WAL позволяет откладывать запись грязных страниц из buffer cache - понимание их взаимодействия критично для тюнинга
- Background Workers и процессы — walwriter процесс отвечает за периодический flush WAL буфера; checkpointer запускает checkpoint и взаимодействует с bgwriter
Вопросы для размышления
- Если увеличить max_wal_size с 1GB до 8GB - как это повлияет на частоту checkpoint'ов, время crash recovery и I/O нагрузку?
- Почему logical replication требует wal_level = logical, а streaming replication работает с wal_level = replica? Что дополнительно пишется в WAL при logical?
- Как рассчитать оптимальный checkpoint_timeout для системы где RTO (Recovery Time Objective) = 5 минут, а WAL генерируется со скоростью 100MB/s?