---
title: "Журналирование и согласованность при сбоях"
description: "Диск выполняет эти три записи по очереди. Между ними может пропасть питание или упасть ядро ОС. Тогда на диске окажется часть изменений, и файловая система будет несогласованной…"
canonical: https://graphlms.ru/os/book/journaling
course: "Операционные системы"
updated: 2026-10-12
lang: ru
publisher: GraphLMS
---

# Журналирование и согласованность при сбоях

> Источник: https://graphlms.ru/os/book/journaling — курс «Операционные системы», GraphLMS.

## Одна операция — несколько записей на диск

В [главе про устройство ФС](/os/book/fs-implementation) мы видели, что даже
простая операция меняет несколько структур в разных местах диска. Возьмём
дозапись одного блока в конец файла. Нужно обновить три вещи:

```text
   битмап данных (B)    inode файла (I)          блок данных (D)
   пометить новый блок  размер + указатель на D  сами байты
   занятым
```

Диск выполняет эти три записи по очереди. Между ними может пропасть питание
или упасть ядро ОС. Тогда на диске окажется часть изменений, и файловая система
будет **несогласованной** (inconsistent): её структуры противоречат друг другу.
Переупорядочить записи может и сам диск или планировщик ввода-вывода, так что
«записали в правильном порядке» ещё не значит «легли в правильном порядке».

## Что бывает, если легла только часть

Разберём все варианты, когда из трёх записей B, I, D на диск попали не все.

```text
   легла только D      данные записаны, но inode на них не ссылается и битмап
                       считает блок свободным. Как будто записи не было.
                       Файловая система согласована, данные пропали.

   легла только I      inode ссылается на блок D, но в D старый мусор,
                       а битмап считает D свободным. Позже блок выдадут другому
                       файлу, и два файла будут делить один блок.

   легла только B      битмап считает блок занятым, но никто на него не
                       ссылается. Утечка места: блок не выдадут никогда.

   легли I и B         структуры согласованы, но в D мусор: файл «читается»,
                       а внутри старое содержимое чужого удалённого файла.

   легли I и D         inode ссылается на D, данные на месте, но битмап
                       считает D свободным → тот же риск двойного использования.

   легли B и D         блок занят и заполнен, но inode о нём не знает → утечка.
```

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

## Старый способ: fsck после каждого сбоя

Исторически с этим жили так: после аварийной перезагрузки, до монтирования,
запускали **fsck** (file system check). Утилита проходит по всей файловой
системе и сверяет структуры:

```text
   1. прочитать все inode, собрать, какие блоки реально используются
   2. сверить с битмапами, исправить расхождения
   3. проверить каталоги: записи ведут на живые inode, числа ссылок верны
   4. inode без единого имени → в /lost+found
```

У этого подхода два больших недостатка.

1. **Время.** fsck читает всю файловую систему, и время растёт с её размером,
   а не с объёмом незавершённой работы. Для терабайтного раздела на HDD это
   десятки минут простоя после каждого сбоя.
2. **Он чинит структуры, а не данные.** fsck вернёт согласованность метаданных,
   но в случае «легли I и B» решит, что всё в порядке, хотя в файле мусор.

Нужен способ, при котором восстановление занимает время, пропорциональное
объёму прерванной работы. Это журналирование.

## Журналирование: сначала записать намерение

Идея пришла из баз данных. Прежде чем трогать основные структуры, файловая
система записывает в отдельную область диска — **журнал** (journal) — что
собирается сделать. И только потом вносит изменения на их места. Если сбой
случится посередине, журнал скажет, что нужно доделать. Отсюда название
**write-ahead logging** (WAL): журнал пишется **до** изменений.

Аналогия: прежде чем переставлять мебель, вы пишете на листке полный план:
«диван к окну, стол на кухню, шкаф в коридор». Прервали посередине —
возвращаетесь к листку и доделываете по плану. Прервали, пока писали листок, —
план неполный, мебель вы ещё не трогали, листок выбрасываете.

Одна запись в журнале — **транзакция**:

```text
   журнал (отдельная область диска, пишется последовательно)

   ┌─────────┬─────┬─────┬─────┬──────────┐
   │ TxBegin │  B  │  I  │  D  │ TxCommit │
   │  TID=5  │     │     │     │  TID=5   │
   └─────────┴─────┴─────┴─────┴──────────┘

   1. записать TxBegin и новые версии блоков B, I, D в журнал
   2. дождаться, пока это реально легло на носитель (барьер)
   3. записать TxCommit
   4. дождаться (барьер)
   5. checkpoint: записать B, I, D на их настоящие места
   6. отметить, что транзакция применена, место в журнале можно переиспользовать
```

