Formatted transcription

Исправленный текст (Часть 1):


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

— Файловые системы читал.

Файловые системы? Хм, что именно он вам рассказал?

— Есть журнальные файловые системы. — И журнальные файловые системы.

О, даже до журнальных дошёл?

— Ну, чуть-чуть.

Так. И что значит «чуть-чуть»? Что же он рассказал про журнальные файловые системы?

— У них есть журнал.

Логично. Кажется, я слышал, что это довольно интересная тема… там приводился какой-то пример?

— Там пример приводился, какая [система] журнальная.

Нет, на самом деле, сейчас почти всё, с чем вы имеете дело, — это журнальные файловые системы. Но тем не менее, зачем вообще файловой системе нужен журнал?

— Чтобы можно было восстанавливать после сбоя.

После сбоя чего?

— После сбоя сервера, на котором файл.

Ну, допустим, сервера. Так, и что же там надо восстанавливать?

— Операция началась, но не закончилась, допустим.

Да, но что именно нужно восстанавливать после такого события? Какое состояние мы хотим вернуть?

— Может быть, транзакции?

Транзакции… Да, когда мы используем журналы, транзакции — это важное понятие. Кстати, как я понимаю, вы еще не дошли до другого интересного типа журналов, в котором восстанавливаются не транзакции. Тем не менее, да, транзакция — это одно из промежуточных понятий, которое может сильно помочь нам в восстановлении. Но всё-таки это не ответ на вопрос: что мы восстанавливаем?

— Запрос?

Запрос мы не можем восстановить, скорее всего.

— Консистентность?

О! Да. Консистентность. То есть почти все файловые системы, с которыми вы имеете дело, — это достаточно сложные структуры данных, у которых есть определенные критерии консистентности.

При этом, на самом деле, есть две истории. Первая: в норме с файловой системой у вас работает только одна нить – нить драйвера файловой системы. И вроде бы та консистентность, которую мы проходили в многопоточности, здесь ни при чём.

Однако, когда у вас, например, выключается электричество, садится батарейка, или если это USB-флешка, вы её выдёргиваете из компьютера, не отключив предварительно… Или когда у вас сетевое хранилище (Storage Access Network, SAN), и вы обращаетесь к диску через коммутационную инфраструктуру, которая может сломаться… Работа с диском прекратится внезапно. И структуры данных на диске могут остаться в неконсистентном состоянии.

Какой вариант неконсистентности наиболее вероятен и при этом достаточно опасен?

— Банковский перевод?

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

— Запись не завершилась? Частичная запись метаданных?

Нет. Консистентность пользовательских данных внутри файла сами файловые системы обычно не обеспечивают. Приложения, которым это нужно (например, базы данных), делают собственные журналы и занимаются этим самостоятельно.

— Запись на диск?

Сама по себе запись на диск не приводит к неконсистентности структур файловой системы.

— Перемещение? Удаление файла?

Удаление файла или его перемещение – не самые частые операции, приводящие к определенному типу критической неконсистентности. Какая самая частая операция над файлом, которая приводит к модификации структур данных файловой системы?

— Открыть?

Нет. Открытие файла само по себе может не приводить к модификации метаданных.

— Изменить параметры?

Нет.

— Перемещение? Переименование?

Перемещение и переименование в пределах одной файловой системы – это, по сути, одно и то же. И это довольно редкие операции по сравнению с той, о которой я думаю.

— Редактирование файла?

Редактирование файла – это операция над его содержимым, а не обязательно над его метаданными (кроме, может быть, времени последнего изменения).

— Обновление времени последнего апдейта?

Даже если вы потеряете эту информацию, это не сильно страшно. И главное, это время лежит в одном месте.

— Создание файла?

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

— Поиск?

Нет, поиск – это не модификация.

— Чтение?

Чтение не приводит к модификации метаданных.

— Просто не могу другую частую операцию вспомнить.

Можете. Чтение вообще не приводит ни к какой модификации.

— Изменение прав доступа?

Права доступа тоже меняют редко. Я говорю об операциях, которые могут проводиться тысячами и миллионами над каждым диском.

— Может, что-нибудь с диском связано?

Все операции файловой системы связаны с диском.

— Исполнение?

Исполнение, с точки зрения файловой системы, мало чем отличается от чтения. Файл не модифицируется.

— Вроде бы говорили, что мы когда ls используем, что-то же меняется?

Нет, когда вы используете ls, вы смотрите информацию, ничего менять не надо.

— Мы как будто все уже операции назвали.

Нет, вы почему-то тщательно обходите её стороной.

— Открытие? Удаление?

Было уже.

— А слово близкое было? Запись?

Запись – самое близкое. В каком случае запись может приводить к глобальным модификациям метаданных?

— Изменение размера?

Да! Обычно это запись в конец файла, но, как мы проходили (разреженные файлы), записи в середину тоже могут приводить к выделению нового места под файл. То есть и в Unix, и в Windows, во всех привычных вам операционных системах, файл можно увеличивать “на ходу”.

