Ночной бэкап, который не успевает закончиться к началу рабочего дня, это не бэкап, а отложенный сбой. Упаковка сотен гигабайт gzip-ом на одном ядре давно стала узким местом конвейера: диски ждут процессор, процессор занимает единственное ядро, а остальные ядра простаивают. Связка tar с zstd меняет картину: сжатие в разы быстрее, архив получается компактнее, а многоядерные серверы наконец работают на полную.

Разберём, где теряет время gzip, какие уровни сжатия выбрать, как включить многопоточность, чем отличаются флаги tar разных версий и как выглядит готовый скрипт ночного бэкапа.

Где gzip теряет время на больших объёмах

Формат gzip и его реализация родом из 1992 года: алгоритм DEFLATE, один поток, одно ядро. На сервере с двумя десятками ядер архиватор, занимающий одно ядро, выглядит анахронизмом. При скорости порядка 30-60 МБ/с на ядро двести гигабайт рабочего каталога растянутся на час с лишним, и это только сжатие, без передачи и восстановления.

Сжатие при этом не лишнее: перекачка архива по SSH между площадками упирается в канал, и коэффициент сжатия экономит и время передачи, и место на хранилище. Вопрос лишь в том, чтобы упаковка не съедала больше, чем экономит. Рабочее окно ночного задания короткое, и каждая минута сжатия на единственном ядре отодвигает готовность копии к утру.

Отдельный вопрос, что класть в архив: каталоги содержимого, конфигурации и дампы баз, а не вся файловая система целиком, тогда объёмы ужимаются лучше, а восстановление точнее. Уже упакованные образы и медиа дают коэффициент около единицы, и их лучше просто копировать, а не жечь процессор впустую. На слабых VPS узким местом становится ввод-вывод, и прежде чем поднимать уровень сжатия, стоит замерить скорость диска.

Математика окна честная. Пусть рабочий каталог весит 300 ГБ, канал до площадки хранения даёт 10 МБ/с, а сжатие gzip-ом идёт на 40 МБ/с и ужимает содержимое вдвое. Передача займёт около четырёх часов, сжатие два, и всё это время исходники лучше не трогать. Если сжатие ускорить в четыре раза, суммарное время падает почти вдвое, и ночное окно снова становится достаточным. Именно на этом шаге проигрывает один поток: процессорная часть не масштабируется, куда ни двигай дисками и сетью.

Уровни сжатия zstd и выбор рабочего значения

Zstandard, коротко zstd, появилась в 2016 году от автора lz4 Яна Коллета и быстро добралась до репозиториев всех дистрибутивов. Уровни от 1 до 22, по умолчанию 3, а крайние уровни 21 и 22 включаются только с --ultra. Правило выбора простое: горячие логи и потоки живут на уровне 1-3, регулярные архивы на 3-10, холодное хранилище на 19-22 с длинным окном. Ориентиры по уровням: переход с 3 на 1 выигрывает в скорости примерно вдвое при небольшой потере плотности, подъём на 19 добавляет десятки процентов плотности ценой кратного времени.

zstd -b3 -e9 /tmp/catalog.tar

Уровень 9 не случайно стал компромиссом для многих: плотность почти как у глубоких режимов, время терпимое, и на серверах его часто держат для еженедельных задач. Подбирать уровень лучше прямо на своих файлах: встроенный бенчмарк zstd -b3 -e9 прогоняет уровни от 3 до 9 на конкретном архиве и показывает скорость и коэффициент каждого. Одна команда отвечает на вопрос, что даст уровень 7 в сравнении с тройкой, и выбор перестаёт быть гаданием. В выводе бенчмарка есть и скорость сжатия, и распаковки, и эти две цифры подсказывают, где именно пройдёт узкое место конвейера. Для десятков гигабайт логов с повторяющимися строками выигрыш от длинного окна --long=27 заметен сразу, для перемешанных бинарников разница меньше. Скорость на уровне 1 достигает сотен мегабайт в секунду на ядро, и для потоков в реальном времени больше и не нужно: канал ограничит раньше, чем процессор. Распаковка слабо зависит от уровня, поэтому тяжёлые режимы удорожают только запись, а чтение остаётся лёгким. Для потоков на общей машине есть режим --adapt: утилита следит за загрузкой процессора и плавно понижает уровень под нагрузкой.

Многопоточность через -T0 на многоядерных серверах

