RFC (Request for Comments, Запрос на комментарии) - серия документов, публикуемая сообществом исследователей и разработчиков, руководствующихся практическими интересами, в которой описывается набор протоколов и обобщается опыт функционирования Интернет.
Восстановление ZFS-пула с помощью подручных средств. Часть 1 [2011]
Введение
Представленный материал ни в коем случае не является инструкцией по восстановлению или чем-то похожим на инструкцию. Это просто рассказ о решении одной не тривиальной, для меня, задачи. И доказательство того факта, что ZFS вполне может быть восстановлена даже когда её драйвер утверждает, что пора доставать резервную копию т.к. все данные превратились в фарш, а фарш... О том, что нельзя сделать с фаршем читайте в эпиграфе. Текст написан в неформальном стиле, что бы подчеркнуть антинаучность представленного материала.
1. Предыстория
Был RAID-Z-пул собранный из трёх дисков по 1.5 Тб фирмы X, модели Y. Работал он работал, и изредка стали появляться в логах сообщения о том, что произошла ошибка чтения или ошибка записи на один из дисков пула. ZFS, ругаясь в логи на ошибки контрольных сумм, отлично отрабатывала такие моменты и пул продолжал нормально функционировать со статусом "ONLINE". Ошибки повторялись, но были не систематичными: различные диски, различные сектора, различное время. Назвав ошибки идиопатическими (неизвестной природы) решил, что обязательно разберусь с ними, но не сейчас. Эпик фейл подкрался незаметно, но всё же наступил. В один, не самый удачный момент, после перезагрузки, файловая система не cмонтировалась.
Листинг 1.1. Развалившийся пул с почти отказавшим диском
~# zpool import
pool: storage
id: 15607890160243212464
state: FAULTED
status: The pool metadata is corrupted.
action: The pool cannot be imported due to damaged devices or data.
The pool may be active on another system, but can be imported using
the '-f' flag.
see: http://www.sun.com/msg/ZFS-8000-72
config:
storage FAULTED corrupted data
raidz1 ONLINE
ad2 ONLINE
ad4 OFFLINE
ad6 ONLINE
При попытке импорта пула, zpool(8) вообще сваливался в кору.
В логах стабильно появлялись два сообщения, свидетельствующие о том, что некоторые сектора на ad4 приказали долго жить. Как выяснилось далее, в этих секторах начинались важные для ZFS структуры данных, но первые несколько килобайт этой структуры, удачным образом, не использовались и были помечены в спецификации, как "Blank space" (пустое место).
Примечание. Здесь и далее под килобайтом понимается 1024 байта, размер сектора так же стандартный, т.е. 512 байт.
Попробовал прочитать эти сектора и убедился в том, что они работать не будут.
Листинг 1.4. Чтение сбойных секторов "в ручную"
~# dd if=/dev/ad4 of=/dev/null bs=512 skip=2930275840
dd: /dev/ad4: Input/output error
0+0 records in
0+0 records out
0 bytes transferred in 2.777067 secs (0 bytes/sec)
Подключил диск такой же модели и в несколько приёмов (обходя сбойные сектора) скопировал на него содержимое глючившего диска. После чего поставил новый диск вместо глючного и попытался импортировать пул. Но, к моему разочарованию, пул отказался импортироваться и с новым диском. zpool(8) продолжал падать в кору.
Листинг 1.5. Развалившийся пул, после замены диска
~# zpool import
pool: storage
id: 15607890160243212464
state: FAULTED
status: The pool metadata is corrupted.
action: The pool cannot be imported due to damaged devices or data.
The pool may be active on another system, but can be imported using
the '-f' flag.
see: http://www.sun.com/msg/ZFS-8000-72
config:
storage FAULTED corrupted data
raidz1 ONLINE
ad2 ONLINE
ad4 ONLINE
ad6 ONLINE
Официальная документация утверждала то же, что и состояние пула - пора доставать резервную копию. Копия была, но последний раз она выполнялась чуть меньше года назад и была, мягко говоря, "неактуальной". Можно было восстановить данные из этой копии, всё лучше, чем ничего, да и не столь важны они были. Но мысль о том, что данные ещё существуют - их только необходимо извлечь не давала покоя.
2. Восстановление
2.1. Рекогносцировка местности
Внутреннее устройство ZFS я знал плохо - пришлось изучать его на форсаже, с прицелом на возможность восстановления данных. После недолгих поисков нагуглил[1] сообщение некоего Nathan Hand, который утверждал, что смог восстановить работоспособность пула. Так же в сообщении обнаружилась ссылка на черновой вариант спецификации[2] ZFS. Настроение стало улучшаться.
2.2. Некоторые сведения об устройстве ZFS
Тем, кто знаком с устройством ZFS этот раздел будет бесполезен, тем, кто не знаком - этот раздел будет даже вреден, лучше читать оригинальную спецификацию. Но, по-традиции, несколько слов об устройстве ZFS, как понял его я.
Структура данных, с которой начинается разбор бинарного месива диска перед тем как это месиво станет навороченной файловой системой, называется метка (англ. Label). На диске хранится несколько меток в строго определённых позициях.
L0, L1, L2, L3 - метки ZFS. L0, L1 - располагаются друг за другом без пропусков в начале диска (с нулевого смещения). L2, L3 - располагаются в конце диска, но не обязательно в самых последних секторах, после них может оставаться свободное место (скорее всего из-за выравнивания на 128 Кб). Пространство между L1 и L2 занято вспомогательными структурами и самими данными, но их позиции строго не определены. Метки на одном диске должны быть строго одинаковыми. О том, почему выбрано такое количество меток и каким образом они обновляются хорошо написано в документации[2].
Структура метки достаточно проста, она состоит из 8Кб пустого пространства (англ. Blank Space), 8Кб загрузочного заголовка (англ. Boot Header), 112Кб конфигурационных данных в формате имя - значение (англ. Name/Value Pair List) и 128Кб массива структур Uberblock. Итого получается 256Кб на одну метку.
Список имя-значение, содержит следующие конфигурационные данные (перечислены самые интересные, на мой взгляд): - version - версия формата хранения; - name - имя пула; - state - состояние пула (активен/экспортирован/удалён); - txg - номер транзакции в которой выполнялась запись (будет описан далее); - pool_guid - уникальный идентификатор пула; - guid - уникальный идентификатор виртуального устройства; - vdev_tree - дерево, описывающее всю конфигурацию входящих в пул виртуальных устройств, само дерево состоит из пар имя-значение, основные поля описывающие отдельное устройство: - type - тип устройства (файл/блочное устройство/зеркало/raidz/корень); - path - имя устройства (только для файлов и блочных устройств); - guid - уникальный идентификатор описываемого виртуального устройства; - children - массив подчинённых виртуальных устройств.
Более подробное описание самих данных и ссылки на формат их хранения доступны в спецификации.
Примечание. Следует отметить, что ZFS активно оперирует таким понятием, как виртуальное устройство (англ. vdev), под которым может пониматься, как вполне реальный диск, так и абсолютно абстрактный RAID-Z массив или корень иерархии устройств.
Uberblock (перевода не придумал) по своей природе похож на Superblock UFS - он является началом всей структуры данных на диске. Почему используется целый массив блоков вместо одного? Всё просто: ZFS никогда не пишет данные поверх уже существующих, вместо этого она записывает новую структуру в новую позицию, и только потом допускает возможность модификации уже имеющихся данных (их перезапись). При чтении структуры диска (напр. после перезагрузки) просто находится запись сделанная последней и используется, остальные записи считаются неактуальными. Сам Uberblock в начале содержит пять 64-битных полей: - ub_magic - "магическое число" идентифицирующее (сигнатура) блок; - ub_version - версия формата хранения; - ub_txg - номер транзакции в которой записана данная структура; - ub_guid_sum - сумма идентификаторов устройств; - ub_timestamp - UTC метка времени.
Далее следуют указатели на подчинённые структуры. Суммарный объём структуры равен 1Кб. С учётом того, что массив имеет объём 128Кб получаем, что в каждой метке может храниться 128 Uberblock'ов, созданных в разное время.
"Магическое число" всегда равно 0x00bab10c (oo-ba-block) на диск данное значение будет записано следующей последовательностью байт.
Таблица 2.2.1. Запись "Магического числа"
+----------------+-------------------------+
| Edians | Bytes |
+----------------+-------------------------+
| Big (x86) | 0c b1 ba 0000000000 |
| Little (Sparc) | 0000000000 ba b1 0c |
+----------------+-------------------------+
Особого внимания заслуживает поле транзакция, в котором хранится номер транзакции, по завершении которой и был записан данный Uberblock. Так же, как было отмечено ранее, номер транзакции отдельно указывается в метке. На основании этого номера и принимается решение, какой Uberblock считается активными. Активным считается Uberblock с максимальным номером транзакции, при этом номер транзакции блока должен быть больше и равен номеру транзакции метки. Если это условие не выполняется, то производится поиск блока с меньшим номером. Т.о. если при выполнении транзакции произошла ошибка и Uberblock записан не был, то ZFS сможет корректно откатиться к предыдущей транзакции.
Для просмотра содержимого меток диска используется опция "-l" утилиты zdb(8).
Дальше начинаются дебри из структур, описывающих расположение блоков на диске. Кому интересно, тот может ознакомиться с ними в документации[2].
2.3. Детальное обследование пациента
Начать было решено с простейших манипуляций - проверить читаемость и корректность меток, с помощью zdb(8).
Листинг 2.3.1. Чтение меток с дисков (показано только начало дампа)
Никакого криминала метки считались - придётся вводить в действие тяжёлую артиллерию.
2.4. Тяжёлая артиллерия
Для того, что бы было удобнее копаться в бинарном содержимом меток их необходимо было скопировать с дисков в отдельные файлы. Для этого нужно знать начала меток и их размер на диске. С размером вопросов не было - размер меток постоянен и равен 256Кб. Смещения L0 и L1 равны 0 и 256Кб соответственно. Оставалось выяснить смещения L2 и L3. Объём диска равен 1465138584Кб, вычитая размер метки, я должен был получить смещение метки L3: 1465138328Кб. Но не тут то было. Сделав дамп 256Кб от полученного смещения я решил его проверить, найдя смещение первого Uberblock'а от начала дампа. Это должно было быть ровно 128Кб или 020000h. Uberblock легко определить в куче байт по "Магическому числу", с которого он начинается, но необходимо делать поправку на порядок байт в записи числа.
Листинг 2.4.1. Неправильный выбор смещения
~# dd if=/dev/ad2 of=tmp.dump bs=1024 count=256 skip=1465138328
256+0 records in
256+0 records out
262144 bytes transferred in 0.057129 secs (4588621 bytes/sec)
~# od -t xC -A x tmp.dump | grep "0c b1 ba 00" | head -n 1
0000000 0c b1 ba 0000000000 0e 00000000000000
Промазал. Первый Uberblock в полученном дампе располагался на нулевом смещении, а это значит, что ошибка составляет минимум 128Кб. Т.к. первый Uberblock должен располагаться на смещении 128Кб от начала метки, а в дампе уже на нулевом смещении я видел один из блоков. Для определения корректного смещения было необходимо определить, сколько Uberbock'ов попало в дамп и сместиться на соответствующее количество блоков + 128Кб.
Листинг 2.4.2. Определение количества Uberblock'ов, попавших в дамп
~# dd if=/dev/ad2 of=tmp.dump bs=1024 count=256 skip=1465138328
256+0 records in
256+0 records out
262144 bytes transferred in 0.057163 secs (4585903 bytes/sec)
~# od -t xC -A x tmp.dump | grep "0c b1 ba 00" | wc -l
104
В дампе 104 блока, должно быть 128 - нам надо сместиться влево (к младшим адресам) на: (128 - 104) + 128 = 152 (Кб). Новое смещение: 1465138328 - 152 = 1465138176 (Кб). Делаем дамп и проверяем количество блоков и смещение первого из них.
Листинг 2.4.3. Проверка нового смещения
~# dd if=/dev/ad2 of=tmp.dump bs=1024 count=256 skip=1465138176
256+0 records in
256+0 records out
262144 bytes transferred in 0.057357 secs (4570387 bytes/sec)
~# od -t xC -A x tmp.dump | grep "0c b1 ba 00" | head -n 1
0020000 0c b1 ba 0000000000 0e 00000000000000
~# od -t xC -A x tmp.dump | grep "0c b1 ba 00" | wc -l
128
Ага, угадал. Смещение L2 получить элементарно - необходимо вычесть 256Кб из полученного смещения L3: 1465138176 - 256 = 1465137920 (Кб). Получилась небольшая таблица смещений. Т.к. диски одинаковой модели, то и смещения для них будут одинаковыми.
Т.к. рутинную работу лучше выполняет машина, я набросал простенький скрипт для дампа всех меток в отдельные файлы.
Листинг 2.4.4. Скрипт для дампа
#!/bin/sh
L0_OFF=0
L1_OFF=256
L2_OFF=1465137920
L3_OFF=1465138176
DISKS="ad2 ad4 ad6"
for DISK in $DISKS; do
echo "Dump labels from: $DISK"
l=0
for OFF in $L0_OFF $L1_OFF $L2_OFF $L3_OFF; do
fdump="$DISK-label$l.dump"
echo " Label: $l (out: $fdump, offset: $OFF)"
# Create output file
touch "$fdump"
# Dumping
dd if="/dev/$DISK" of="$fdump" bs=1024 count=256 skip=$OFF 2> /dev/null
l=`expr $l + 1`
done
done
После его выполнения метки были сохранены в файлы adN-labelX.dump (N - номера дисков: 2, 4, 6; X - номер метки: 0, 1, 2, 3). Разбираться стало немного удобнее.
Далее я начал сравнивать конфигурации, записанные в метках, но результата это не принесло - оставалось лезть в код реализации ZFS для того, что бы выяснить какие именно данные были повреждены.
2.5. Поиск ответов в коде реализации
Немного порывшись по коду zpool и libzfs, обнаружил следующие строки в libzfs_status.c, которые и устанавливают статус пула "The pool metadata is corrupted".
Листинг 2.5.1. Код определяющий состояние ZFS-пула
/*
* Corrupted pool metadata
*/
if (vs->vs_state == VDEV_STATE_CANT_OPEN &&
vs->vs_aux == VDEV_AUX_CORRUPT_DATA)
return (ZPOOL_STATUS_CORRUPT_POOL);
Проверяемые поля устанавливаются в модуле ядра - пришлось перебираться в ядро. После недолгих поисков был найден файл spa.c, ответственный за открытие/закрытие, а так же импорт пула. В данном файле было несколько участков кода, в которых поля устанавливались в интересующее состояние. Надо было как-то выяснить что именно не нравиться алгоритму импорта. Для этого насовал кучу отладочного вывода в модуль, пересобрал его и загрузил вместо стандартного, разрешив вывод отладочной информации. Теперь, в момент импорта пула, в логи сыпалось множество отладочной информации, показывающей ход выполнения алгоритма импорта/открытия пула. Среди прочего был найден код проверки активного Uberblock'а.
Листинг 2.5.2. Проверка активного Uberblock'а
/*
* If we weren't able to find a single valid uberblock, return failure.
*/
if (ub->ub_txg == 0) {
vdev_set_state(rvd, B_TRUE, VDEV_STATE_CANT_OPEN,
VDEV_AUX_CORRUPT_DATA);
Примечание. У zpool(8) есть специальная недокументированная опция "-F", которая позволяет импортировать даже сбойный пул. Работать после этого он конечно не будет, но для отладки и восстановления данных опция полезная.
Добавив перед проверкой вывод номера транзакции активного блока, получил номер транзакции, которую ZFS считает последней удачной. Это была 253277 транзакция.
Немного порывшись в коде, пришёл к выводу, что ошибка где-то глубже чем неправильно записанная метка. Т.к. восстанавливать всю структуру блок за блоком в мои планы не входило (пул содержал несколько сотен гигабайт не такой уж и ценной информации), я решил прибегнуть к грубой силе - заставить ZFS откатиться к предыдущей транзакции.