Увеличение файла – это выделение ему нового блока, который до этого был помечен как свободный. Как хранятся свободные блоки в большинстве современных файловых систем? Хлевков (Хаверко?), по идее, должен был вам это рассказать.

— В суперблоке?

Нет, в суперблоке может храниться их количество, но их список туда очевидно не влезет.

— [Неразборчиво, возможно “FAT file system?”]

Как ни странно, мимо. Хотя видал я и такое.

— Таблица?

Теплее. Какую таблицу логично для этого использовать? Какого формата?

— Вектор, может быть? Растущая таблица?

Не совсем понял.

— У нас там айноды (inodes), таблицы нод…

Нет, айноды – это сами файлы. Я спрашиваю, как хранить свободные блоки. Свободные блоки по определению не принадлежат никаким файлам.

— Bitmap?

Да. Битовая карта (bitmap). Почти все файловые системы, с которыми вы имеете дело, хранят свободные блоки в виде битмапа. То есть на каждый блок есть битик: единица – свободен, ноль – занят (или наоборот).

А занятые блоки хранятся в инодах, часто в виде каких-то развесистых деревьев.

Итак, у вас информация о занятых и свободных блоках лежит, мало того, что в разных местах, так ещё и в разных форматах (иноды и bitmap). И вот тут возникает самая страшная неконсистентность: если вы умудритесь посчитать блок одновременно и свободным (в bitmap), и принадлежащим какому-то файлу (в inode). Тогда при попытке создать другой файл или расширить существующий, у вас получится так называемый cross-linked file – блок, который принадлежит двум файлам одновременно. Очевидно, что, по крайней мере, в одном файле данные будут испорчены, с высокой вероятностью – в обоих.

Второй вариант тоже плохой: когда блок не считается ни занятым (нет в инодах), ни свободным (помечен как занятый в bitmap). Просто потерянный блок. Как это возможно? Очень просто: вы начали операцию выделения блока файлу, пометили его как занятый в bitmap, но не успели добавить ссылку на него в inode, и тут всё “умерло”. Это очень возможно.

Более того, диски (особенно старые HDD со шпинделями) умеют делать так называемое переупорядочение операций записи для оптимизации движения головок (всякие там “элеваторные” сортировки). В результате операции модификации метаданных могут выполниться не в том порядке, в котором их запросила ОС. И даже если ваш порядок операций вроде бы гарантирует, что у вас могут быть только потерянные блоки, из-за переупорядочения на диске вы всё равно можете получить cross-linked файл.

Эта проблема известна с 60-х годов. Есть разные направления борьбы с ней. Одно из них – пытаться строить файловую систему так, чтобы она от этого не страдала. Вот файловая система FAT интересна не только тем, что её, как говорят, изобрёл лично Билл Гейтс. Она интересна тем, что у неё информация о занятых и свободных блоках лежит в одном месте – в этой самой таблице FAT (File Allocation Table). И операция выделения блока файлу там в некотором роде атомарна. Поэтому FAT одним из своих преимуществ имел относительную устойчивость к аварийным выключениям. Но у FAT куча других недостатков, поэтому от него в основном отказались для системных дисков.

До массового распространения журнальных файловых систем была другая техника, с которой народ тоже жил. В Unix она называется FSCK (File System Check). Давайте я напомню, как она работает.

[Поиск презентации] … Она у меня лежит, к сожалению, в корне файловой системы… Вот, восстановление файловой системы. Да, как раз то, о чём я пытался рассказать. Потерянные блоки или cross-linked блоки – это две наиболее типовые проблемы.

Как работают сервера баз данных? Они могут играть роль мониторного процесса, сериализовать транзакции. Но при выключении питания вся эта сериализация не поможет сохранить консистентность дисковых структур без дополнительных механизмов.

Итак, три подхода к восстановлению консистентности:

  1. Традиционный: “Dirty Flag” и Check Disk (или FSCK).
  2. Журнальные файловые системы.
  3. Copy-on-Write (COW) или Log-structured файловые системы.

Кстати, оба последних подхода имеют аналоги в базах данных, о которых вы будете систематически проходить в следующем учебном году.

Рассмотрим первый подход: Dirty Flag и Check Disk/FSCK. Идея простая: в заголовке файловой системы (обычно в суперблоке) есть флажок, “флаг загрязнения” (Dirty Flag). Когда мы монтируем файловую систему на чтение-запись, мы этот флажок взводим. Если мы её корректно размонтируем, мы его снимаем. (Бывают варианты: если драйвер видит, что изменений нет, он может временно погасить флаг, потом снова взвести при записи, но это детали).

Когда система стартует и пытается смонтировать файловую систему, она проверяет этот флаг. Если флаг стоит (т.е. система была выключена некорректно), запускается специальная утилита: Check Disk в Windows или FSCK в Unix.

