Восстановление ZFS: как вернуть случайно удаленный Zvol
В один из будних дней раздался звонок из компании, специализирующейся на системном администрировании. Собеседник сразу поинтересовался, доводилось ли нам сталкиваться с ZFS. Разумеется, мы ответили, что у нас имеются собственные наработки в этой сфере, не говоря уже о стандартных программно-аппаратных комплексах вроде PC-3000, где базовый функционал работы с данной файловой системой заложен изначально.
Успешно пройдя импровизированное тестирование на знание архитектуры ZFS, мы спустя некоторое время приняли в работу пару твердотельных накопителей Samsung 870 EVO объемом по 2 ТБ, объединенных в зеркальный RAID-массив (Mirror). Изначально вводные данные сводились к тому, что оба SSD исправны, однако один из них по каким-то причинам выпал из пула. Главной же неприятностью стало случайное удаление критически важного Zvol (блочного устройства ZFS, применяемого в качестве виртуального диска). Заказчик пояснил, что пытался откатить состояние пула к более раннему uberblock, чтобы извлечь снимок (snapshot) эпохи, когда этот объект еще существовал, но затея не увенчалась успехом. Единственная имеющаяся резервная копия датировалась апрелем текущего года.
Проведя первичную диагностику накопителей, мы быстро установили, что первый SSD ушел в аварийную блокировку, а второй, хоть и определялся штатно, изобиловал нечитаемыми секторами. Задействовав Data Extractor, мы провели стандартную процедуру вычитывания проблемных носителей, сформировав максимально полные посекторные копии для дальнейшего углубленного реверс-инжиниринга.

Стандартный синтаксический разбор метаданных ZFS силами PC-3000 не выявил никаких следов удаленного тома в актуальной версии файловой системы. Попытки перебрать альтернативные пары UB/MOS (uberblock и Meta Object Set) также не принесли результатов. Второй же диск, ранее исключенный из массива, не представлял никакой ценности, поскольку содержал двухлетнюю давность информации.
Мы честно предупредили клиента, что привычными методами восстановить структурированные данные не получится. Теоретически можно было построить карту неразмеченной области и запустить сканирование по регулярным выражениям для поиска разрозненных файлов. Однако эффективность такого подхода заведомо стремилась к нулю: ZFS славится колоссальным уровнем фрагментации, усугубляемым активным применением компрессии LZ4.
Пробный поиск по сигнатурам и шаблонам закономерно продемонстрировал удручающую статистику: несмотря на огромное число потенциальных кандидатов, процент документов, прошедших строгую валидацию структуры, оказался микроскопическим.

Согласовав с заказчиком нестандартный сценарий, мы решили задействовать собственные наработки: обернуть наши консольные утилиты в графический интерфейс и объединить их в единый конвейер обработки, что могло качественно изменить исход дела.
Разумеется, начали мы с проверки готовых инструментов, позволяющих оценить состояние файловой структуры и поискать уцелевшие фрагменты метаданных. Мы последовательно изучили цепочки uberblock/MOS, пытаясь выявить расхождения между снимками и извлечь из них хоть какую-то пользу.

Следующим шагом стал поиск структур deadlists, но детальные проверки показали, что они абсолютно пусты.
На этом этапе традиционные методы восстановления ZFS исчерпали себя полностью. Метаданные стерты, информация зажата алгоритмами упаковки, а вспомогательные указатели, описывающие дислокацию сжатых блоков в незанятом пространстве, канули в лету.
Дальнейший глубокий анализ уцелевших сжатых блоков в пределах пула продемонстрировал отсутствие жестких статических сигнатур, из-за чего классический поиск по регулярным выражениям оказался невозможен. Тем не менее, многолетний опыт разработки софта для восстановления данных подсказывает: сжатые потоки никогда не бывают абсолютным хаосом, в них всегда прослеживаются определенные закономерности.
В случае с алгоритмом LZ4 в среде ZFS имеется характерный маркер — первые 4 байта содержат размер сжатых данных (Csize) в формате big endian. Это не константа, а динамическое значение, за которым сразу же следует сам упакованный поток байтов.
Учитывая, что Zvol представляет собой иерархическую блочную структуру, где конечный узел L0 (описанный в косвенных блоках верхнего уровня L1) имеет фиксированный размер, мы обратились к архивной копии. Выяснилось, что размер блока утерянного Zvol составлял 64 КБ.
Минимальная грануляция размещения данных задается параметром ashift конкретного пула, прописанным в структуре vdev. Точкой отсчета для адресации в ZFS служит смещение до начала тома, складывающееся из LBA 0, двух резервных копий vdev/UB_rings, загрузочной зоны и технических отступов. Сразу за ними начинается нулевая координата для DVA-адресов.
В нашем случае параметр ashift равнялся 12 (двойка в двенадцатой степени), что соответствовало минимальному блоку записи в 4096 байт. Отсюда следовало логичное заключение: сканировать пул на предмет признаков сжатых массивов нужно строго с этим шагом от нулевой точки. Такой подход гарантировал полноту охвата при условии корректной верификации маркеров.
Первая проба пера заключалась в передаче проверяемых блоков стандартным распаковщикам с целью вычисления Usize (размера распакованных данных) или получения ошибки. Эксперимент обернулся колоссальной нагрузкой на центральный процессор и лавиной ложноположительных срабатываний из-за шага в 4 КБ. Пришлось внедрять дополнительные фильтры для отсечения мусора, не похожего на упакованные потоки. Это несколько ускорило процесс, но общая производительность все еще оставляла желать лучшего.
Тогда было принято решение написать собственный анализатор LZ4-потоков для быстрого подсчета Usize. Как только вычисленное значение выходило за рамки допустимых лимитов, алгоритм мгновенно прекращал обработку и отбраковывал блок. Данная оптимизация позволила достичь приемлемой скорости сканирования свободного пространства и локализовать все сжатые фрагменты.

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

