Learning Platform
Глоссарий Troubleshooting
Урок 07.05 · 23 мин
Средний
storage-formatwalcheckpointdurability

WAL и checkpointing

До сих пор мы разбирали файл DuckDB как статичную структуру: заголовок, блоки, row groups, сегменты. Но база меняется — в неё пишут. И вот тут возникает фундаментальная проблема надёжности: что, если процесс упадёт прямо посреди записи? Питание отключилось, ОС убила процесс, программа крашнулась. На диске останется наполовину записанное состояние. Этот урок про механизм, который превращает «наполовину записанное» в «либо всё, либо ничего» — про write-ahead log и checkpointing. И про то, как DuckDB 1.5 убрал паузу, которую checkpoint раньше вносил в работу базы.

Проблема: запись не атомарна

Когда вы делаете INSERT или UPDATE, изменения в идеале должны попасть в блоки файла. Но обновление блоков — операция не мгновенная и не атомарная. Транзакция может затронуть много блоков: данные в одних, метаданные каталога в других, free list в третьих. Записать их на диск — это серия операций ввода-вывода. Между первой и последней из них процесс может умереть.

Если он умрёт в этот момент, файл окажется в противоречивом состоянии: часть блоков транзакции обновлена, часть — нет. Указатель в каталоге уже ведёт на новые данные, а самих данных ещё нет. Или строки записаны, а статистика старая. Такой файл нельзя корректно прочитать — он не отражает ни состояние «до транзакции», ни «после».

Транзакция обязана быть атомарной — это буква A в ACID: либо она применилась целиком, либо не применилась вовсе, промежуточного состояния на диске быть не должно.

ACID на пальцах: что именно обещает СУБД

Delta Lake: архитектура Transaction Log Просто «писать аккуратно» эту гарантию не даёт: момент сбоя выбираете не вы. Нужен отдельный механизм.

WAL: сначала запиши намерение

Решение — write-ahead log, WAL. По-русски — журнал упреждающей записи. Принцип в названии: ahead, «вперёд», означает, что запись в журнал происходит раньше, чем изменение основного файла.

Работает так. Когда транзакция фиксируется (COMMIT), DuckDB не бросается сразу перекраивать блоки основного файла. Вместо этого он дописывает в конец WAL-файла компактную запись: «вот что сделала эта транзакция» — вставленные строки, удаления, изменения схемы. WAL — это отдельный файл рядом с базой, обычно с расширением .wal. Запись в него — это дозапись в конец, append. Append — самая дешёвая и самая надёжная форма записи на диск: ничего не перемещается, не перезаписывается, просто файл подрастает с конца.

COMMIT считается завершённым, как только запись о транзакции надёжно легла в WAL. Основной файл базы в этот момент ещё не тронут — он отражает старое состояние. Но это уже безопасно, и вот почему. WAL пишется последовательно и его запись либо успела целиком (запись транзакции в журнале есть), либо нет (записи нет). Состояния «полузаписанная транзакция в журнале» система не считает зафиксированным.

Путь транзакции: сначала WAL, потом основной файл
COMMITПользователь фиксирует транзакцию: INSERT, UPDATE, изменение схемы.
append
Запись в WALКомпактная запись об изменениях дописывается в конец .wal файла. Дозапись в конец — самая дешёвая и надёжная операция записи.
COMMIT завершён
Основной файл — позжеБлоки основного .duckdb файла обновятся отдельно, при checkpoint. На момент COMMIT основной файл ещё содержит старое состояние.

Восстановление после сбоя через WAL

Теперь сценарий аварии. База открыта, прошло несколько COMMIT, их записи легли в WAL, основной файл пока обновить не успели — и тут процесс умирает.

При следующем открытии файла DuckDB первым делом проверяет WAL. Если в журнале есть записи о транзакциях, которых нет в основном файле, движок их проигрывает заново — последовательно применяет к основному файлу всё, что записано в WAL. Этот процесс называется recovery, восстановление. После проигрывания WAL основной файл доведён до состояния «все зафиксированные транзакции применены», и база открывается консистентной.