Что эта утилита делает? Она похожа на сборку мусора по алгоритму Mark-and-Sweep. Она игнорирует старую информацию о свободных блоках (например, bitmap). Она проходит по всем инодам (или аналогичным структурам), начиная с корневого каталога, и строит карту реально занятых блоков. Всё, что не попало в эту карту, объявляется свободными блоками, и информация о них (например, новый bitmap) перезаписывается.

На этапе прохождения по инодам решаются и другие проблемы. Например, хардлинки: если имя файла добавили в каталог, а счётчик ссылок в иноде не увеличили, или наоборот – имя удалили, а счётчик не уменьшили, и инод не освободился. Такой “потерянный” файл без имени, но с ненулевым счётчиком ссылок, FSCK может найти. Во многих Unix-системах есть специальная папка /lost+found в корне каждой файловой системы, куда FSCK помещает такие найденные потерянные файлы (давая им имена по номеру инода). Кстати, наличие папки lost+found – это признак того, что вы смотрите на точку монтирования отдельной файловой системы.

В общем, все подобные проблемы решаются одним путем – полным сканированием структур файловой системы и восстановлением их целостности на основе найденной информации, аналогом Mark-and-Sweep.

В чём проблема с этим подходом?

  1. Долго: Время проверки пропорционально общему размеру файловой системы и количеству файлов/каталогов на ней. На больших дисках это может занимать часы.
  2. Ресурсоёмко: Требуется память для хранения промежуточных данных (карт занятых/свободных блоков). Если памяти мало, а диск большой, утилите приходится использовать сам проверяемый диск для временных файлов, что еще больше замедляет процесс. Особенно весело, если файл подкачки (swap) лежит на этом же диске – его нельзя использовать!

Поэтому, если у вас машина с большими дисками и относительно маломощная (какими часто бывают файловые серверы), проверка диска после сбоя могла быть очень долгой. Исторически всегда были конфигурации, где fsck после аварийного выключения мог идти очень долго. Всех это, конечно, доставало.

Особенно забавна проблема восстановления корневой файловой системы. Ведь утилита FSCK сама лежит на этой файловой системе! Как её запустить, если файловая система повреждена и не может быть смонтирована в обычном режиме (на чтение-запись)? Ответ: её монтируют в режиме “только чтение” (read-only). В этом режиме её можно проверять утилитой FSCK (которая читается с этого же read-only раздела), не боясь усугубить повреждения. После проверки и исправления ошибок файловую систему перемонтируют уже в обычном режиме чтения-записи. В Unix этот процесс можно даже наблюдать при загрузке системы после сбоя.

Тем не менее, долгое время восстановления всех достало. И первыми, кто придумал более эффективное решение, были разработчики баз данных, потому что у них проблема консистентности стояла еще острее.

Решение состоит во введении понятия транзакции. Транзакция – это группа операций, которая обладает свойством атомарности: либо все операции выполняются успешно, либо ни одна из них не оказывает влияния (система возвращается в исходное состояние). Важно, что до начала транзакции и после её успешного завершения данные находятся в целостном (консистентном) состоянии. Внутри транзакции целостность может временно нарушаться.

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

Логика выделения кода транзакции очень похожа на понятие критической секции в многопоточности. Но для транзакции нам нужна атомарность не только относительно других потоков, но и относительно сбоев всей системы (например, отключения питания). Обычные блокировки тут не помогут – они исчезнут вместе с памятью при сбое.

— По журналу пройтись?

Вот! Тут и возникает понятие журнала (log). Мы выделяем на диске специальную область для журнала. Обычно это область фиксированного размера, организованная как кольцевой буфер. (Иногда журнал выносят на отдельный, возможно, более быстрый носитель, но это реже).

Далее, транзакции выполняются в несколько этапов (здесь описывается один из вариантов – redo log):

  1. Сначала описание всех операций транзакции пишется в журнал. Сами рабочие данные файловой системы пока не трогаются.
  2. Система убеждается, что записи журнала физически сохранены на диске (операция sync или flush).
  3. Только после этого система начинает применять изменения к основным структурам данных файловой системы – это называется “накат” транзакции.
  4. После успешного наката в журнал может быть добавлена специальная запись о завершении (commit).

(Существует и другой тип журнала – rollback log, где сохраняется старое состояние данных, чтобы можно было “откатить” незавершенную транзакцию. Redo-журналы часто логически проще для файловых систем).

Преимущество redo-журнала в том, что можно записывать сами операции, а не полные копии изменяемых данных. Если операция короткая (например, изменить один бит в bitmap), а затрагиваемый блок данных большой, это может быть выгоднее.

В файловых системах это упрощается тем, что транзакции обычно затрагивают только метаданные (иноды, bitmap, блоки каталогов). Типы таких транзакций известны разработчикам драйвера файловой системы (их не так много, порядка десятка: создание файла, удаление, изменение размера и т.д.). В базах данных же транзакция может включать произвольные операции над данными, что усложняет устройство журнала.