На достигнутом мы решили не останавливаться, так как в неразмеченной зоне обнаружилось множество объектов DMU (Data Management Unit) и разноуровневых Indirect-блоков. Обилие структур, отличных по размеру от стандартных блоков Zvol, представляло прекрасный плацдарм для дальнейших изысканий.
Нам потребовалось спроектировать специализированный анализатор для поиска DMU и косвенных блоков ZFS. Несмотря на отсутствие жестких сигнатур, поля внутри DMU подчиняются строгим правилам логической согласованности, равно как и указатели blkptr в Indirect-структурах. Сравнивая их с ограничениями конкретного пула, можно безошибочно отличать служебные метаданные от случайного цифрового шума. Тюнинг валидатора потребовал немалых усилий, но в итоге мы добились отсесечения 99,999% посторонних данных без единого ложноотрицательного срабатывания на тестовых выборках, содержащих миллионы случайно сгенерированных DMU.
Следующим шагом стало обучение сканера распознавать сжатые блоки, распаковывать их, классифицировать типы контента и формировать единый массив. Особенность ZFS заключается в том, что при активной компрессии часть метаданных упаковывается, а часть остается в исходном виде, поэтому анализа одной лишь сжатой зоны было недостаточно.

Обнаружив почти 33 миллиона DMU-объектов, мы сосредоточились на типах 23 (DMU_OT_ZVOL) и 24 (DMU_OT_ZVOL_PROP), которых насчиталось свыше 490 тысяч. Ситуацию осложняло присутствие в пуле другого Zvol, чья емкость в точности совпадала с искомой.
Со слов клиента, пропавший виртуальный диск занимал около 300 ГБ. Этот ориентир позволил отсеять свыше 95% из полумиллиона вариантов, выявив записи с подходящим объемом. Однако разворачивать тысячи образов по 300 ГБ было бессмысленно из-за колоссальных временных затрат и неизвестного состояния их целостности.
Мы пошли другим путем, написав парсер blkptr и реализовав сквозной проход от DMU через иерархию Indirect-блоков уровней L1 и L2 к верхушке дерева L3, вплоть до стартового блока L0. Процедура не требовала интенсивного чтения с носителя и позволяла за мгновения вытягивать первые 64 КБ из каждой вариации Zvol. Часть цепочек оказалась безвозвратно затерта, но часть осталась доступной. В результате мы даже смогли разглядеть MBR и таблицу разделов виртуального диска.
Радость омрачилась тем фактом, что по номеру транзакции (смещение 0x1B8) найденные структуры принадлежали существующему в данный момент тому. Следовательно, начальные сектора удаленного диска по классическому пути через DMU обнаружить уже не удавалось.
Далее предстояло самое сложное — глубокий анализ Indirect-блоков, оценка их иерархических взаимосвязей и целостности. Задача усугублялась огромными объемами: распакованный вид нужных косвенных блоков превышал 300 ГБ, что делало невозможным их единовременную загрузку в оперативную память для детального изучения.
Напомним принципы хранения данных в ZFS. Крупный объект, не помещающийся в единственный указатель blkptr внутри DMU, получает в свое распоряжение Indirect-блок, содержащий массив записей blkptr для дочерних элементов.
L0 — это сам упакованный блок данных.
L1 — промежуточный блок, чей массив blkptr хранит DVA-адреса, размеры и служебные флаги блоков L0.
В нашем случае распакованный Indirect-блок занимает 128 КБ. Каждая запись blkptr весит 128 байт, то есть блок L1 вмещает 1024 дескриптора L0. Учитывая, что размер распакованного L0 равен 64 КБ, полностью заполненный L1 описывает 64 МБ пространства Zvol. Блок L2, в свою очередь, ссылается на тысячу блоков L1, покрывая уже 64 ГБ. Для оцифровки 300-гигабайтного тома требуются блоки L3, объединяющие группы L2 (разумеется, при условии его плотного заполнения данными).
Поскольку клиент хранил на пропавшем томе более 200 ГБ, дерево блоков обещало быть развесистым. Не имея точного maxblk без DMU, мы оценили структуру эмпирически: для L3 требовалось 5 блоков L2, а остальные слоты оставались пустыми. Расчет общего числа блоков L0 выглядел так: 5 × 1024 × 1024 = 5 242 880 (реальное значение оказалось чуть ниже, а обрезку нулевого хвоста мы оставили напоследок).
Из-за огромного массива Indirect-блоков возникла острая необходимость в жесткой фильтрации и проверке целостности. Для верификации блока L(x) мы анализировали все его blkptr: для уровней L2 и выше проверялось, действительно ли по указанным адресам располагается подчиненный блок L(x-1). Также контролировались номера транзакций (TxG birth) — номер в дочернем указателе не мог превышать родительский. Это позволило отсекать мусор гораздо быстрее, чем через ресурсоемкий подсчет контрольных сумм fletcher, ведь перезапись блока более ранней транзакцией в архитектуре ZFS технически невозможна.

