linuxlab.io
Учебники▾
  • Линукс и сети
    Файловая система, процессы, TCP/IP, BGP и OSPF
    →
  • Terraform и IaC
    HCL, state, plan/apply на sandbox LocalStack
    →
  • Git и GitHub
    Объектная модель, plumbing, ветвление, GitHub Actions
    →
Все учебники →
ЦеныО платформеВойтиСоздать аккаунт
/
Intro
Lessons
Footer
linuxlab-УчебникиЦеныО платформеКонфиденциальность и куки
Copyright © 2026 LinuxLab. Все права защищены.
linuxlab.io
Учебники▾
  • Линукс и сети
    Файловая система, процессы, TCP/IP, BGP и OSPF
    →
  • Terraform и IaC
    HCL, state, plan/apply на sandbox LocalStack
    →
  • Git и GitHub
    Объектная модель, plumbing, ветвление, GitHub Actions
    →
Все учебники →
ЦеныО платформеВойтиСоздать аккаунт
/
  • Введение
  • Главы
  • How it worksскоро
  • Уроки
  • База знаний
  • Собеседование
Часть IV — Буферный кеш и журнал

$ глава 18 · 55 минут

Журнал предзаписи (WAL): принцип

В прошлой главе мы оставили открытым неуютный вопрос: изменённая страница живёт в памяти, на диск попадает позже - что будет, если сервер упадёт в этот зазор? Ответ - журнал предзаписи, WAL (Write-Ahead Log): вся надёжность держится на одном простом правиле.

Правило звучит так: прежде чем изменить страницу данных, запиши намерение в журнал и убедись, что журнал на диске. Журнал - это последовательный лог: «в такой-то странице, по такому-то смещению, изменилось вот это». Записывать его дёшево, потому что это всегда дозапись в конец одного потока, а не разбросанные по диску правки.

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

18.1 Правило write-ahead

Идея WAL - в порядке записи. Любое изменение данных делается в два приёма:

  1. сначала в журнал дописывается запись об изменении (что, где, как);
  2. только потом (и не обязательно сразу) меняется сама страница в кеше, а на диск файла данных она попадёт ещё позже.

Ключ - в слове «сначала». Журнальная запись об изменении гарантированно оказывается на диске раньше, чем изменённая страница данных. Отсюда название - запись с опережением, write-ahead.

Почему это спасает. Если сбой случится после записи в журнал, но до записи страницы - не беда: при старте PostgreSQL прочитает журнал и применит изменение к данным заново. Если сбой случится до записи в журнал - значит, транзакция ещё не была подтверждена, и терять нечего. В любом исходе данные согласованы.

18.2 Почему журнал дешевле, чем запись данных

Может показаться, что мы пишем всё дважды - и в журнал, и в данные. Но журнал обходится гораздо дешевле по одной причине: он пишется последовательно.

Изменения в большой базе разбросаны по диску: обновили строку здесь, вставили там, тронули индекс в третьем месте. Записывать эти страницы сразу - значит гонять головку диска (или нагружать SSD случайными записями) по всему тому. А журнал - это один поток, который всегда пишется в конец. Последовательная запись на порядки быстрее случайной.

Поэтому WAL позволяет подтвердить транзакцию быстрой последовательной дозаписью в журнал, а медленную случайную запись страниц данных отложить и размазать во времени (этим занимаются bgwriter и контрольная точка). Журнал превращает множество мелких случайных записей в одну последовательную.

18.3 LSN: адрес в потоке журнала

Журнал - это непрерывный поток байт. Позицию в нём задаёт LSN (Log Sequence Number) - просто смещение в байтах от начала потока. Это монотонно растущее число; у него даже свой тип - pg_lsn, печатается как 16/B374D848.

LSN - это «время» в мире WAL. У каждой журнальной записи свой LSN, и каждая страница данных помнит LSN последнего изменения, которое в ней отражено (в заголовке страницы - pd_lsn). Это даёт точное правило согласованности: страницу нельзя записать на диск, пока журнал не сброшен хотя бы до её pd_lsn. Иначе на диске оказалась бы страница, изменение которой ещё не зафиксировано в журнале - нарушение write-ahead.

sql
SELECT pg_current_wal_lsn();              -- текущая позиция записи в WAL
SELECT pg_current_wal_lsn() - '0/0';      -- сколько байт WAL уже сгенерировано

Разница двух LSN - это объём журнала между ними в байтах. По ней измеряют темп генерации WAL и лаг репликации. Подробнее - в wal.

18.4 Durability: что значит COMMIT

Теперь понятно, что физически означает успешный COMMIT. В момент подтверждения транзакции PostgreSQL дописывает в журнал запись о коммите и дожидается, пока журнал будет сброшен на диск (fsync) до этой записи включительно. Только после этого клиент получает «ОК».