Интереснее становится, когда транзакции выполняются параллельно и могут перекрываться. У вас может быть несколько транзакций, записанных в журнал, но еще не полностью “накаченных” на основные данные. Управление этим процессом – отдельная сложная тема, которую вы будете изучать позже. Но даже при параллелизме, идея журнала остается основной.

Понимание механизма транзакций и журналирования очень важно. Я как-то помогал собеседовать кандидатов на роль администратора Lotus Notes и заметил закономерность: если человек понимал, что такое журнал транзакций в Lotus и зачем он нужен, он обычно хорошо отвечал и на другие вопросы. Если нет – то и в остальном часто была “разруха в мозгах”. Обработка транзакций – фундаментальное понятие, с которым вы столкнетесь во многих областях, даже в машинном обучении, хотя и в ином виде. Если оно у вас в голове не уляжется, вас ждут проблемы при изучении более продвинутых материалов.

Кстати, современные системы часто используют готовые библиотеки или движки для хранения данных с поддержкой транзакций (SQLite, Berkeley DB, etcd и т.д.). Вы просто используете их API и верите, что под капотом всё работает правильно. Но когда что-то идет не так, понимание принципов журналирования и транзакций становится необходимым для диагностики.

— Я вот не особо понимаю, зачем запись commit нужна?

Затем, что транзакция – это атомарная единица. Кусок транзакции сам по себе не гарантирует целостности. Запись commit в журнале явно отмечает, что всё описание данной транзакции было успешно записано в журнал. Если при восстановлении мы видим запись транзакции без commit на конце, это значит, что запись в журнал не была завершена (например, из-за сбоя), и эту транзакцию выполнять (накатывать) нельзя, её нужно считать невыполненной вовсе.

— А мы понимаем, какие изменения в файловую систему внесли, посредством sync? Если sync нет?

Записи sync (или аналогичные им чекпойнты) в журнале показывают, до какого момента изменения, описанные в журнале, были реально применены к основным структурам данных. В системах с параллельными транзакциями эти отметки могут ставиться не строго по границам транзакций. Они важны, чтобы при восстановлении знать, какие операции из журнала нужно повторно применить (redo), а какие уже были применены до сбоя.

Здесь возникает еще одно важное понятие – идемпотентная операция. Вы это слово уже проходили?

— [Неразборчиво] …при одинаковых запусках она должна давать [одинаковый результат]…

Не совсем точно. Идемпотентность имеет смысл для операций с побочным эффектом (т.е. изменяющих состояние системы). Операция называется идемпотентной, если её повторное выполнение дает тот же самый результат (тот же самый побочный эффект), что и однократное выполнение. Например:

  • x = 10; – идемпотентная операция. Сколько раз ни выполни, x будет равен 10.
  • x = x + 1; (x++) – не идемпотентная операция. Повторное выполнение изменяет результат.

Если вы можете описать вашу транзакцию как набор идемпотентных операций, это сильно упрощает восстановление. Вы можете просто повторно применить (redo) все операции из журнала, относящиеся к незавершенным транзакциям. Если какая-то операция из-за сбоя была применена частично или применена, но отметка об этом не сохранилась, повторное её применение не навредит. Однако преобразовать произвольную последовательность изменений в идемпотентные операции – это творческая задача, не всегда простая.

Итак, как происходит восстановление с использованием redo-журнала:

  1. Читаем журнал с конца.
  2. Самая приятная ситуация: Последняя запись – это sync или чекпойнт, показывающий, что все предыдущие транзакции полностью применены. Значит, файловая система целостна. Ура, можно загружаться дальше.
  3. Ситуация 2: Последняя запись в журнале – это незавершенное описание транзакции (нет записи commit). Это значит, что система упала во время записи самой транзакции в журнал. Эту транзакцию мы игнорируем (считаем невыполненной вовсе).
  4. Ситуация 3: В журнале есть одна или несколько полностью описанных транзакций (с записью commit), но они еще не были полностью применены к основным данным (нет соответствующей отметки sync или она указывает на более раннюю точку). Мы должны “накатить” эти транзакции – то есть выполнить все описанные в них операции над основными структурами данных файловой системы. Если операции идемпотентны, мы можем просто выполнить их все, начиная с последней точки sync.

Видите, концептуально это сложнее, чем fsck, но имеет огромное преимущество: время восстановления зависит только от размера активной части журнала, а не от размера всего диска. Оно обычно занимает секунды, а не часы. Даже если у вас агрессивно распараллелены транзакции, их количество, находящихся одновременно “в работе”, ограничено. Поэтому полезный объем журнала ограничен, и он восстанавливается за фиксированное (и небольшое) время.

Именно поэтому вы сейчас почти никогда не видите долгую проверку диска при загрузке, даже если у вас села батарейка или выдернули флешку. Компьютер включается, быстро “пробегает” по журналу (вы этого можете даже не заметить) и работает.

— А при запуске Windows есть эта штука? Check Disk может 2-3 часа длиться?