Главный выигрыш даёт параллельность. Флаг -T0 отдаёт zstd все физические ядра, и на машине с 16-32 ядрами упаковка перестаёт быть узким местом: узким местом становится диск. Память на поток растёт с уровнем, поэтому на уровне 19-22 с большим числом потоков стоит следить за свободной памятью. Внутри tar многопоточность включается передачей аргументов через -I. Замеры стоит делать на наборе в несколько гигабайт: на маленьком файле старт потоков съест весь выигрыш, и вывод будет льстить однопоточному режиму.

time tar -I "zstd -T0" -cf backup.tar.zst /srv/db
time tar -czf backup.tar.gz /srv/db

Пара запусков с time на реальном каталоге отвечает на главный вопрос и даёт локальный ориентир вместо чужих цифр. У gzip существует параллельный собрат pigz, который снимает проблему скорости, но коэффициент сжатия остаётся прежним, а zstd и сжимает плотнее, и распаковывается быстрее.

На общий сервер с чужими задачами все ядра отдавать не обязательно: -T4 ограничит упаковку четырьмя потоками и оставит соседям воздух. Приоритет усмиряют nice и ionice, а расписание запуска разводит сжатие с пиковыми часами. Правило кормления фоновых задач простое: пусть сжатие терпеливо ждёт, а не интерактивные пользователи терпят тормоза. Число потоков фиксируют и явно: -T6 на машине с восемью ядрами оставит пару ядер живым ответам и сессиям. Память под окно растёт с уровнем: на уровне 19 каждый поток держит заметный объём, шестнадцать потоков легко занимают несколько гигабайт, и на машинах со скромной памятью число потоков стоит умерить.

Сравнение времени, размера и распаковки

Типовая картина на смешанных данных: zstd на уровне 3 сжимает плотнее, чем gzip на уровне 6, при этом работает в 3-5 раз быстрее на одном ядре, а с -T0 разрыв на многопроцессорных серверах вырастает до десятков раз. Распаковка у zstd почти не зависит от уровня сжатия и идёт на скоростях порядка гигабайта в секунду на ядро: восстановление из архива перестаёт быть отдельной задачей.

Экстремальное сжатие xz даёт файлы компактнее, но платит за это на порядок большим временем упаковки. Многопоточная распаковка возможна, но выигрыш скромнее: часть работы по своей природе последовательна, и -T0 при чтении ускоряет меньше, чем при записи. Границу разумного показывает прямой замер: --ultra -22 против xz -9 на одном файле, и счёт времени обычно отправляет xz в разряд холодного хранения. Для ночных бэкапов баланс обычно лежит на zstd уровня 3-7, для годовых слепков на 19-22. Практический приём: держать два профиля в скрипте, быстрый для ежедневных копий и глубокий для воскресных, и перестать платить максимальную цену сжатия каждый день.

Для полноты картины рядом стоят lz4 и xz. lz4 на порядки быстрее, но сжимает заметно слабее, и его место это живые потоки и кэши. xz добирает плотность там, где место дороже времени. zstd закрывает весь спектр: на низких уровнях близка к lz4 по скорости, на высоких приближается к xz по плотности, и один инструмент заменяет оба.

Флаги --zstd и -I zstd в разных версиях tar

GNU tar умеет zstd напрямую с версии 1.31, вышедшей в 2019 году: флаг --zstd вызывает утилиту с настройками по умолчанию. Для старых систем и для передачи аргументов служит -I "zstd -T0 -3": tar просто запускает переданную команду как фильтр. Читает tar оба варианта одинаково, формат сжатия определяется автоматически.

tar --zstd -cf logs.tar.zst /var/log
tar -tf logs.tar.zst | head -5
tar --zstd -xf logs.tar.zst -C /restore

Список содержимого не требует распаковки, извлечение отдельного файла работает той же командой с путём внутри архива. Пайп-вариант выручает в потоковых задачах: tar -cf - каталог | zstd -T0 > archive.tar.zst, а если следующий шаг это отправка по сети, zstd встаёт между ними без промежуточного файла на диске: связка tar -cf - каталог | zstd -T0 | ssh host "cat > backup.tar.zst" сжимает и передаёт параллельно, и канал занят полезными байтами. Ссылка на уровень внутри -I означает, что уровень сжатия перестаёт быть свойством tar и становится обычным аргументом zstd, доступным в любой версии tar.