Это и есть буква D в ACID - durability. После ответа «закоммичено» данные переживут сбой, даже если страницы в файлах данных ещё не записаны: журнал на диске, а из него всё восстановимо.

У этого ожидания есть рычаг - synchronous_commit:

  • on (по умолчанию) - COMMIT ждёт сброса журнала на диск. Полная надёжность;
  • off - COMMIT возвращается, не дожидаясь fsync. Быстрее, но при сбое можно потерять несколько последних подтверждённых транзакций (данные при этом остаются согласованными - теряется только хвост).

Это честный компромисс «скорость против гарантии», и включать off можно осознанно для данных, которые не жалко потерять в последние доли секунды.

18.5 CRC: журнал замечает порчу

Журнал - последняя линия обороны данных, поэтому он защищён от тихой порчи. Каждая WAL-запись несёт контрольную сумму (CRC) своего содержимого. При восстановлении PostgreSQL проверяет CRC каждой записи перед тем, как её применить.

Это даёт чёткую границу целого журнала. Если при чтении журнала встречается запись с битой CRC или оборванная на полузаписи (так бывает, если сбой случился прямо во время записи) - PostgreSQL считает, что журнал здесь и кончился. Всё до этой точки целостно и применяется; всё после - отбрасывается как недописанное. Так восстановление никогда не применит «полузапись».

18.6 Full-page images: защита от рваной записи

Есть коварная проблема на стыке с железом. Страница PostgreSQL - 8 КБ, а диск (или ОС) пишет блоками поменьше, например по 4 КБ. Если сбой случится прямо во время записи 8-КБ страницы, на диск может попасть её половина: первые 4 КБ новые, вторые - старые. Это «рваная запись» (torn page), и обычная WAL-запись («измени байт X на Y») такую страницу не починит - она опирается на то, что страница целая.

Решение - full-page image (FPI). При первом изменении страницы после каждой контрольной точки PostgreSQL пишет в журнал не дельту, а всю страницу целиком. При восстановлении такая полная копия кладётся поверх возможно рваной страницы - и дальше уже применяются обычные мелкие записи.

За это отвечает параметр full_page_writes (по умолчанию on). Отсюда же - всплеск объёма WAL сразу после контрольной точки: первые изменения каждой страницы тащат за собой по полной копии. Подробнее - в full-page-images.

Подводный камень: отключать full_page_writes почти всегда нельзя. Соблазн есть - FPI заметно раздувают журнал. Но без них рваная запись при сбое означает повреждённую страницу без шансов на восстановление. Отключают их только на хранилищах, которые сами гарантируют атомарную запись страницы целиком (некоторые специализированные ФС и устройства), и только зная это точно.

18.7 Где журнал живёт и кто его пишет

Физически журнал - это набор файлов-сегментов в каталоге pg_wal/, по умолчанию по 16 МБ каждый. Когда сегмент заполняется, начинается следующий. Старые сегменты, уже не нужные для восстановления и репликации, переиспользуются или удаляются при контрольной точке.

Записывают журнал:

  • бэкенды - в буфер WAL в разделяемой памяти (wal_buffers);
  • walwriter - фоновый процесс, который регулярно сбрасывает буфер WAL на диск, чтобы бэкендам не приходилось делать это самим;
  • на COMMIT при synchronous_commit = on сброс инициирует сам коммитящий бэкенд.

Темп генерации журнала видно в pg_stat_wal:

sql
SELECT wal_records, wal_bytes, wal_fpi FROM pg_stat_wal;

wal_fpi - число full-page images; всплеск после контрольной точки виден именно здесь.

18.8 Что делает возможным журнал

WAL придумали ради восстановления после сбоя, но из одного потока изменений естественно вырастают и другие возможности:

  • репликация - тот же поток WAL можно передавать на другой сервер и проигрывать там, поддерживая копию базы (часть VIII);
  • восстановление на точку (PITR) - храня архив сегментов WAL, можно восстановить базу на любой момент в прошлом;
  • логическое декодирование - из WAL можно извлекать изменения в виде логических событий (вставка/обновление строки), а не физических правок страниц.

Все они опираются на один и тот же журнал - меняется лишь то, сколько в него пишут. Этим управляет wal_level, и о нём - глава 20. А следующая глава - о том, как именно журнал проигрывается при восстановлении и зачем нужны контрольные точки.

Уроки в sandbox

lab-18.1. WAL и контрольная точка