Если Check Disk в Windows (особенно на системном разделе с NTFS или ReFS, которые являются журнальными) запускается и длится долго (часы), это обычно означает одно из двух:

  1. Либо система по какой-то причине решила, что журнал поврежден или недостоверен, и сделала “фолбэк” на полную проверку в стиле старого FSCK. Это может быть признаком бага в драйвере или, что более вероятно, аппаратной неисправности диска (плохие сектора и т.д.).
  2. Либо у вас действительно очень большие диски при маломощном компьютере (хотя для журнального восстановления это не должно быть главным фактором, только для полной проверки).
  3. Или диск физически неисправен, и чтение/запись секторов занимает аномально много времени.

В любом случае, долгий Check Disk на современной журнальной системе – это плохой знак.

Есть и второе, менее очевидное преимущество журнальных систем. Казалось бы, мы пишем все изменения дважды (сначала в журнал, потом на основное место), это должно снижать производительность при нормальной работе. На практике это часто не так. Благодаря журналу, система может гораздо агрессивнее кэшировать операции записи в оперативной памяти. Она может накапливать изменения и сбрасывать их на диск более оптимальными порциями, не боясь потерять данные при сбое, так как всё необходимое для восстановления уже записано (или будет записано перед применением) в журнал. Поэтому журнальные файловые системы часто показывают лучшую производительность записи, чем их нежурнальные аналоги.


Исправленный текст (Часть 2):


Возвращаясь к производительности: журнальные файловые системы часто показывают наблюдаемое повышение производительности.

Проблемы журнальных файловых систем:

  1. Двойная запись: Данные метаданных пишутся на диск дважды (в журнал и на основное место). Это может быть значимо, например, для SSD-накопителей с ограниченным ресурсом циклов записи.
  2. Усложнение драйвера: Логика драйвера файловой системы усложняется. Чем больше кода, тем выше вероятность ошибок.
  3. Сложные сценарии ошибок: Возможны неприятные ситуации, когда сами данные на диске корректны, но из-за бага в драйвере в журнал пишется некорректное описание транзакции. Поймать такой баг сложно, так как он проявится только при сбое аппаратуры и последующей попытке восстановления по неверному журналу.

На заре внедрения журнальных файловых систем такие чудеса случались. Я помню времена Windows NT 4.0: до Service Pack 6 была типовая ситуация, когда пользователь писал на форум: “Винда упала в синий экран, перезагружается и снова падает в синий экран”. При попытке загрузиться с CD-ROM установочная программа сообщала, что хотя раздел помечен как NTFS, действительную NTFS-структуру она там найти не может. То есть при накате журнала после сбоя система записывала что-то такое, что результат переставал выглядеть как распознаваемая файловая система. Это был довольно массовый баг (или несколько багов). Я хорошо помню момент, когда это примерно прекратилось – где-то в 1998-1999 году, при том что NTFS начала свою жизнь году в 91-м. Внедрение шло довольно удручающим темпом, пока это не починили.

Подобный опыт спровоцировал волну так называемых гибридных журнальных файловых систем. Например, в старых Linux была файловая система Ext2 – нежурнальная. Народ ей пользовался, она была отлажена. Потом к ней просто “пришлёпнули” сверху журнал, оставив основную структуру без изменений. Это назвали Ext3. Где-то с середины 90-х до середины 2010-х Ext3 была рабочей лошадкой во многих Linux-системах – и на серверах, и на рабочих станциях. Почему этот подход был популярен? Потому что сам основной код (Ext2) был уже хорошо отработан, ошибок в нём было мало, ему можно было доверять. Добавление журнала решало проблему долгого восстановления (fsck), не внося радикальных изменений в проверенный код.


(Начало перерыва / нерелевантный диалог удален)


Итак, сейчас большинство используемых файловых систем — журнальные: NTFS в Windows, Ext4 или XFS в Linux. Они относятся к рабочим лошадкам.

Тем не менее, есть некоторые нюансы. Главный, как мы уже обсуждали, состоит в том, что в транзакцию у “классических” журнальных файловых систем включаются только метаданные файловой системы, но не пользовательские данные внутри файлов. Почему?

  1. Определение границ транзакций: Чтобы делать транзакции над пользовательскими данными, нужно понимать их логические границы. Драйвер файловой системы этого знать не может, это может знать только приложение. Стандартные API для работы с файлами (типа POSIX read/write/seek) вообще не имеют операций для обозначения начала и конца пользовательской транзакции. Существующие приложения не смогли бы этим воспользоваться. А без четких границ транзакций вся затея с гарантией целостности данных теряет смысл.
  2. Производительность: Стоимость транзакции примерно пропорциональна объему данных, которые в неё включены. Если включать в транзакции файловой системы еще и пользовательские данные, объем записи в журнал (и потенциально двойной записи) резко возрастет, что сильно ударит по производительности.

Пользователи, которым нужна транзакционная целостность их собственных данных внутри файлов, должны реализовывать это сами. Большинство серверов баз данных так и делают: они ведут свои собственные журналы транзакций поверх обычной файловой системы. При этом им приходится использовать системный вызов fsync (или аналогичный), чтобы гарантировать, что данные их журнала физически записаны на диск перед тем, как считать транзакцию зафиксированной. Кто помнит, что делает fsync?