Так WAL даёт durability — букву D в ACID, долговечность: если COMMIT вернул успех, изменения переживут сбой, потому что они уже в журнале, и recovery их докатит. И атомарность: транзакция, чья запись не успела целиком лечь в WAL, при recovery просто игнорируется — будто её и не было. Полузаписанного состояния не возникает: либо запись в журнале целая и транзакция докатывается, либо записи нет и транзакции нет.

Ключевая мысль: durability обеспечивает не основной файл, а WAL. Основной файл может временно отставать от реального состояния базы — недостающее всегда лежит в журнале.

Checkpoint: слияние WAL в основной файл

Если бы WAL только рос, он раздулся бы до огромных размеров, а каждое открытие базы означало бы проигрывание всей истории изменений с начала времён. Нужен механизм, который периодически переносит накопленное из WAL в основной файл и обнуляет журнал. Этот механизм — checkpoint.

Checkpoint делает три вещи. Он применяет все накопленные в WAL изменения к блокам основного файла — данные ложатся в колоночные сегменты, обновляются метаданные каталога. Он переключает один из двух database header (помните, в первом уроке их было два?) на новое, актуальное состояние — атомарным переключением: пока пишется новое состояние, старый header остаётся валидным, и только в самом конце происходит переход. И он очищает WAL — после успешного checkpoint журнал больше не нужен, потому что всё, что в нём было, теперь в основном файле.

После checkpoint основной файл самодостаточен: он содержит полное актуальное состояние базы, WAL пуст, открытие не требует recovery.

Checkpoint: из WAL в основной файл
WAL накопил транзакцииЖурнал содержит записи нескольких зафиксированных транзакций, ещё не применённых к блокам основного файла.
CHECKPOINT
Изменения слиты в блокиВсе накопленные изменения применены к колоночным сегментам и метаданным основного файла. Один из двух database header атомарно переключён на новое состояние.
журнал больше не нужен
WAL очищенПосле успешного checkpoint WAL пуст. Открытие базы теперь не требует recovery — основной файл самодостаточен.

Checkpoint случается не только по явной команде CHECKPOINT. DuckDB запускает его автоматически, когда WAL вырастает сверх порога (checkpoint_threshold, по умолчанию около 16 MiB), и при штатном закрытии базы. Явная команда CHECKPOINT нужна, когда вы хотите гарантированно слить WAL прямо сейчас — например, перед копированием файла базы или перед измерением «честного» размера на диске.

-- Порог автоматического checkpoint
SELECT current_setting('checkpoint_threshold');
-- 16.0 MiB

-- Принудительно слить WAL в основной файл прямо сейчас
CHECKPOINT;
NOTE

Размер WAL виден в выводе PRAGMA database_size — поле wal_size. Сразу после CHECKPOINT или после штатного закрытия и открытия базы оно равно нулю: журнал пуст. Если же база сейчас активно принимает записи, wal_size будет ненулевым — это нормально и означает лишь, что часть зафиксированных изменений пока живёт в журнале и ещё не слита в основной файл.

Non-blocking checkpoint в 1.5: checkpoint без паузы

До версии 1.5 у checkpoint был неприятный побочный эффект — он блокировал. Пока шёл checkpoint, конкурентная запись в базу была невозможна: транзакции, желающие что-то изменить, ждали завершения checkpoint. На большой базе с активной записью это означало заметные паузы: чем больше накопилось в WAL, тем дольше checkpoint, тем длиннее окно, в котором запись стоит.

DuckDB 1.5 ввёл non-blocking checkpointing — неблокирующий checkpoint. Теперь checkpoint выполняется конкурентно с обычной работой базы: и чтения, и записи продолжаются, пока checkpoint сливает WAL в основной файл. Паузы на запись больше нет.

Идея, лежащая в основе, опирается на MVCC и snapshot-изоляцию (детально — в модуле про транзакции). Грубо: checkpoint работает с консистентным снимком состояния на момент своего старта, а транзакции, пришедшие во время checkpoint, пишут поверх, не мешая ему. Checkpoint не пытается «заморозить» базу — он переносит зафиксированный снимок, а новые изменения накапливаются параллельно и попадут в следующий checkpoint.