На старых дистрибутивах без --zstd работает тот же -I, и разница только в переносимости: строка с -I одинакова для старых и новых tar. Список содержимого показывает имена и размеры без полного чтения, а фактическую целостность проверяет zstd -t, читающий поток целиком. Для быстрой прикидки, что лежит внутри сотни архивов, комбинация tar -tf и grep по именам заменяет разворачивание на диск. Извлечение одного файла не тянет весь архив: tar --zstd -xf logs.tar.zst var/log/syslog достаёт конкретный путь, и на архивах с миллионами записей фильтр по пути экономит десятки минут.

Словари для мелких файлов и --long для крупных

Мелкие файлы сжимаются хуже всего: заголовки и метаданные съедают выигрыш, и тысячи конфигов в архиве дают слабое уменьшение размера. Для таких наборов zstd умеет обучать словарь: zstd --train по выборке файлов создаёт общий словарь, сжатие с -D словарём даёт кратный выигрыш на файле в килобайты, и даже сотни мелких XML сжимаются ощутимо лучше.

Для крупных файлов полезно окно побольше: --long=27 поднимает окно до 128 МБ, и повторяющиеся блоки в огромных образах сжимаются заметно плотнее. Один нюанс: при распаковке архива, сжатого с --long, тот же параметр нужен и разжимающей стороне, иначе утилита откажется читать файл. Пара флагов должна совпадать на обеих сторонах, и её стоит зафиксировать в документации к скрипту, а не держать в голове.

Тренировка словаря выглядит так: zstd --train -r samples -o dict собирает словарь по выборке, затем сжатие идёт с -D dict. Словарь размером в десятки килобайт экономит гигабайты на тысячах мелких файлов, но хранить его нужно рядом с архивом: без словаря распаковка завершится отказом. Рассчитан такой режим на файлы до десятков килобайт, крупным он не поможет, для них служит длинное окно. Для конфигурационных каталогов, где одинаковые шаблоны повторяются сотнями файлов, словарь окупается с первого прогона.

Скрипт ночного бэкапа и переход с gzip

Переход сводится к короткой проверке и правке одной строки в скрипте. Порядок внедрения выглядит так:

  1. сжать копию рабочего каталога zstd -T0 на уровне 3 и замерить время;
  2. сравнить размер с привычным gzip-архивом и с уровнем 9;
  3. подобрать рабочий уровень под окно бэкапа;
  4. заменить строку сжатия в скрипте на tar с -I "zstd -T0";
  5. восстановить архив на стенде и сверить контрольные суммы.
#!/usr/bin/env bash
set -euo pipefail
ts=$(date +%F)
tar -I "zstd -T0 -3" -cf "/backup/srv-$ts.tar.zst" -C /srv data
zstd -t "/backup/srv-$ts.tar.zst"
find /backup -name "srv-*.tar.zst" -mtime +14 -delete

Проверка zstd -t читает архив целиком и роняет скрипт на испорченном файле, ротация find -mtime убирает копии старше двух недель, а set -euo pipefail не даёт скрипту притвориться, что всё прошло хорошо. Время развёртывания замеряют на стенде: если восстановление превышает согласованное окно, уровень снижают или режут архив на части по каталогам. Два профиля реализуются функциями в скрипте: pack_fast с уровнем 3 для будней и pack_deep с уровнем 19 и --long=27 для воскресений, а расписание вешает их на разные дни. Файлы .tar.zst читаются и вне tar: zstdcat печатает поток в stdout, а tar -xOf выведет содержимое конкретного файла без сохранения на диск. Формат Zstandard стабилен и описан в RFC 8878, распаковка будет доступна в любом свежем дистрибутиве через годы, и для архивов, которые обязаны открываться долго, это весомый аргумент. Итоговое правило простое: gzip остаётся для совместимости, zstd становится вариантом по умолчанию везде, где окно сжатия и размер архива важнее привычки.

Раз в квартал стоит проводить учения: развернуть свежий архив на стенде, засечь время восстановления, сверить размеры каталогов. Проверка zstd -t ловит испорченные файлы, но не ловит ситуацию, когда в архив попали не те каталоги, и только развёртывание показывает правду. Контрольная пара du -s на источнике и распакованном каталоге закрывает вопрос с точностью до метаданных, и после этого бэкап можно называть бэкапом.