— Записать всё, что [в кэше]?

Не столько “записать” (запись может и так идти асинхронно), сколько дождаться, пока данные, относящиеся к указанному файлу (включая его метаданные), будут гарантированно сброшены из кэша ОС на физический носитель. Используя fsync для файла журнала, приложение может реализовать фазу “убедиться, что данные легли на устройство”.

Однако есть задачи, где стандартный подход с журналированием только метаданных создает проблемы. Самый интересный пример – резервное копирование работающей системы, например, сервера базы данных. Вам нужно скопировать и сами файлы данных, и журнал транзакций. Но копирование больших объемов занимает время. Вы получите состояние журнала на один момент времени, а состояние файлов данных – растянутое по времени или на другой момент. Журнал без соответствующих данных и данные без соответствующего журнала бесполезны. Если вы попытаетесь восстановиться из такого бэкапа и “накатить” журнал на неконсистентные данные, произойдет катастрофа – все данные превратятся в бесполезный фарш, как в примере с багами ранней NTFS.

Как быть?

— Остановить приём новых транзакций?

Да, один вариант – остановить сервер базы данных (или приложение), дождаться сброса всех данных на диск, сделать бэкап и потом запустить сервер снова. Во многих инструкциях так и рекомендуют. Но это не всегда приемлемо, особенно для систем, требующих круглосуточной доступности (24/7). Кроме того, бэкап больших данных может быть долгим, и простой на это время тоже нежелателен.

Здесь на помощь приходит другая технология – моментальные снимки (snapshots) или чекпойнты. Идея перекликается с уже знакомым нам принципом Copy-on-Write (COW).

Представьте: вы даете команду создать снимок файловой системы в определенный момент времени. Система создает некую виртуальную копию, которая отражает состояние всех файлов и каталогов на этот момент. Эта копия доступна только для чтения (обычно). При этом исходная файловая система продолжает работать и модифицироваться. Как это возможно?

— Copy-on-Write?

Именно. Когда поступает запрос на изменение какого-либо блока данных (или метаданных) на исходной файловой системе после создания снимка, система не перезаписывает старый блок. Вместо этого она выделяет новое место, записывает измененные данные туда, а старый блок оставляет нетронутым, так как на него “смотрит” снимок. Это немного напоминает rollback-журнал, но без явного окончания (коммита), и старые версии блоков сохраняются до тех пор, пока существует снимок, ссылающийся на них.

В некоторых системах (например, NTFS в Windows) снимки реализованы поверх “обычной” журнальной файловой системы. Неизмененные блоки для снимков хранятся в свободном месте на том же томе. Пока свободное место не кончится, вы можете иметь один или несколько таких “мгновенных снимков” состояния файловой системы. С такого снимка можно спокойно делать резервное копирование, не останавливая работу основной системы. Большинство утилит бэкапа Windows так и работают: они инициируют создание временного теневого снимка (VSS - Volume Shadow Copy Service) и копируют данные с него. (Правда, в Windows нет удобного пользовательского интерфейса для произвольного создания и управления такими снимками, в основном это используется под капотом системными службами и бэкапом).

Однако реализация снимков поверх традиционных файловых систем имеет свои недостатки: это может быть дорого по производительности и выглядит несколько “костыльно”. Народ задумался: а нельзя ли улучшить сам принцип работы файловой системы, сделав снимки и COW её неотъемлемой частью?

Здесь мы подходим ко второму основному типу современных файловых систем: Log-structured или Copy-on-Write файловые системы.

Вспомним старые CD-ROM. Изначально компакт-диски (CD) вообще нельзя было записать в домашних условиях, их печатали на фабрике. Потом появились записываемые CD-R (Recordable). Классический CD-R можно было записывать, но запись была необратимой – переписать уже записанный сектор было нельзя. Как же тогда обновлять данные на таком диске?

Файловая система на CD-ROM (обычно ISO 9660) устроена просто: файлы лежат подряд, занимая непрерывные области. Каталоги – это тоже специальные файлы, содержащие ссылки на начало файлов и подкаталогов. Всё похоже на архив.

Теперь представим, что мы хотим “переписать” файл на CD-R. Мы не можем изменить старые сектора. Что делать?

— Две копии?

Да. Мы находим начало свободного места на диске и пишем туда новую версию файла целиком. Но теперь нам надо обновить запись в каталоге, чтобы она указывала на новое местоположение файла. А каталог тоже уже записан! Значит, нам надо записать новую версию каталога в свободное место, изменив в ней ссылку на файл. Но на этот каталог ссылается родительский каталог! Значит, надо записать новую версию родительского каталога… и так далее, пока мы не дойдем до корневого каталога.

— Получается, мы весь диск переписали?

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

