Файловая система ZFS была разработана компанией Sun Microsystems для ОС Solaris и впоследствии перенесена на другие UNIX-системы с некоторыми ограничениями (FreeBSD 7.0, Mac OS X Leopard (Build 9a321), Linux через FUSE). Особенностями этой файловой системы являются организация всего дискового пространства в единый пул, сквозной контроль целостности данных и транзакционность.
При создании пула возникает его описание в системе (никаких физических объектов не создается), после создания описания можно добавить в пул накопители или создать одну или несколько файловых систем внутри пула, при этом фактическим размещением файлов на физических носителях внутри пула управляет драйвер ZFS, следовательно, появляется возможность динамически устанавливать и снимать ограничения на размер файловой системы.
Сквозной контроль целостности - это запись на диск контрольной суммы для каждого блока данных, причем контрольная сумма и данные хранятся максимально далеко друг от друга для снижения вероятности их совместного повреждения. Если в пуле несколько устройств, то для данных, размещенных на одном из них, контрольная сумма будет записана на другом. Контрольные суммы вычисляются не только для данных, но и для метаданных (метаданные - служебная информация, описывающая объект). При считывании любого блока вычисляется его контрольная сумма и результат сравнивается с контрольной суммой, хранящейся на носителе. В случае расхождения ошибка обнаруживается, и даже если в пуле не используется резервирование, поврежденные данные хотя бы не будут выданы за истинные.
Эта возможность реализована и в других файловых системах, но отличие ZFS состоит в том, что контрольные суммы блоков сохраняются не вместе с блоками, а в блоке адресации верхнего уровня. Контрольные суммы считаются по любому объекту: не только по блоку данных, но и по блоку адресации, и если в блоке адресации из-за сбоя при хранении оказался неверный адрес, то поврежденные данные не будут выдаваться по запросу, т.к. система определит, что адрес является неверным.
Транзакционность ZFS обеспечивается способом записи данных на диск. Операции записи группируются в транзакции, а те – в группы транзакций. Каждая транзакция, состоящая из операций записи в разные файлы, выполняется следующим образом: вначале в свободные блоки записываются данные, затем - метаданные, относящиеся к модифицированным файлам; если запись не прошла до конца, изменения в файлах не учитываются.
Такая схема называется "copy on write" (копирование при записи) - старые данные не перезаписываются, а остаются записанными в других блоках, а файл считается окончательно измененным только после изменения всех метаданных. Запись верхнего уровня метаданных - атомарная операция, представляющая собой запись одного блока на диск.
Физическое устройство ZFS достаточно сложно, а ПО состоит из семи компонентов:
1. SPA (Storage Pool Allocator)
2. DSL (Dataset and Snapshot Layer)
3. DMU (Data Management Layer)
4. ZAP (ZFS Attribute Processor)
5. ZPL (ZFS POSIX Layer)
6. ZIL (ZFS Intent Log)
7. ZVOL (ZFS Volume)
Storage Pool Allocator
Виртуальные устройства
Пул ZFS состоит из нескольких виртуальных устройств. Виртуальные устройства (vdev) бывают физическими (leaf vdev, т.е. устройства нижнего уровня) и логическими (interior vdev, "внутренние устройства"). Логические виртуальные устройства – это логические группы физических устройств.
Каждый пул имеет логическое виртуальное устройство, которое считается корневым (root). Все прямые потомки корневого устройства называются виртуальными устройствами верхнего уровня.
Метки виртуальных устройств
На каждом физическом виртуальном устройстве записывается структура размером 256 Кб, которая называется меткой виртуального устройства. Эта метка описывает устройство, на котором она записана, и все устройства, которые в этом пуле являются потомками того же виртуального устройства верхнего уровня. Метка нужна для предоставления доступа к пулу и проверке его целостности и доступности устройств в нем.
На каждом физическом виртуальном устройстве в пуле хранится четыре копии метки устройства. Когда устройство добавляется в пул, ZFS помещает две метки в начало устройства и две – в конец. Метки – единственный объект в ZFS, для которого не используется механизм копирования при записи, поэтому используется поэтапное изменение: на первом этапе на диск записываются четные копии меток, а на втором – нечетные. Такой алгоритм нужен для того, чтобы на диске в любой момент времени была правильная копия метки устройства.
Метка виртуального устройства состоит из четырех частей: 8 Кб свободного пространства, 8Кб для заголовка загрузочного блока, 112 Кб для содержимого в виде пар «имя – значение» и 128Кб для ста двадцати восьми uber-блоков. ZFS поддерживает и метки диска VTOC, и метки EFI. Последние записываются в отдельную область диска, а метки VTOC – в первые 8Кб первого раздела диска (поэтому они остаются пустыми до записи этих меток).
Следующие 8Кб занимает заголовок загрузочного блока, место для которого зарезервировано, а затем 112 Кб метки виртуального устройства заняты набором пар «имя-значение», которые описывают это виртуальное устройство и все устройства, связанные с ним. Далее располагается массив uber-блоков.
Uber-блок – часть метки, необходимая для того, чтобы получить доступ к содержимому пула. Активным в данный момент времени является uber-блок, имеющий максимальный номер группы транзакций и правильную контрольную сумму. Этот блок никогда не перезаписывается, потому что к нему должен быть постоянный доступ, а все изменения сохраняет другой элемент массива uber-блоков. При записи нового uber-блока его номер группы транзакций и метка времени увеличиваются. Uber-блок из массива для записи выбирается по алгоритму кругового выбора – все блоки составляют логическое кольцо. По кольцу циркулирует маркер, дающий право на начало записи. Получив маркер, блок либо приступает к записи данных, либо передает маркер дальше.
Uber-блок записывается с используемым в системе порядком байт. Порядок байт может быть:
- big endian, в младших адресах памяти располагаются более значимые байты, SPARC;
- little endian, в младших адресах памяти располагаются менее значимые байты, x86.
Структура uber-блока:
struct uberblock {
uint64 t ub magic;
uint64 t ub version;
uint64 t ub txg;
uint64 t ub guid sum;
uint64 t ub timestamp;
blkptr t ub rootbp;
}
;
ub_magic
64-битное целое, зависит от порядка байт: big endian - 0x00bab10c, little endian - 0x0cb1ba00.
ub_version
Нужно для указания формата, в котором хранятся данные.
ub_txg
Показывает, в пределах какой группы транзакций был записан этот uber-блок. Должно быть больше или равно значению поля txg в nvlist метки виртуального устройства.
ub_guid_sum
Во время использования пула драйвер ZFS суммирует значения GUID с каждого устройства в пуле с именем GUID в списке пар «имя-значение». Сумма сравнивается с ub_guid_sum для подтверждения доступности всех устройств в пуле.
ub_timestamp
Время записи uber-блока в секундах, отсчитанное с 1 января 1970 г. (UTC).
ub_rootbp
Структура blkptr, содержащая расположение MOS (Meta Object Set, содержит описатели всех ФС пула, их снимков и клонов).
Структура blkptr:
typedef struct blkptr {
dva_t blk_dva[3]; /128-bit Data Virtual Address/
uint64_t blk_prop; /размер, сжатие, тип, и т.д./
uint64_t blk_pad[3]; /зарезервировано/
uint64_t blk_birth; /номер группы транзакций/
uint64_t blk_fill; /fill count/
zio_cksum_t blk_cksum; /256-bit checksum/
}
Данные передаются между памятью и диском блоками. Указатель блока blckptr_t – это 128-байтная структура ZFS, применяемая для описания физического расположения блока на диске, описания блока и проверки его целостности.
Побайтовая структура указателя блока:
DVA – Data Virtual Address
Виртуальный адрес данных – это комбинация полей vdev и offset указателя блока. Каждая копия имеет свой виртуальный адрес – dva1, dva2 и т.д. По каждому из этих адресов хранятся одинаковые данные. Количество используемых виртуальных адресов называется «шириной» указателя блока - обычный указатель (1 DVA), указатель двойной (2 DVA) и тройной ширины (3 DVA).
Поле vdev (32-разрядное целое) каждого DVA в указателе блока идентифицирует виртуальное устройство, на котором хранится блок. Поле offset (63-разрядное целое) – это адрес блока на устройстве, отсчет начинается сразу после меток L0 и L1 и загрузочного блока. Vdev и offset определяют адрес блока данных. В поле offset указывается адрес в секторах (по 512 байт).
Чтобы найти смещение физического блока в байтах от начала раздела (slice), значение поля offset нужно сдвинуть влево на 9 разрядов (2^9 = 512) и затем сложить с 0x400000 (размер двух меток устройства vdev_label и загрузочного блока):
physical block address = (offset << 9) + 0x400000
Фактически значение умножается на 512 и к этому значению добавляется 4Мб.
Также представляют интерес блоки GRID (это поле зарезервировано для использования в некоторых конфигурациях Raid-Z) и GANG.
Группирующий (gang) блок содержит указатели на блоки. Такие блоки используются, если запрошенное дисковое пространство невозможно выделить непрерывным блоком. В этом случае выделяются несколько блоков с условием, чтобы их общий размер был равен запрошенному, и создается группирующий блок, в котором будут храниться указатели для этих блоков. Приложению возвращается указатель на группирующий блок. Группирующий блок идентифицируется установленным битом "G" – если его значение равно единице, то блок является группирующим.
Группирующий блок занимает 512 байт и сам содержит свою контрольную сумму. Он может содержать до 3 указателей на блоки, за которыми следует 32-разрядная контрольная сумма.
Формат группирующего блока:
typedef struct zio_gbh {
blkptr_t zg_blkptr[SPA_GBH_NBLKPTRS];
uint64_t zg_filler[SPA_GBH_FILLER];
zio_block_tail_t zg_tail.;
} zio_gbh_phys_t;
zg_blkptr - массив указателей на блоки,
zg_filler -поле заполнителя, чтобы дополнить группирующий блок до 512 байт.
typedef struct zio_block_tail {
uint64_t zbt_magic;
zio_cksum_t zbt_cksum;
}
zbt_magic - значение, завершающее блок ZIO, равное 0x210da7ab10c7a11 (zio-data-bloc-tail).
typedef zio_cksum {
uint64_t zc_word[4];
}zio_cksum_t;
zc_word - четыре 8-байтных слова, содержащих контрольную сумму группирующего блока.
В ZFS контрольная сумма вычисляется для всех данных и метаданных. Поддерживается несколько алгоритмов контрольного суммирования, такие как fletcher2, fletcher4 и SHA-256. Какой алгоритм будет использован для вычисления контрольной суммы этого блока, определяется значением 8-разрядного целого в поле cksum указателя блока (например, 8 – SHA-256, 256-bit Secure Hash Algorithm из FIPS 180-2).
ZFS поддерживает алгоритм сжатия lzjb (lossless data compression algorithm). Использовать сжатие или нет для данного блока, определяется полем comp указателя блока.
Размер блока определяется тремя полями в указателе блока: psize, lsize, and asize. Lsize (логический размер) показывает размер данных без учета сжатия, RAID-Z или наличия группирующих блоков (gang); psize - физический размер блока на диске после сжатия; asize -фактически занятое данными пространство, общий размер всех блоков, включая заголовки группирующих блоков или блоки четности RAID-Z. Если сжатие данных не включено и ZFS используется без RAID-Z, то значения lsize, asize, и psize одинаковы и равны количеству 512-байтных секторов минус один (его занимает блок).
Поле типа данных в указателе блока показывает, какой тип данных хранится в этом блоке. Всего существует 25 типов (0-24), каждому типу соответствует свое значение поля (DMU_OT_NONE – 0, DMU_OT_ACL – 18). Уровень (lvl) содержит число уровней до этого блока, т.е. количество указателей блоков, которые нужно просмотреть, чтобы получить доступ к данным в текущем блоке. Счетчик заполненных блоков содержит количество ненулевых блоков под данным указателем блоков. Это поле равно 1 для указателя на блок данных - он не имеет ни одного указателя блоков ниже себя, но имеет иное значение для указателя блоков типа DMU_OT_DNODE – в данном случае поле содержит количество свободных дескрипторов ниже этого указателя блоков. 64-битное целое поле birth txg (birth transaction) содержит номер группы транзакций, в ходе которой данный блок был занят. Три блока-заполнителя (padding) в указателе блоков зарезервированы.