Два инженера спорят о файловой системе. Один говорит, что ZFS непробиваема. Другой отвечает, что Btrfs достаточно хороша и живёт прямо в ядре. Оба правы, и оба не договаривают. Btrfs и ZFS реализуют copy-on-write принципиально разными способами, по-разному управляют памятью, по-разному считают контрольные суммы и по-разному деградируют при нехватке ресурсов. Разобраться с этим один раз стоит того, чтобы не переносить данные с одной системы на другую под давлением аварии.
Обе системы выросли из одной идеи: не перезаписывать данные на месте. Когда блок данных нужно изменить, CoW-файловая система записывает новую версию в свободное место, обновляет метаданные так, чтобы они указывали на новый блок, и только потом освобождает старый. Это делает снепшоты мгновенными, атомарные записи гарантированными, а повреждения при внезапном отключении питания куда менее опасными. Но дальше пути расходятся, и расхождения эти существенные.
Как устроено дерево метаданных в Btrfs и почему оно ломается иначе
Btrfs строит свою внутреннюю структуру на основе B-деревьев. Каждое дерево хранит определённый тип данных: дерево файловой системы содержит иноды и экстенты, дерево чанков описывает разметку физических устройств, дерево корней хранит ссылки на все подтома и снепшоты. Все деревья связаны через суперблок, который является единственной точкой входа в файловую систему.
При каждой записи Btrfs не модифицирует существующие узлы B-дерева, а создаёт новые копии изменённых узлов и всех их родителей вплоть до корня. Это называется path copying, и именно это обеспечивает атомарность: либо новый корень записан и суперблок обновлён, либо файловая система осталась в предыдущем состоянии. Незавершённая запись физически не может сделать метаданные противоречивыми.
Однако у этой схемы есть своя слабость. Btrfs поддерживает журнал транзакций отдельно от CoW-дерева, и при определённых сценариях, особенно при интенсивной синхронной записи мелких файлов, журнал и B-дерево могут рассинхронизироваться при некорректном завершении работы. Именно поэтому fsck для Btrfs существенно сложнее, чем для традиционных файловых систем, а флаг --repair используется с осторожностью.
# Проверка состояния файловой системы Btrfs без изменений
btrfs check /dev/sda1
# Показать все подтома и снепшоты
btrfs subvolume list /mnt
# Статистика ошибок на уровне устройства
btrfs device stats /mnt
Как ZFS строит пул и почему транзакционная группа меняет всё
ZFS организована принципиально иначе. Там нет понятия "раздел" или "файловая система на устройстве" в привычном смысле. Есть пул, zpool, который объединяет физические устройства в единое адресное пространство. Внутри пула создаются датасеты, каждый из которых ведёт себя как независимая файловая система со своими настройками сжатия, квотами и снепшотами.
Атомарность в ZFS обеспечивается через механизм транзакционных групп, txg. Все записи, поступившие в систему за определённый интервал времени (по умолчанию пять секунд), объединяются в одну транзакционную группу. Группа записывается на диск атомарно: либо вся группа зафиксирована, либо нет. Это означает, что ZFS никогда не оказывается в состоянии частично применённой транзакции. После сбоя питания файловая система всегда консистентна и не требует fsck.
Ключевой структурой ZFS является uberblock, хранящийся в нескольких копиях в начале каждого устройства пула. Uberblock указывает на корень дерева объектов, MOS (Meta Object Set), из которого разворачивается вся иерархия данных. При каждой фиксации транзакционной группы новый uberblock записывается в следующую ячейку кольцевого буфера, и только после успешной записи он становится активным. Это делает откат тривиальным.
# Создать зеркальный пул из двух дисков
zpool create mypool mirror /dev/sda /dev/sdb
# Проверить статус пула и здоровье устройств
zpool status mypool
# Создать датасет с включённым сжатием
zfs create -o compression=lz4 mypool/data
# Показать все свойства датасета
zfs get all mypool/data
Контрольные суммы и защита данных на уровне блоков
Обе файловые системы считают контрольные суммы для каждого блока данных, но реализуют это по-разному, и разница проявляется именно в момент, когда данные уже повреждены.
В Btrfs контрольные суммы хранятся в отдельном дереве метаданных, дереве контрольных сумм. При чтении блока его сумма вычисляется заново и сравнивается с хранимой. Если суммы не совпадают, Btrfs сообщает об ошибке. По умолчанию используется crc32c, но начиная с ядра 5.5 доступны xxhash, sha256 и blake2b. Алгоритм выбирается при создании файловой системы и не меняется впоследствии.
ZFS хранит контрольную сумму каждого блока в его родительском указателе (блоке метаданных, который на него ссылается), а не в самом блоке. Это тонкое, но принципиальное отличие: контрольная сумма и данные физически никогда не находятся в одном блоке, что исключает сценарий, при котором повреждение уничтожает и данные, и их сумму одновременно. ZFS поддерживает sha256, sha512, skein и blake3, причём алгоритм можно менять на уровне отдельного датасета.
Важнее разницы в алгоритмах то, что ZFS умеет автоматически восстанавливать повреждённые данные при наличии зеркала или RAIDZ. Обнаружив несовпадение контрольной суммы при чтении, ZFS читает ту же копию блока с другого устройства, проверяет её сумму и если она верна, молча подставляет исправную копию. Btrfs поддерживает аналогичный механизм, называемый scrub, но автоматическое исправление при чтении работает только при использовании raid1 или raid10 профилей. При одиночном устройстве ни одна из систем восстановить данные не может, однако ZFS диагностирует повреждение точнее благодаря хранению сумм в родительских указателях.
# Запустить полную проверку с исправлением ошибок в ZFS
zpool scrub mypool
# Посмотреть результат последнего scrub
zpool status -v mypool
# Запустить проверку в Btrfs
btrfs scrub start /mnt
btrfs scrub status /mnt
ARC в ZFS и page cache в Btrfs
Одно из самых заметных различий между двумя системами проявляется в управлении кэшем чтения. ZFS реализует собственный кэш, ARC (Adaptive Replacement Cache), который работает независимо от page cache ядра. ARC использует алгоритм, учитывающий как частоту обращений к блоку, так и давность последнего обращения, и динамически балансирует между двумя внутренними списками. На системах с большим объёмом RAM ARC способен занять значительную её часть, что часто вводит в заблуждение: free показывает минимум свободной памяти, хотя система работает нормально.
Btrfs не имеет собственного кэша и полностью полагается на page cache ядра Linux. Это означает, что Btrfs хорошо интегрируется с остальной подсистемой памяти: cgroups управляют page cache процессов, vm.dirty_ratio контролирует объём грязных страниц, memory pressure через PSI работает одинаково для всех файловых систем. Для контейнерных сред, где память строго делится между изолированными рабочими нагрузками, это важное преимущество.
ZFS c ARC в контейнерной среде требует аккуратной настройки: ARC живёт вне page cache и не контролируется через cgroups, что при плотной упаковке контейнеров создаёт непредсказуемое давление на общий пул памяти хоста.
# Посмотреть текущий размер ARC в ZFS
cat /proc/spl/kstat/zfs/arcstats | grep -E "^size|^c |^hits|^misses"
# Установить максимальный размер ARC вручную (в байтах, здесь 4 ГБ)
echo 4294967296 > /sys/module/zfs/parameters/zfs_arc_max
# Сделать ограничение постоянным через modprobe
echo "options zfs zfs_arc_max=4294967296" > /etc/modprobe.d/zfs.conf
Снепшоты, субтома и модель хранения
Снепшоты в обеих системах мгновенны и не копируют данные при создании, однако модель их хранения и поведение при накоплении расходятся заметно.
В Btrfs снепшот является полноценным подтомом, доступным для монтирования и записи. Разница между подтомом и снепшотом формальна: снепшот создаётся командой btrfs subvolume snapshot и изначально разделяет все экстенты с источником. При записи в снепшот задействуется CoW: изменённые экстенты копируются для снепшота, а исходные остаются нетронутыми. Глубокая вложенность снепшотов и длинные цепочки рефлинков способны замедлять операции удаления, так как ядру приходится обходить дерево ссылок.
В ZFS снепшот доступен только для чтения. Записываемая копия называется клоном и создаётся отдельной командой. Такая строгость упрощает учёт пространства: размер снепшота всегда равен объёму данных, изменённых после его создания, без двусмысленности. Удаление промежуточных снепшотов в ZFS также не вызывает проблем с производительностью, что при активной ротации снепшотов важно.
# Создать снепшот подтома в Btrfs
btrfs subvolume snapshot /mnt/data /mnt/snapshots/data_$(date +%Y%m%d)
# Создать снепшот датасета в ZFS
zfs snapshot mypool/data@$(date +%Y%m%d)
# Посмотреть список снепшотов ZFS с занятым местом
zfs list -t snapshot -o name,used,refer
# Откатиться к снепшоту в ZFS
zfs rollback mypool/data@20250101
RAID в Btrfs и VDEV в ZFS
Встроенный RAID в Btrfs долгое время был его болезненной точкой. Профили raid5 и raid6 оставались экспериментальными вплоть до ядра 5.x и по сей день несут предупреждение о возможной потере данных при определённых сценариях отказа, связанных с проблемой записи с частичными полосами. Профили raid1 и raid10 работают надёжно и рекомендуются для продуктивных систем. Существенное отличие Btrfs от ZFS состоит в том, что RAID в Btrfs работает на уровне файловой системы и не видит физической геометрии дисков, что ограничивает возможности оптимизации записи.
ZFS строит RAID через абстракцию vdev. Vdev может быть зеркалом, RAIDZ1 (аналог RAID5 с одним диском чётности), RAIDZ2 (два диска чётности) или RAIDZ3 (три диска чётности). Принципиальное отличие RAIDZ от классического RAID5 состоит в том, что ZFS использует динамическую ширину полосы: каждая полоса записи имеет ровно столько блоков данных, сколько нужно для текущей транзакции, без дозаполнения. Это полностью устраняет проблему "дыры при записи" (write hole), от которой страдает классический RAID5, и делает RAIDZ надёжным без батарейного кэша.
# Создать пул с RAIDZ2 из пяти дисков
zpool create mypool raidz2 /dev/sda /dev/sdb /dev/sdc /dev/sdd /dev/sde
# Добавить кэш чтения L2ARC на быстром SSD
zpool add mypool cache /dev/nvme0n1
# Добавить ZIL (журнал синхронной записи) на отдельном быстром устройстве
zpool add mypool log /dev/nvme1n1
# Создать файловую систему Btrfs с профилем raid1
mkfs.btrfs -d raid1 -m raid1 /dev/sda /dev/sdb
Сравнение двух систем не даёт однозначного победителя, потому что они оптимизированы под разные условия. ZFS предлагает более зрелую реализацию RAID, предсказуемое поведение при отказах, богатую экосистему управления пулами и строгую модель транзакций. Btrfs глубоко интегрирована в ядро Linux, прозрачно работает с cgroups и page cache, легко управляется стандартными инструментами дистрибутива и не требует отдельного модуля ядра. Выбор между ними начинается не с вопроса "какая лучше", а с вопроса "какая лучше подходит для конкретной нагрузки и конкретной команды, которая будет её обслуживать".