Посмотрим на журнал в работе. Замерим текущий LSN, нагенерим изменений, увидим, как вырос объём WAL и счётчики pg_stat_wal, затем вызовем CHECKPOINT и заметим всплеск full-page images. Перед шагами предскажи, вырастет ли счётчик.

  1. Замерь текущую позицию журнала: SELECT pg_current_wal_lsn();.

  2. Сними счётчики pg_stat_wal (wal_records, wal_bytes, wal_fpi).

  3. Нагенерируй изменения серией INSERT/UPDATE и снова замерь LSN - предскажи, на сколько байт он сдвинется.

  4. Сравни pg_stat_wal до и после - какие счётчики выросли.

  5. Вызови CHECKPOINT;, затем сделай UPDATE и посмотри wal_fpi - первые изменения страниц после контрольной точки тащат full-page images.

sandbox с автопроверкой - открыть в песочнице

Резюме

  • Правило write-ahead: запись об изменении попадает в журнал на диск раньше, чем изменённая страница данных - поэтому сбой не теряет подтверждённые данные.
  • Журнал дёшев, потому что пишется последовательно в конец одного потока, превращая множество случайных записей страниц в одну последовательную.
  • LSN - смещение в байтах в потоке WAL (`pg_lsn`); страница нельзя записать на диск, пока журнал не сброшен до её `pd_lsn`.
  • COMMIT при `synchronous_commit = on` ждёт сброса журнала на диск (fsync) - это и есть durability; `off` ускоряет ценой потери хвоста транзакций при сбое.
  • Каждая WAL-запись защищена CRC; битая или оборванная запись считается концом журнала - восстановление не применяет полузаписи.
  • Full-page image - полная копия страницы в журнал при первом изменении после контрольной точки; защищает от рваной записи (torn page).
  • Журнал живёт в сегментах `pg_wal/` (16 МБ); пишут его бэкенды и walwriter; темп виден в `pg_stat_wal` (`wal_records`, `wal_bytes`, `wal_fpi`).

Контрольные вопросы

  1. Сформулируйте правило write-ahead и объясните, почему оно спасает данные при сбое.

    Показать ответ

    Правило: прежде чем изменённая страница данных попадёт на диск, запись об этом изменении должна быть на диске в журнале. То есть журнал опережает данные. Если сбой случится после журнальной записи, но до записи страницы - изменение восстановят, доиграв журнал при старте. Если сбой случится до журнальной записи - транзакция не была подтверждена, и терять нечего. В обоих случаях данные остаются согласованными.

  2. Почему писать в журнал дешевле, чем сразу записывать изменённые страницы?

    Показать ответ

    Изменения в базе разбросаны по диску, и запись соответствующих страниц

    • это случайные обращения по всему тому, что медленно. Журнал же пишется строго последовательно, дозаписью в конец одного потока, а последовательная запись на порядки быстрее случайной. WAL позволяет подтвердить транзакцию быстрой последовательной записью в журнал, а медленную случайную запись страниц отложить и размазать во времени (bgwriter и контрольная точка).
  3. Что физически происходит при COMMIT и за что отвечает synchronous_commit?

    Показать ответ

    При COMMIT в журнал дописывается запись о коммите, и PostgreSQL ждёт, пока журнал будет сброшен на диск (fsync) до этой записи; только потом клиент получает подтверждение. Это обеспечивает durability. Параметр synchronous_commit = on (по умолчанию) включает это ожидание; off позволяет COMMIT вернуться, не дожидаясь fsync - быстрее, но при сбое можно потерять несколько последних подтверждённых транзакций. Данные при этом остаются согласованными, теряется только их хвост.

  4. Что такое full-page image и зачем он нужен?

    Показать ответ

    Это полная копия страницы (8 КБ), записанная в журнал при первом её изменении после контрольной точки, вместо обычной дельты. Нужна она против рваной записи: если сбой случится во время записи 8-КБ страницы, на диск может попасть её половина (часть новая, часть старая), и обычная дельта-запись такую страницу не починит. Full-page image при восстановлении кладётся поверх возможно повреждённой страницы целиком, после чего применяются обычные мелкие записи. Управляется параметром full_page_writes.

  5. Что показывает разница двух LSN и где смотреть темп генерации WAL?

    Показать ответ

    LSN - это смещение в байтах в потоке журнала, поэтому разница двух LSN равна объёму WAL между ними в байтах. По ней измеряют, сколько журнала сгенерировано за период, и лаг репликации (насколько отстала реплика). Темп генерации и состав журнала видно в pg_stat_wal: wal_records - число записей, wal_bytes - объём, wal_fpi - число full-page images, по которому заметен всплеск сразу после контрольной точки.

← Предыдущая17-buffer-cacheСледующая →19-checkpoints-recovery
Footer
linuxlab-
Copyright © 2026 LinuxLab. Все права защищены.
Учебники
Цены
О платформе
Конфиденциальность и куки