Что остаётся от бэкапа, если в архиве перевернулся один бит

Формат ночной копии обычно выбирают по размеру и скорости. Автор lzip Антонио Диас Диас пишет, что данных о надёжности форматов мало: разработчики редко публикуют что-то, кроме замеров скорости и сжатия. Цена одного испорченного бита так и остаётся неизвестной.
Я упаковал одни и те же 10 МБ в девять форматов, в каждом перевернул по одному биту в 200 случайных позициях и распаковал обычной командой. С настройками по умолчанию порчу пропускал только tar.br: код 0 при испорченных файлах в 55 случаях из 200 на исходниках и в 180 из 200 на смеси с несжимаемыми данными. Остальные восемь завершились с ошибкой во всех 200 случаях, но потери у них разные: zip почти всегда терял один файл, а tar.xz обычно всё, что лежало после бита. На смеси в 44 случаях из 200 tar.xz обошёлся одним файлом.
Как я портил архивы
Наборов два, оба по 10 МБ. В text лежат 450 файлов .py из стандартной библиотеки Python. В mixed 312 файлов: по объёму половина те же исходники, половина случайные байты, которые сжимаются примерно как фото или уже сжатые данные.
Один случай выглядит так: берём копию архива, переворачиваем бит, распаковываем, запоминаем код возврата и сверяем sha256 каждого файла с манифестом. В каждом случае бит стоит на одной и той же доле длины архива у всех форматов.
tar -I xz --no-same-owner -xf copy.tar.xz -C out # и так же gzip, bzip2, zstd, lz4, brotli, lzip
unzip -qq -o copy.zip -d out
7z x -y copy.7z -oout
Дальше «молчит» значит одно: код 0, а хотя бы один файл не совпал с манифестом. Потерянным считаю файл, который испорчен или не распаковался. Контроль без порчи у всех вариантов прошёл чисто.
Безвредных битов не нашлось ни одного.
Что осталось от 450 файлов
Формат | Размер, % от исходного | Молчит, из 200 ( | Целых файлов, медиана, % | Целых файлов в худших 10% случаев, до % | Потеряно не больше 3 файлов, из 200 |
|---|---|---|---|---|---|
tar.gz | 23,3 | 0 / 0 | 98,2 | 26,4 | 77 |
tar.zst | 22,9 | 0 / 0 | 97,8 | 28,2 | 60 |
tar.lz4 | 37,1 | 0 / 0 | 85,8 | 12,9 | 0 |
tar.bz2 | 18,4 | 0 / 0 | 53,6 | 0,0 | 0 |
tar.xz | 16,8 | 0 / 0 | 58,2 | 4,9 | 1 |
tar.lz | 17,0 | 0 / 0 | 58,2 | 5,1 | 1 |
7z | 17,1 | 0 / 0 | 58,0 | 5,1 | 0 |
tar.br | 16,7 | 55 / 180 | 0,0 | 0,0 | 39 |
zip | 26,1 | 0 / 0 | 99,8 | 99,8 | 199 |
Все столбцы, кроме «Молчит», относятся к набору text. Ноль из 200 — это оценка: с доверием 95% незамеченной порчи не больше 1,9%.
tar.br: встроенные проверки пропускают порчу
В формате brotli нет поля контрольной суммы потока, и на утилите это видно. Обычно незамеченная порча касается немногих файлов: медиана 2 на text, 1 на mixed. Но бывает и хуже: в одном случае код 0 пришёл при 86 испорченных или пропавших файлах.
Проверить такой архив его же средствами не получилось. На mixed я взял 80 испорченных архивов: brotli -t и tar -tf вернули 0 на 71, хотя распакованный поток отличался от исходного во всех 80.
Когда brotli всё же замечает ошибку, остаётся пусто: все 145 отказов на text и все 20 на mixed дали ноль файлов. Поэтому медиана в таблице 0%. При обрыве потока на половине brotli тоже выдал 0 байт из 10 373 120, а gzip, xz и zstd отдали примерно половину. Похоже, на таком размере утилита не успевает ничего вывести до ошибки. В код я не смотрел, и на больших архивах это не проверял.
Если выключить контрольную сумму
Диас Диас считает необязательную проверку в xz и zstd слабым местом форматов, а спецификация LZ4 Frame разрешает декодеру игнорировать суммы. В обычной команде этого риска нет: zstd и lz4 включают сумму сами, и незамеченных у них 0 из 200. Он появляется, если её выключить при упаковке:
Вариант | Молчит, из 200 ( |
|---|---|
| 120 / 178 |
| 146 / 191 |
| 0 / 44 |
На чистом тексте xz без суммы не промолчал ни разу. Видимо, порчу сжатых данных там ловит сам декодер, а несжимаемые участки проходят мимо него. Эту причину я вывел из разницы наборов и отдельно не проверял.
У tar.lz4 сумма, наоборот, сама добавляет потери. Без суммы медиана 99,8% целых файлов, с суммой 85,8%: утилита при отказе отрезает хвост вывода. На архиве 10 МБ это почти всегда всё сверх 8 МиБ при любом размере блока, на 41 МБ потеря зависела от блока. Точной причины я не знаю. Повторная распаковка того же архива через lz4 --no-crc подняла медиану с 85,8% до 99,8%, и потерю не больше трёх файлов дали 60 случаев из 80 вместо нуля. Для zstd --no-check и xz --ignore-check то же самое почти ничего не дало: 95,8% → 96,0% и 55,8% → 55,8%.
Потеря всего или одного блока
В обсуждении на Hacker News в 2016 году один участник писал, что порча сжатого tar «may lead to the loss of everything», другой отвечал, что у gzip и bzip2 один бит стоит максимум блока. Цифр не привёл никто. Полная потеря действительно возможна, но зависит от места бита, а обещание про один блок при обычной распаковке не выполнилось.
На исходниках bzip2, xz, lzip, 7z теряют всё после бита. Число целых файлов на text почти линейно зависит от позиции: корреляция 0,96–0,99. Значит, медиана 53–58% в таблице показывает случай, когда бит попал примерно в середину архива. Корреляция не показывает, какие именно файлы пропали, поэтому я отдельно сверил это на 40 новых случаях для каждого формата. На text у всех четырёх после первого потерянного файла целых не осталось ни разу. На mixed то же у bzip2 и lzip, а у xz и 7z целые файлы после потери остались в 10 случаях из 40. В основной серии на mixed у них пострадал ровно один файл в 44 и 42 случаях из 200. У xz это те же 44 позиции, что молчали с --check=none. У bzip2 в 25 случаях из 200 не осталось ни одного файла. Утверждение про один блок при обычной распаковке не подтвердилось; bzip2recover я не проверял.
tar.gz от случайного бита всё не терял. Ни один из 200 случаев не дал ноля файлов, худший оставил 15 из 450. Полная потеря у него всё же бывает, если бит попал в начало файла: об этом ниже. Но и «один файл» здесь не гарантия: не больше трёх файлов потеряно в 77 случаях из 200 на text и в 168 из 200 на mixed, а худшие 10% случаев на text сохранили не больше 26,4%. gzip доводит поток до конца, но в 45 случаях из 80 длина выхода отличалась от исходной, в самых частых вариантах на единицы байт. Думаю, из-за этого tar теряет выравнивание и следующие заголовки, хотя напрямую я такую связь не мерил. tar.zst ведёт себя похоже.
zip почти всегда теряет один файл, потому что сжимает файлы по отдельности: ровно один файл пострадал в 199 случаях из 200 на text и в 200 из 200 на mixed. В оставшемся случае на text не распаковались 47 файлов: бит лежал в самом конце архива, на 99,88% его длины. Об этой защите говорили и на HN. Цена на исходниках: 26,1% против 16,8% у tar.xz. На mixed разница почти исчезает: 63,2% против 60,5%, а tar.gz с его 64,6% даже больше zip.
У zip есть слабое место, которое случайные позиции почти не задевают. Я перебрал биты служебных зон на архиве в 1 МБ, и на последних 32 байтах 77 битов из 256 привели к потере всего. Из 1750 проверенных битов центрального каталога 6 дали полную потерю, а 29 прошли с кодом 0 при несовпадении с манифестом. Эти 29 я не разбирал.
У потоковых форматов та же беда в начале файла: бит в первых 32 байтах давал полную потерю в 206–256 случаях из 256, у zip в 9. 7z в этой проверке оказался хрупким с обоих концов: 248 и 256 из 256.
Код возврата не показывает, какие файлы сохранились
Диас Диас предупреждает, что сумма сама может испортиться: данные целы, а декодер их отвергает. На архиве в 1 МБ это выглядит так: из 256 битов в последних 32 байтах ненулевой код при полностью целых файлах дали 256 у tar.xz, 243 у tar.lz и 131 у tar.gz. Если копия из таких не распаковалась чисто, это ещё не повод её выбрасывать: файлы могли лечь на диск целыми.
Обратный случай я не планировал. На tar.bz2 9 битов из 256 в тех же 32 байтах дали код 0 от tar -xf при испорченном bdb.py. bzip2 -t поймал все девять. В 8 случаях tar напечатал A lone zero block: порча дала блок нулей, tar счёл его концом архива и вышел с нулём. Случай редкий, на 200 случайных позициях его не было, и видел я его только на bzip2 и одном архиве.
Проверять копию лучше самим распаковщиком, а не tar -xf.
Что из этого взять в скрипт копии
Рядом с архивом держать хеш:
sha256sumменяется от любого бита. Для tar.br другой рабочей проверки я не нашёл.Не выключать сумму при упаковке:
--no-check,--no-frame-crc,--check=none.Если после порчи нужна хотя бы часть файлов, сравнивать форматы по столбцу потерь. На
mixedzip по размеру почти не проигрывал: 63,2% против 60,5% у tar.xz.Разбирать отказ по файлам. Ненулевой код бывает при целых данных, а у lz4 стоит попробовать
--no-crc.
Границы вывода: один перевёрнутый бит, два набора по 10 МБ, по 200 случаев на вариант. Частоту такой порчи на дисках и лентах я не мерил, битые секторы не проверял. Обрыв копии уже разбирали на Хабре. На другом размере архива цифры сдвинутся, особенно у lz4.
Версии: gzip 1.13, bzip2 1.0.8, xz 5.8.1, zstd 1.5.7, lz4 1.10.0, brotli 1.1.0, lzip 1.25, Zip 3.0, UnZip 6.00, 7-Zip 25.01, GNU tar 1.35, образ python:3.15-slim.
KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.