Почему commit пишется отдельно, после барьера? Если отправить всё одной
пачкой, диск может записать commit раньше, чем блок D (он вправе
переупорядочивать запросы). Тогда после сбоя журнал выглядел бы полным, а
внутри был бы мусор. А сама запись commit маленькая — помещается в один сектор
(512 байт), а запись одного сектора диски выполняют атомарно: она либо
целиком легла, либо нет. Поэтому commit — надёжная «печать»: если она есть,
всё перед ней на месте.

Этот барьер можно сэкономить, если положить в commit контрольную сумму всей
транзакции и писать всё сразу: после сбоя транзакция с неверной суммой
считается неполной. В ext4 так работает опция `journal_async_commit` (по
умолчанию выключена).

Перенос изменений из журнала на настоящие места называется **checkpoint**.
Журнал обычно небольшой (у ext4 — от десятков мегабайт до 1 ГБ) и используется
по кругу: после checkpoint место транзакции освобождается.

## Восстановление: проигрывание журнала

После сбоя файловая система не проверяет весь диск. Она читает только журнал и
смотрит на каждую транзакцию:

```text
   транзакция в журнале         что делаем при монтировании
   ─────────────────────────    ───────────────────────────────────────
   есть TxBegin и TxCommit  →   replay: записать B, I, D на их места
                                (даже если уже писали — это те же байты,
                                повтор безопасен)

   есть TxBegin, нет Commit →   пропустить: на настоящие места мы пишем
                                только после commit, значит, их не трогали
```

Посмотрим на два момента сбоя.

```text
   сбой до commit:                   сбой после commit, до checkpoint:
   │Begin│ B │ I │ ...               │Begin│ B │ I │ D │Commit│
   транзакция неполная →             транзакция полная → replay,
   игнорируем; файловая              B, I, D доводятся до своих мест;
   система как до операции           операция выполнена целиком
```

Главный инвариант: **на настоящие места пишется только то, что уже полностью
закоммичено в журнал**. Поэтому после восстановления операция либо выполнена
целиком, либо не выполнена вовсе, промежуточного состояния нет. Это и есть
атомарность, которой не хватало.

Сколько длится восстановление? Столько, сколько нужно на чтение журнала —
секунды, независимо от того, 100 ГБ раздел или 100 ТБ. Проигрывание повторяет
только операции с commit, и это ровно то, что вы реализуете в задаче
[«WAL: восстановление по журналу»](/os/tasks/wal-replay).

## Что журналировать: три режима ext4

Внимательный читатель заметит, что всё пишется дважды: сначала в журнал, потом
на место. Для метаданных это терпимо, они маленькие. Блоки данных бывают
огромными, и удваивать запись гигабайтного файла дорого. Поэтому ext4 умеет
три режима (опция монтирования `data=`):

| Режим | Что в журнале | Порядок данных | Что может случиться после сбоя |
|---|---|---|---|
| `data=journal` | метаданные и данные | данные тоже через журнал | ничего неожиданного; всё пишется дважды |
| `data=ordered` (по умолчанию) | только метаданные | данные пишутся на место **до** коммита метаданных | последние изменения данных могут не сохраниться, но мусора в файлах не будет |
| `data=writeback` | только метаданные | порядок не гарантирован | структуры целы, но в недавно изменённых файлах может оказаться старое или чужое содержимое |

Режим **ordered** закрывает самый неприятный случай из начала главы — «inode
указывает на блок, а в блоке мусор»:

```text
   1. записать блок данных D на его место
   2. только после этого записать B и I в журнал и commit
   3. checkpoint B и I

   если inode ссылается на блок, в блоке уже новые данные
```

XFS и большинство других журналируемых файловых систем журналируют только
метаданные и тоже следят за порядком, чтобы не выставлять наружу чужие
данные.

**Частая ошибка: считать, что журналирование защищает данные приложения.** Оно
гарантирует, что после сбоя **файловая система** будет согласованной. Данные,
которые лежали в page cache и не прошли `fsync`, теряются в любом режиме,
включая `data=journal` (транзакции журнала тоже копятся в памяти и сбрасываются
раз в несколько секунд — у ext4 по умолчанию раз в 5 секунд, опция `commit=`).
Долговечность конкретной записи обеспечивает только `fsync`, см.
[fsync и durability](/os/book/fsync-durability). Журналирование делает `fsync`
дешевле и надёжнее, но не заменяет его.