Для CD-ROM была придумана концепция мультисессионных дисков. На диске резервируется место под несколько (до 256) корневых каталогов (точнее, указателей на них, “Volume Descriptors”). Когда вы “дописываете” файлы на диск, вы формируете новую “сессию”: записываете новые и измененные файлы и каталоги в свободное место, создаете новую версию корневого каталога, ссылающуюся на актуальную структуру, и записываете указатель на этот новый корневой каталог. При чтении такого диска система должна найти последний записанный корневой каталог и работать с ним. Старые сессии остаются физически на диске, но игнорируются.

Этот подход несколько расточителен по месту, но для CD-ROM это было приемлемо. И главный бонус: если во время записи новой сессии что-то пойдет не так (например, отключится питание), старые сессии останутся нетронутыми. Данные предыдущей сессии не будут повреждены. Это обеспечивает консистентность.

И вот эта идея – никогда не перезаписывать данные на месте, а всегда писать изменения в новое место и затем атомарно обновлять указатель на “вершину” структуры (корневой каталог) – легла в основу Log-structured или Copy-on-Write (COW) файловых систем.

Первой популярной коммерческой реализацией была система WAFL (Write Anywhere File Layout) от компании NetApp. Они запатентовали свои решения, и долгое время это мешало появлению аналогичных открытых систем.

Принцип работы COW ФС (на примере ZFS/BTRFS, хотя детали могут отличаться): Вся файловая система представляет собой дерево блоков. Есть некий “корневой узел” (root node), который указывает на блоки, описывающие таблицу инодов (или её эквивалент). Сами иноды, в свою очередь, указывают на блоки данных файлов (тоже часто через дерево указателей, а не экстенты, так как экстенты плохо сочетаются с COW).

Когда вы меняете хотя бы один байт в блоке данных, происходит следующее:

  1. Система выделяет новый свободный блок на диске.
  2. Измененный блок данных записывается в этот новый блок. Старый блок остается нетронутым.
  3. Теперь нужно обновить указатель в родительском узле (например, в иноде или в блоке косвенных указателей), чтобы он указывал на новый блок вместо старого. Но мы не можем перезаписать родительский узел!
  4. Поэтому мы создаем новую копию родительского узла, записываем её в новое свободное место, изменив в ней нужный указатель.
  5. Этот процесс (copy-on-write) рекурсивно распространяется вверх по дереву до самого корневого узла. В результате у нас формируется новое дерево, частично совпадающее со старым (неизмененные ветви), а частично состоящее из новых блоков.
  6. В конце всей операции (которая может включать изменения во многих файлах) система атомарно записывает указатель на новый корневой узел в специальное место на диске (суперблок или его аналог). Теперь этот новый узел становится текущим состоянием файловой системы. Старый корневой узел и все блоки, на которые он ссылался, но которые не вошли в новое дерево, становятся кандидатами на удаление (если на них не ссылаются снимки).

Создавать новое состояние всей файловой системы (новый корневой узел) при изменении каждого сектора было бы слишком дорого. Поэтому изменения накапливаются пачками (транзакциями). Но если система упадет до записи нового корневого узла, все накопленные изменения потеряются. Это тоже плохо.

Поэтому COW-системы тоже используют журнал, но совсем другого типа! В этот журнал просто последовательно пишутся все операции записи данных и метаданных, которые должны войти в следующую “пачку”. Параллельно в памяти строится новое дерево блоков (зелененькое на картинке). Когда журнал достигает определенного размера или проходит время, система:

  1. Гарантирует, что все данные из журнала записаны на диск.
  2. Записывает на диск новый корневой узел, указывающий на только что построенное дерево.
  3. После этого секция журнала, соответствующая этой пачке, становится ненужной и может быть очищена (её данные уже интегрированы в основное дерево).

При восстановлении после сбоя система смотрит на последние записанные корневые узлы и журнал. Если последняя транзакция не была завершена записью корневого узла, она может быть “накачена” из журнала.

Простейшая форма такой системы – ReFS (Resilient File System) от Microsoft (появилась в Windows Server 2012). Она использует всего два корневых узла, которые пинг-понгом сменяют друг друга, похоже на копирующий сборщик мусора. ReFS позиционируется как система, хорошо работающая с очень большими файлами (например, образами виртуальных машин).

Но два корневых узла – это скучно. Главная “фишка” COW-архитектуры – это возможность легко и дешево реализовывать моментальные снимки (snapshots). Каждый записанный корневой узел – это, по сути, и есть снимок состояния файловой системы на определенный момент времени. Если мы просто не будем удалять старые корневые узлы и связанные с ними уникальные блоки данных, мы получим историю состояний.

Системы вроде ZFS (изначально от Sun/Oracle, есть и для Linux) и BTRFS (Linux GPL) развивают эту идею:

  • Они позволяют создавать множество снимков.
  • Снимки могут быть доступны не только на чтение, но и на запись (превращаясь в клоны или ветки, похожие на Git, но без возможности слияния (merge)).
  • Можно клонировать не всю ФС, а отдельные каталоги или файлы. Это очень удобно для создания копий виртуальных машин или контейнеров: клон создается мгновенно и не занимает места, пока вы не начнете вносить в него изменения.