Завершив экспресс-тест на валидность, мы приступили к генерации карт уровней L2 и L1. Полученные 5 блоков L2 образовали карту из 5120 элементов, позволившую с высокой точностью сопоставлять поврежденные структуры и вычислять процент схожих указателей на L1. Поскольку львиная доля данных на виртуальном диске была статичной, в косвенных блоках это выглядело как зеркально повторяющиеся DVA-адреса. Данный маркер помог окончательно отделить чужие структуры от искомого тома.
Попытки построить карты напрямую из L3-блоков давали удручающий результат: в лучшем случае удавалось собрать половину тома, так как новейшие версии таблиц оказались частично затерты спустя всего 4 дня после удаления. И тем не менее, философия Copy-on-Write (CoW) в ZFS сыграла нам на руку: каждая новая модификация записывалась на новое место, а старые данные оставались в свободных пулах до момента физической перезаписи, сохраняя фрагменты прежних состояний.
Реализовав функцию мержа нескольких карт с привязкой к максимальному TxG, мы проверили гипотезу о суммарном обогащении данных. Расчет оправдался: «белых пятен» в структуре стало значительно меньше. Склеивать тысячи карт вручную было безумием, поэтому мы написали алгоритм комбинаторной сборки. Он показал неплохую эффективность, хотя процент потерь все еще требовал оптимизации.

Глубокий анализ процесса сборки показал, что при высокоуровневом картографировании мы упускаем критически важные детали на подуровнях L1 и L0, где и происходили основные утраты. Проблему решили внедрением версионирования. На этапе формирования карты объектов L0 сборщик получал не просто единственный блок по старшему номеру транзакции: если актуальный Indirect-указывал на затертый или битый L1, система автоматически подтягивала версию из более ранней транзакции. Благодаря этому карта L0 приросла сотнями тысяч восстановленных блоков.
Финальным аккордом стал экспорт блоков L0 в целевой образ виртуального диска с обязательной проверкой контрольных сумм fletcher, дабы исключить подмешивание постороннего мусора. При обнаружении коллизий алгоритм возвращался к архивным версиям в поисках нетронутых фрагментов.

Удаленный Zvol удалось реанимировать частично: мы вернули порядка 75–80% исправных файлов с сохранением оригинального каталожного древа, что для столь запущенного случая можно считать блестящим результатом.
Резюмируя сказанное, отметим, что задачи по восстановлению информации бывают невероятно многогранными. Фундаментальное понимание внутренней архитектуры файловой системы в сочетании с научно-исследовательским подходом позволяют добиваться успехов там, где стандартные утилиты расписываются в бессилии.
Ну и, конечно же, аксиома, не теряющая актуальности: регулярно создавайте резервные копии и подходите к удалению ценных данных с максимальной осмотрительностью.
Acer выпустила ультралегкий ноутбук Swift Blade 14 с OLED-экраном и чипом Core 7
Анонсирован тонкий Huawei nova 16s с батареей на 7000 мАч и тремя камерами по 50 Мп
Xiaomi 17 Max с батареей на 8000 мАч и камерой 200 Мп возглавил рейтинг довольных пользователей AnTuTu
Анонсирован портативный игровой ПК Acer Predator Atlas 7 с 7-дюймовым экраном
Acer представила Project DualPlay Mini: уникальный портативный игровой трансформер, превращающийся в ноутбук
Представлен неубиваемый ноутбук Acer Vero 16 с OLED 3K, сменными АКБ и SSD, Arc B390 и 24 ГБ ОЗУ
Acer представила портативный монитор PD163Q P3 с четырьмя экранами
В России начались продажи Poco F9 Ultra и F9 Pro: 185 Гц, 200 Мп, 8050 мА·ч, 100 Вт и IP68