## Это тот же WAL, что в базах данных

Журналирование файловой системы и WAL базы данных — одна идея: сначала
последовательный журнал намерений, потом изменения на местах.

```text
   ext4 (журнал jbd2)                 PostgreSQL, etcd, SQLite в режиме WAL
   ────────────────────────────       ──────────────────────────────────────
   TxBegin / блоки / TxCommit    ≈    записи WAL + commit-запись
   барьер после commit           ≈    fsync WAL перед ответом «COMMIT»
   checkpoint на места           ≈    checkpoint: грязные страницы → файлы таблиц
   replay при монтировании       ≈    crash recovery при старте
   журнал по кругу               ≈    старые сегменты WAL удаляются после checkpoint
```

Что из этого следует на практике:

- **Почему дополнительная запись в журнал ускоряет, а не замедляет?** Журнал
  пишется последовательно, а изменения на местах — разбросаны по диску.
  Последовательная запись намного дешевле случайной (см. [HDD](/os/book/hdd) и
  [SSD](/os/book/ssd)). Подтвердить коммит можно, как только лёг журнал, а
  случайные записи выполняются потом, в фоне, пачками.
- **Группировка.** Много мелких операций объединяются в одну транзакцию журнала
  и один барьер. В базах это group commit.
- **Всё держится на честном сбросе.** Если диск подтверждает запись журнала, не
  записав её, журнал бесполезен.

## Copy-on-write: ZFS и btrfs

Другой подход к той же проблеме — **copy-on-write** (COW, копирование при
записи): никогда не перезаписывать данные на месте. Новая версия блока пишется в
свободное место, потом новая версия блока, который на него ссылается, и так до
корня дерева. Последним шагом атомарно переключается указатель на корень.

```text
   было:          root → A → B
   пишем новое:   B' в свободное место
                  A' (ссылается на B') в свободное место
   переключаем:   root → A'

   до переключения корня на диске целиком старая версия,
   после — целиком новая
```

Согласованность получается без журнала: пока корень не переключён, старое
дерево цело и нетронуто. Бонусом бесплатны **снапшоты**: достаточно сохранить
старый корень, и старая версия всей файловой системы останется доступна.
Минусы — фрагментация (файл, который часто меняют кусками, разбрасывается по
диску) и нужда в сборке освободившихся блоков.

Одна оговорка: у COW-систем журнал всё-таки есть, но для другого. Переключать
корень на каждый `fsync` дорого, поэтому ZFS ведёт ZIL (ZFS intent log), а
btrfs — log tree: туда быстро записываются синхронные операции, чтобы `fsync`
возвращался, не дожидаясь полного переключения дерева.

Идея «писать новое рядом, а не поверх» продолжается в
[лог-структурированных файловых системах](/os/book/lfs) — там она доведена до
предела.

## Сводка

| | fsck | Журналирование | Copy-on-write |
|---|---|---|---|
| Когда работает | после сбоя, при загрузке | постоянно + replay при монтировании | постоянно |
| Время восстановления | пропорционально размеру ФС | пропорционально журналу (секунды) | почти мгновенно |
| Двойная запись | нет | метаданные всегда; данные в `data=journal` | нет, но пишутся новые версии узлов дерева |
| Снапшоты | нет | нет | дешёвые |
| Примеры | ext2, FAT | ext3, ext4, XFS, NTFS | ZFS, btrfs, APFS |

> Квиз по разделу: см. вопросы в конце документа или на https://graphlms.ru/os/book/journaling

## Частые ошибки

- Думать, что журналирование сохраняет данные приложения без `fsync`.
- Путать режимы ext4: `writeback` быстрее, но после сбоя в файлах может оказаться старое или чужое содержимое.
- Считать, что журнал удваивает стоимость записи и потому медленный: он последовательный, а подтверждать коммит можно сразу после записи журнала.
- Полагать, что fsck восстанавливает данные. Он восстанавливает согласованность структур.

## Проверьте себя

1. Почему fsck работает тем дольше, чем больше диск, а replay журнала — нет?
2. Почему commit-запись пишут после барьера, а не вместе с транзакцией?
3. Чем `data=ordered` защищает от появления в файле чужих данных?
4. После сбоя в журнале три транзакции: T1 (Begin, B, I, Commit), T2 (Begin, I, D, Commit), T3 (Begin, B). Checkpoint успел выполниться только для T1. Что сделает ФС с каждой и почему повторная запись блоков T1 ничего не сломает?

