WAL (Write-Ahead Log) — universal durability mechanism. Append-only sequential file, fsync semantics, group commit, checkpoint, crash recovery. Used in Postgres, MySQL InnoDB, RocksDB, Kafka log, etcd, Spanner, ext4 journal. Concept page covering write path (modify in-memory → WAL append → fsync → ack → flush page later), crash recovery (replay WAL from last checkpoint), group commit batching, and synchronous_commit=off trade-off.
WAL (Write-Ahead Log) — фундамент durability у всех серьёзных СУБД, очередей и распределённых движков. Идея простая до банальности: прежде чем менять данные на диске, запиши намерение в append-only журнал и сделай fsync — только потом отвечай клиенту OK. Из этой одной инвариантности вырастает почти всё, что мы считаем "взрослой" базой данных:
Без WAL нет ничего из этого. С WAL — есть Postgres, MySQL InnoDB, RocksDB, Kafka log, etcd, Spanner, ext4 journal, NTFS USN journal. Одна и та же идея.
"Сначала пиши то, что собираешься сделать, в журнал. fsync. Только потом делай. При crash — догоняй журнал с последней безопасной точки."
Три ключевых инварианта:
LSN=X должен быть на диске (fsync) раньше, чем dirty data page с pageLSN=X уйдёт на диск. Это правило (Write-Ahead Logging rule из ARIES) — основа корректности recovery.Канвас показывает анатомию одного DBMS-процесса (Postgres-style) и его взаимодействие с диском:
wal_buffers=16MB в Postgres), куда падают новые WAL-записи перед flush.checkpoint_timeout, по умолчанию 5 мин) проходит по dirty pages в shared buffers и flushит их в data files, затем обновляет redo LSN.Edges показывают физические потоки I/O: клиент -> backend (SQL), backend -> shared_buf (modify), backend -> wal_buf (append), wal_writer -> wal_file (write + fsync), checkpointer -> data_file (flush pages). Обратный edge wal-file -> backend нужен для recovery-сценария (replay при старте).
Write path: классический WAL-протокол. Клиент шлёт UPDATE, backend модифицирует страницу в shared buffers (но НЕ на диске!), параллельно пишет WAL-запись {LSN, xid, page, before, after} в WAL buffer. На COMMIT добавляется commit-record, WAL writer делает write() + fsync() — это блокирующий вызов на ~100µs (NVMe с PLP) до ~5ms (consumer SSD). Только после успешного fsync backend возвращает клиенту ACK. Data page всё ещё dirty в RAM — на диск она попадёт позже, при следующем checkpoint. Это контр-интуитивно: WAL-запись на диске, а изменённая страница — нет. Но именно так и нужно: при crash мы реплеем WAL и восстановим страницу.
Crash + recovery. Power loss посреди работы — dirty pages в RAM исчезли, data files не обновлены. На рестарте postmaster читает pg_control, видит флаг "in production" => нужна recovery. Analysis phase: находит redo LSN последнего checkpoint. Redo phase: стримит WAL от redo LSN до конца, для каждой записи загружает page из data file и применяет изменение, если page.LSN < record.LSN (idempotent replay). Undo phase: в Postgres no-op (aborted txns остаются в heap, MVCC + vacuum уберут позже); в системах с in-place update (MySQL InnoDB) — настоящий undo через undo log. После recovery commit, который клиент видел до crash, на месте — durability honored.
Group commit: 1 fsync на 100 коммитов. Три бэкенда коммитят почти одновременно. Каждый пишет commit-record в WAL buffer и засыпает на fsync barrier. WAL writer ждёт commit_delay (100µs) или собирает commit_siblings (3+ ждущих) и делает один write() + fsync() для всех трёх. Один syscall = ~100µs на NVMe, амортизированная стоимость fsync — ~33µs/tx вместо 100µs/tx. На write-heavy workload (1000 параллельных коммитов в группе) можно выжать 100k tx/sec на одном NVMe без потери durability. Тот же приём в MySQL (binlog group commit + 2PC prepare/commit), в RocksDB (WriteBatch + JoinBatchGroup), в Kafka producer (linger.ms).
synchronous_commit=off: 5× throughput, окно потери до 200ms. WAL пишется в WAL buffer, ack возвращается клиенту немедленно без ожидания fsync. WAL writer раз в wal_writer_delay (200ms) batch-флашит накопленное. Latency коммита падает с ~500µs до ~10µs, throughput — в 2-5×. Цена: power loss до следующего background flush => последние ~200ms коммитов потеряны, хотя клиенты видели ACK. Это сценарий нарушения durability — он допустим только когда данные восстановимы (analytics, audit, staging), и категорически не подходит для payments / orders / inventory. Альтернатива противоположного полюса — synchronous_commit=remote_apply: ждём fsync на standby тоже (+1ms RTT), переживёт даже одновременный crash master + disk failure.
ADR-001 на ноде backend разбирает главный практический выбор: synchronous_commit=on (group commit) vs synchronous_commit=off. Открой её на канвасе — там полный контекст про fsync-стоимость (NVMe PLP ~50µs, consumer SSD без PLP 1-5ms, HDD 5-10ms), про group commit как способ амортизировать fsync до 100k tx/sec без потери durability, и про когда осознанно нарушать ACID-D.
Дополнительные trade-offs, которые не оформлены отдельным ADR, но критичны:
full_page_writes=on/off — защита от torn page (page разорван между блоками 4KB на NVMe). С on — первая модификация страницы после checkpoint логирует всю страницу (8KB) в WAL вместо delta. WAL пухнет 2-5×, но переживает torn page. Off безопасно только если у тебя ZFS/btrfs (CoW), либо ты уверен в atomicity 8KB write на железе. Дефолт — on, не трогай без понимания.wal_compression — pglz/zstd компрессия full-page images. Снижает WAL volume на 50-70%, стоит 5-10% CPU. На write-heavy включай.max_wal_size / checkpoint_timeout — чем реже checkpoint, тем меньше I/O spikes, но дольше recovery после crash. Дефолт 5 мин — компромисс; на high-write OLTP можно 15-30 мин (терпишь долгий recovery ради smooth I/O).pg_wal/ directory, 16MB сегменты, formats описаны в xlogrecord.h. Logical decoding (pgoutput, wal2json) — основа Debezium / Supabase Realtime / встроенной logical replication.innodb_flush_log_at_trx_commit=1 ≈ Postgres synchronous_commit=on. 2PC между InnoDB и binlog — источник сложности и багов (см. известные кейсы потери транзакций при некорректном sync_binlog).*.log файлы) + memtable + SSTables. На startup WAL реплеется в memtable. Cassandra использует RocksDB-style commitlog.flush.messages / flush.ms. Consumers читают WAL "наоборот" — это и есть весь продукт.fsync() (или O_DSYNC) page cache потеряется при power loss.pg_test_fsync или diskchecker.pl от Postgres community. На bare-metal с критичными данными — только enterprise SSD с PLP или RAID-контроллер с battery.pg_wal на отдельном tablespace) — классическое tuning-решение для high-write OLTP.archive_command failures — если archive ломается, WAL накапливается, диск переполняется, БД встаёт. Алерт на archive lag — must-have.checkpoint_timeout=30s) — каждый checkpoint — большая I/O волна. На write-heavy получишь постоянные стоны latency. Лучше реже + tune checkpoint_completion_target=0.9 (размазать flush во времени).fsync=off "для скорости" в production — это synchronous_commit=off × 1000. Полный отказ от durability. Используется только в эпhemeral test setups / build serverах.WAL — это всегда правильно для stateful durable хранилищ. Вопрос скорее "когда можно без него" или "когда WAL не нужен в твоём слое":
И обратное: если у тебя есть mutable state, который пользователь считает сохранённым после ACK — у тебя должен быть WAL. Либо ты его написал, либо используешь систему, которая написала за тебя. Третьего не дано.
src/backend/access/transam/xlog.c, xlogrecord.h — если хочется увидеть, как это реально написано.Связанные концепты:
Применяется в кейсах: см. любой кейс с Postgres / MySQL / Kafka — WAL под капотом везде.