Основная сложность в системах с множеством снимков – это управление свободным пространством. Блок считается свободным, только если на него не ссылается ни один активный снимок (или текущее состояние). Когда вы удаляете старый снимок, нужно определить, какие блоки данных освободились (т.е. на них больше никто не ссылается), а какие еще используются другими снимками. Это нетривиальная задача. NetApp запатентовал свою эффективную механику управления свободным пространством (используя битмапы для каждого снимка и операции над ними в памяти). ZFS и BTRFS используют свои, более сложные подходы.

Преимущества COW-систем (ZFS, BTRFS):

  • Надежность: Старые данные никогда не перезаписываются, что снижает вероятность их потери при сбое. Восстановление обычно очень быстрое.
  • Снимки и клоны: Встроенная, дешевая и эффективная поддержка снимков и клонов для бэкапа, версионирования, тестирования.
  • Целостность данных: Часто включают контрольные суммы для данных и метаданных, что позволяет обнаруживать “тихие” повреждения данных на диске.
  • Гибкое управление томами: Интегрируют управление логическими томами (как LVM) и файловой системой.

Недостатки COW-систем:

  • Фрагментация: Постоянное выделение новых блоков может приводить к сильной фрагментации как данных, так и свободного пространства.
  • Производительность записи: Производительность записи может быть ниже, чем у лучших журнальных ФС, особенно при случайной записи и сильной фрагментации.
  • Требовательность к памяти: Часто требуют больше оперативной памяти для хранения метаданных (особенно для управления свободным пространством при наличии множества снимков).
  • Сложность управления свободным пространством: Удаление больших объемов данных или старых снимков может быть медленной операцией (особенно в BTRFS есть известные проблемы с “возвратом” свободного места).

Пример использования снимков: В Solaris при обновлении ОС система автоматически создает “boot environment” – снимок корневой файловой системы до начала обновления. Обновление ставится в этот клон. Загрузчик настраивается так, чтобы можно было выбрать, с какой версии (старой или новой) загрузиться. Если после обновления что-то пошло не так, можно легко загрузиться со старого снимка.

Лицензирование: ZFS имеет лицензию CDDL, несовместимую с GPL ядра Linux, поэтому его не включают в основное ядро (хотя модули можно собрать и установить отдельно). BTRFS имеет лицензию GPL и входит в ядро Linux.

Дедупликация: Некоторые COW-системы (ZFS, BTRFS) пытаются реализовать дедупликацию данных: поиск одинаковых блоков данных, хранящихся в разных файлах или снимках, и хранение только одной копии. Это “священный грааль” систем хранения данных, но эффективная и быстрая дедупликация “на лету” – очень сложная задача, и существующие реализации часто имеют проблемы с производительностью.


Вопросы из аудитории:

— Вы говорили, были две причины, почему в [журнальных] транзакциях используют только метаданные? Вы назвали только одну (про границы транзакций).

Да, спасибо, что заметили. Вторая причина – производительность и объем. Стоимость транзакции (объем записи в журнал и время обработки) пропорциональна объему данных. Метаданные обычно занимают гораздо меньше места, чем пользовательские данные. Включение пользовательских данных в транзакции ФС сделало бы их намного “тяжелее” и медленнее. COW-системы, по сути, так и делают (пишут все измененные блоки), но у них другая архитектура.

— А как всё-таки помечается свободное место [в COW-системах со снимками]?

Хранится отдельная структура данных (или несколько) для описания занятого/свободного места. В NetApp WAFL это были битмапы для каждого снимка. При работе системы (например, при выделении нового блока или удалении снимка) выполняется логическая операция (например, OR для битмапов занятого пространства всех активных снимков), чтобы получить актуальную карту реально свободного места. Эта операция может выполняться в памяти, но она требует ресурсов (памяти и процессорного времени), особенно при большом количестве снимков. Именно поэтому управление свободным пространством может быть дорогой операцией.

— Можно про флаги у журнала: commit и sync? Commit происходит после того, как все операции описаны в журнале?

Да, в контексте redo-журнала: commit – это специальная запись в журнале, которая означает, что полное описание всех операций данной транзакции было успешно записано в журнал. В этот момент сами операции над основными данными могли еще даже не начинаться.

— А sync – на одну транзакцию или на каждую операцию?

Sync (или чекпойнт) – это отметка, показывающая, что все операции из журнала до этой точки были успешно применены (“накачены”) к основным структурам данных. Sync делается не обязательно по границам транзакций. Он может покрывать несколько транзакций или даже часть операций одной транзакции (если операции идемпотентны). При восстановлении система использует последнюю отметку sync, чтобы понять, с какого места журнала нужно начать повторное применение (redo) операций из коммитированных, но еще не полностью накаченных транзакций.


(Конец лекции, шум удален)