Ответы. (1) fsck проверяет все структуры ФС, а replay читает только журнал, размер которого не зависит от диска. (2) Диск может переупорядочить записи и положить commit раньше содержимого транзакции; после сбоя такая транзакция выглядела бы полной. (3) Данные записываются на место раньше, чем коммитятся ссылающиеся на них метаданные, поэтому inode никогда не указывает на блок со старым содержимым. (4) T1 и T2 будут проиграны (у T1 место в журнале могло быть уже освобождено, тогда её просто нет), T3 пропущена: у неё нет commit. Повтор T1 записывает на места те же байты, что уже там лежат, результат не меняется.

## Что спрашивают на собесе

- **Почему создание файла не атомарно?** Это несколько записей в разные места
  диска (битмапы, inode, каталог, данные); сбой посередине оставляет ФС
  несогласованной.
- **Что такое write-ahead logging?** Сначала намерение записывается в журнал и
  фиксируется commit-записью, потом изменения применяются на местах. После сбоя
  проигрываются только транзакции с commit.
- **Почему commit пишется отдельно?** Чтобы диск не записал его раньше
  содержимого транзакции; сама commit-запись умещается в атомарно записываемый
  сектор.
- **Чем журнал лучше fsck?** Время восстановления зависит от размера журнала, а
  не диска, и согласованность гарантируется построением, а не эвристикой.
- **Режимы ext4?** `journal` — всё через журнал; `ordered` (по умолчанию) —
  только метаданные, данные пишутся раньше них; `writeback` — только
  метаданные, без порядка.
- **Защищает ли журнал данные приложения?** Нет. Без `fsync` последние записи
  теряются в любом режиме.
- **Как журналирование ФС связано с WAL в базах?** Это одна идея:
  последовательный журнал, барьер, checkpoint, replay.
- **Чем COW отличается?** Не перезаписывает на месте, атомарно переключает
  корень, даёт дешёвые снапшоты; платит фрагментацией.

## Вопросы для самопроверки

**Квиз «Журналирование ФС»**

1. Почему операция «создать файл» опасна при внезапном отключении питания?
   - Диск физически ломается от записи
   - Это несколько независимых записей в разные места диска, и часть может не лечь (верно)
   - ОС не умеет создавать файлы без перезагрузки
   - Файл всегда теряется целиком
   - Пояснение: Создание/изменение файла = обновить inode, bitmap и каталог/данные. Это разные записи в разные секторы; краш между ними оставляет ФС несогласованной.
2. В чём суть write-ahead logging (WAL)?
   - Никогда не перезаписывать блоки, только добавлять новые
   - Сначала записать намерение в журнал и зафиксировать commit, потом применить на места (верно)
   - Сначала применить изменения на места, потом записать лог
   - Писать данные в двух копиях на разные диски
   - Пояснение: WAL = запись с опережением: журнал пишется ДО основных структур. Применяем на места только после commit, поэтому после краша состояние восстановимо.
3. После краша найдены две транзакции: одна с begin и commit, другая с begin, но без commit. Что делает ФС?
   - Первую (с commit) проигрывает (replay), вторую (без commit) пропускает (верно)
   - Обе проигрывает заново
   - Запускает fsck по всему диску
   - Обе пропускает
   - Пояснение: Только полностью зафиксированные транзакции (с commit) докатываются на места. Без commit транзакция не подтверждена и на настоящих местах ФС её нет.
4. Главное преимущество журналирования над fsck:
   - Журнал восстанавливает удалённые пользователем файлы
   - Журнал не требует диска
   - Время восстановления зависит от размера журнала, а не всего диска (верно)
   - fsck невозможно запустить на SSD
   - Пояснение: fsck сканирует весь диск (минуты-десятки минут). Replay журнала идёт за секунды, потому что журнал маленький и фиксирует только незавершённую работу.
5. Что верно про ordered (metadata) journaling?
   - Данные пишутся на место ДО коммита метаданных (верно)
   - В журнал попадают и данные, и метаданные
   - Данные могут потеряться, но структура ФС останется целой (верно)
   - В журнал попадают только метаданные (inode, bitmap) (верно)
   - Пояснение: Ordered журналирует только метаданные и требует записать данные раньше ссылающихся метаданных. Быстрее full-режима, но последняя запись данных может не сохраниться.
6. Сколько раз в full data journaling реально записывается блок данных D (журнал + место)?
   - Ответ: 2
   - Пояснение: В полном журналировании данные пишутся дважды: сначала в журнал, затем при checkpoint на своё место в ФС. Это цена максимальной надёжности.