Эффект измерим. По замерам DuckDB на бенчмарке TPC-H масштаба SF100 неблокирующий checkpoint дал прирост пропускной способности порядка 17% на нагрузке со смешанными чтениями и записями — ровно за счёт того, что записи больше не простаивают в окне checkpoint. Для практики это означает: на DuckDB 1.5 базу можно держать под постоянной пишущей нагрузкой, и фоновый checkpoint не будет периодически её подвешивать.

АспектДо 1.5 (blocking)С 1.5 (non-blocking)
Запись во время checkpointБлокируется, транзакции ждутПродолжается параллельно
Чтение во время checkpointРазрешеноРазрешено
Пауза на больших базахЗаметная, растёт с размером WALОтсутствует
Эффект на throughputПросадка в окне checkpointДо +17% на TPC-H SF100 (смешанная нагрузка)

Попробуй сам

Понаблюдайте за WAL и checkpoint руками.

  1. Создайте базу: duckdb wal.duckdb. Сразу посмотрите PRAGMA database_size; — поле wal_size должно быть нулевым.
  2. В той же сессии вставьте данные несколькими отдельными транзакциями: например, трижды выполните INSERT INTO ... после CREATE TABLE log AS SELECT range AS n FROM range(100000);. Не закрывая сессию, снова посмотрите wal_size. Изменился ли он?
  3. В отдельном терминале (пока первая сессия открыта) посмотрите файлы на диске: ls -la wal.duckdb*. Виден ли рядом файл wal.duckdb.wal? Каков его размер?
  4. Вернитесь в сессию DuckDB, выполните CHECKPOINT;, снова посмотрите wal_size и снова ls -la wal.duckdb* в другом терминале. Что стало с .wal-файлом?
  5. Смоделируйте «сбой»: вставьте данные новой транзакцией, НЕ делайте CHECKPOINT, и завершите процесс DuckDB жёстко (закрытие через .quit штатное — оно делает checkpoint; для эксперимента можно убить процесс из другого терминала). Затем снова откройте wal.duckdb и проверьте SELECT count(*) FROM log; — данные на месте? Откуда они восстановились?
  6. Посмотрите текущее значение checkpoint_threshold через SELECT current_setting('checkpoint_threshold'); и прикиньте, сколько примерно нужно вставить, чтобы автоматический checkpoint сработал сам.

Этот эксперимент показывает живьём: WAL растёт при записи, обнуляется при checkpoint, и именно он спасает данные, если процесс умер до checkpoint.


Проверка знанийKnowledge check
Зачем DuckDB нужен WAL, если есть основной файл базы, и что именно изменил non-blocking checkpoint в версии 1.5?
ОтветAnswer
WAL (write-ahead log) нужен потому, что обновление блоков основного файла не атомарно: транзакция трогает много блоков (данные, метаданные, free list), и процесс может умереть посреди этой серии операций, оставив файл в противоречивом полузаписанном состоянии. WAL решает это принципом упреждающей записи: при COMMIT движок не перекраивает основной файл, а лишь дописывает в конец журнала компактную запись об изменениях транзакции — дозапись в конец дёшева и надёжна. COMMIT считается завершённым, как только запись легла в WAL. Если процесс падает, при следующем открытии DuckDB проигрывает WAL заново (recovery), докатывая до основного файла все зафиксированные транзакции; транзакция, чья запись не успела лечь в журнал целиком, просто игнорируется. Так WAL даёт durability и атомарность — гарантии обеспечивает именно журнал, а не основной файл, который может временно отставать. Checkpoint периодически сливает накопленное из WAL в блоки основного файла, атомарно переключает один из двух database header и очищает журнал. До версии 1.5 checkpoint блокировал запись: транзакции ждали его завершения, что на больших базах давало заметные паузы. Non-blocking checkpointing в 1.5 выполняет checkpoint конкурентно с чтениями и записями — пауза на запись исчезла, и на бенчмарке TPC-H SF100 со смешанной нагрузкой это дало прирост пропускной способности порядка 17%.

Проверьте понимание

Результат: 0 из 0
Концептуальный
Вопрос 1 из 4. Что происходит при COMMIT транзакции в DuckDB?

Закончили урок?

Отметьте его как пройденный, чтобы отслеживать свой прогресс

Войдите чтобы оценить урок

Прогресс модуля
0 из 7