MAATRIX / Блог / zfs send и receive: репликация на другой сервер

zfs send и receive: репликация на другой сервер

MAATRIX

Локальный ZFS-снапшот — это откат «я случайно удалил файл пять минут назад», а не защита от того, что сервер сгорит, у хостера отвалится диск или кто-то по ошибке снесёт весь пул. Снапшот живёт в том же пуле, что и данные, и погибает вместе с ним. Пара zfs send и zfs receive решает именно эту проблему: превращает снапшот в поток байт, который можно передать по сети на физически другой сервер и там развернуть в полноценный датасет. Дальше — как это устроено, какие команды реально работают и на каких граблях спотыкаются в первую неделю.

Как устроены zfs send и zfs receive — принцип потока данных

zfs send не копирует файлы по одному и не архивирует директорию — он сериализует внутреннее состояние ZFS-датасета на уровне блоков, как оно зафиксировано в снапшоте. На выходе получается бинарный поток, который можно куда угодно перенаправить: в файл, в ssh, в nc, через пайп в любую программу-посредник. zfs receive на другой стороне читает этот поток и восстанавливает из него датасет — с теми же данными, тем же деревом снапшотов внутри потока и (при необходимости) теми же ZFS-свойствами (compression, recordsize и так далее, если использован флаг -p).

Важно понимать разницу между двумя режимами:

  • Полная (full) отправка — сериализуется всё содержимое снапшота целиком, от начала до конца. Так делается первая передача.
  • Инкрементальная (incremental) отправка — сериализуется только разница между двумя снапшотами одного датасета. Передаётся не весь массив данных, а только изменённые блоки с момента предыдущего снапшота.

Это устроено похоже на то, как локальный снапшот вообще фиксирует состояние пула — если хотите разобраться в механике снапшотов подробнее, у нас есть отдельный разбор про то, как устроены снапшоты в Proxmox — принцип copy-on-write там объясняется на уровне, который прямо переносится и на голый ZFS.

Ключевой момент: zfs send сам по себе ничего никуда не передаёт по сети — это просто генератор потока в stdout. Транспорт вы выбираете сами, и в 95% случаев это ssh, потому что он даёт шифрование и аутентификацию бесплатно, без отдельной настройки VPN или голого TCP-сокета.

Первая полная передача: разворачиваем датасет на другом сервере

Предположим, у вас есть пул pool с датасетом pool/dataset на исходном сервере, и пул backup-pool на удалённом сервере, к которому вы уже настроили доступ по SSH-ключу (без пароля — это обязательное условие для автоматизации).

Шаг 1 — создаём снапшот на источнике:

zfs snapshot pool/dataset@snap1

Шаг 2 — отправляем его на удалённый сервер одной командой:

zfs send pool/dataset@snap1 | ssh remote-host zfs receive backup-pool/dataset

Что здесь происходит: zfs send читает снапшот pool/dataset@snap1 и пишет бинарный поток в stdout. Этот поток по пайпу уходит в ssh remote-host, где на другом конце выполняется zfs receive backup-pool/dataset, читающий поток из своего stdin и материализующий его в новый датасет backup-pool/dataset — с тем же содержимым и тем же снапшотом @snap1 внутри.

После этой команды на удалённом сервере появляется полноценный, смонтируемый (если не указано иное) датасет — не архив, а именно ZFS-датасет с собственными свойствами. Проверить результат:

ssh remote-host zfs list -t snapshot backup-pool/dataset

Для первой передачи большого датасета это может занять часы — передаётся весь объём данных целиком. Это разовая операция, но именно она задаёт «базовую точку», от которой дальше пойдут инкременты.

Нужен сервер под эту задачу?

Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.

Арендовать сервер

Инкрементальная передача: разница между двумя снапшотами

Гонять полный объём каждый раз — бессмысленно и медленно. Для регулярной репликации нужна разница. Делаем новый снапшот на источнике:

zfs snapshot pool/dataset@snap2

И отправляем не весь датасет, а только изменения между @snap1 и @snap2:

zfs send -i pool/dataset@snap1 pool/dataset@snap2 | ssh remote-host zfs receive backup-pool/dataset

Флаг -i говорит zfs send сериализовать не содержимое @snap2 целиком, а только блоки, изменившиеся с момента @snap1. zfs receive на удалённой стороне применяет эту разницу поверх уже существующего backup-pool/dataset@snap1 — и в результате там появляется backup-pool/dataset@snap2, идентичный источнику, но переданный за долю времени и трафика по сравнению с полной отправкой.

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

Обязательное условие инкремента: базовый снапшот должен существовать на обеих сторонах

Это правило часто ломает репликацию у тех, кто не продумал политику хранения снапшотов заранее: для инкрементальной отправки базовый снапшот (@snap1 в примере выше) обязан существовать одновременно и на источнике, и уже быть принят на приёмнике в момент отправки. zfs send -i строит diff относительно конкретного снапшота — если его нет на отправляющей стороне (например, вы его удалили скриптом ротации, посчитав устаревшим), команда не выполнится, потому что zfs send не от чего строить разницу.

Практическое следствие: если вы автоматически чистите старые снапшоты по возрасту (zfs destroy в cron-скрипте по расписанию), нельзя удалять снапшот, который ещё служит базой для следующей инкрементальной репликации. Рабочая схема:

  • Храните на источнике минимум N+1 последних снапшотов, где N — сколько инкрементов вы планируете делать между полными пересылками.
  • Удаляйте самый старый снапшот только после того, как подтверждено, что следующий за ним снапшот успешно принят на удалённой стороне.
  • Если реплика по какой-то причине отставала несколько дней (сервер был недоступен, канал упал) — не удаляйте базовый снапшот, пока не убедитесь, что цепочка инкрементов не разорвана.

Разорванная цепочка чинится только повторной полной передачей — то есть заново тем самым долгим zfs send без -i, что для десятков-сотен гигабайт данных может занять неприемлемо много времени именно тогда, когда бэкап нужнее всего.

Автоматизация: cron-скрипт для регулярной инкрементальной репликации

Ручной запуск команд раз в месяц — это не стратегия резервного копирования, а иллюзия. Рабочая схема — периодический снапшот плюс инкрементальная отправка разницы с предыдущим, запускаемые по расписанию. Базовый вариант скрипта:

#!/bin/bash
set -euo pipefail

DATASET="pool/dataset"
REMOTE_HOST="remote-host"
REMOTE_DATASET="backup-pool/dataset"
TODAY=$(date +%Y%m%d-%H%M)
NEW_SNAP="${DATASET}@${TODAY}"

# находим предыдущий снапшот (последний, который уже реплицирован)
PREV_SNAP=$(zfs list -t snapshot -o name -s creation -H "${DATASET}" | tail -1)

# создаём новый снапшот
zfs snapshot "${NEW_SNAP}"

# отправляем разницу
zfs send -i "${PREV_SNAP}" "${NEW_SNAP}" | ssh "${REMOTE_HOST}" zfs receive "${REMOTE_DATASET}"

echo "Реплицирован снапшот ${NEW_SNAP}"

Добавляете строку в crontab -e на исходном сервере, например для ежедневного запуска в 3 ночи:

0 3 * * * /opt/scripts/zfs-replicate.sh >> /var/log/zfs-replicate.log 2>&1

Если хотите не гадать, отработал ли скрипт ночью, а получать уведомление при сбое — подключите внешний пинг вроде healthchecks.io в конец скрипта; мы отдельно разбирали, как настроить мониторинг cron-задач через healthchecks.io, схема там прямо переносится и на этот скрипт. Общие принципы настройки самого расписания — в статье про примеры настройки cron-задач, если формат */15 * * * * вам ещё не родной.

Важный нюанс продакшен-версии скрипта: логика поиска «предыдущего снапшота» выше упрощена для примера. В реальном скрипте лучше явно хранить имя последнего успешно реплицированного снапшота в отдельном файле-маркере (например, /var/lib/zfs-replicate/last-snap) и обновлять его только после успешного zfs receive, а не полагаться на «последний снапшот по времени создания» — если параллельно кто-то создаёт снапшоты вручную, логика по tail -1 сломается.

Сжатие потока на медленном канале

Между дата-центрами, особенно если один из серверов в другой стране, канал не всегда широкий. zfs send сам по себе не сжимает поток (если только на источнике не включено ZFS-свойство compress и вы не используете zfs send -c для передачи уже сжатых блоков как есть — это ускоряет передачу заранее сжатых данных, но не сжимает «на лету» несжатые). Универсальный способ — сжать поток непосредственно на транспортном уровне, через компрессию SSH:

zfs send -i pool/dataset@snap1 pool/dataset@snap2 | ssh -C remote-host zfs receive backup-pool/dataset

Флаг -C включает встроенное сжатие SSH-сессии. Это помогает на медленных или дорогих по трафику каналах и почти не помогает (а иногда даже вредит из-за накладных расходов на CPU), если данные уже сжаты на уровне ZFS свойством compression=zstd — сжимать уже сжатое бессмысленно, лишняя нагрузка на процессор без выигрыша в объёме.

Альтернатива для тех, кому нужен больший контроль над алгоритмом и уровнем сжатия — прогнать поток через zstd или pigz явно, минуя встроенное сжатие SSH:

zfs send -i pool/dataset@snap1 pool/dataset@snap2 | zstd -T0 | ssh remote-host "zstd -d | zfs receive backup-pool/dataset"

Такой вариант даёт возможность подобрать уровень сжатия под соотношение «загрузка CPU / экономия канала» под конкретную пару серверов — на быстром внутреннем канале между дата-центрами это обычно не нужно, на канале через океан или при аренде с ограниченным трафиком может ощутимо сократить время и объём передачи.

Практическая архитектура защиты: снапшот плюс репликация, а не что-то одно

Сведём это в таблицу, чтобы разница была видна сразу:

МеханизмОт чего защищаетОт чего НЕ защищает
Только локальные снапшотыСлучайное удаление файла, откат перед рискованным обновлениемОтказ диска/пула, потеря сервера, физическая катастрофа дата-центра
Только zfs send/receive без снапшотов на источнике— (без снапшотов передавать нечего)
Снапшоты + регулярная zfs send/receive репликацияВсё вышеперечисленное — и локальные ошибки, и потерю всего исходного сервераЛогическую порчу данных, которая уже попала в реплику до того, как её заметили (нужна глубина хранения снапшотов на приёмнике)

Вывод простой и не новый, но именно поэтому им часто пренебрегают: снапшот без репликации — это удобство, а не бэкап. Репликация без регулярных снапшотов — это разовая копия, которая устаревает с первого же дня. Только связка «регулярный снапшот на источнике + регулярная инкрементальная передача разницы на физически отдельный сервер» даёт вам восстановление в сценарии, когда исходный сервер целиком недоступен — сгорел диск, хостер потерял стойку, кто-то по ошибке снёс весь пул целиком. Отдельный сервер для реплики должен быть именно физически отдельным — не другим пулом на той же машине, иначе единственная точка отказа никуда не делась.

Если вы уже сравнивали разные инструменты резервного копирования и думали, ZFS-репликация — это замена им или дополнение: как правило, дополнение. Файловые бэкап-инструменты вроде тех, что разобраны в статье про Restic и BorgBackup, решают задачу версионирования файлов и удобного частичного восстановления, а zfs send/receive — задачу быстрой блочной репликации целого датасета один в один. Для инфраструктуры на ZFS разумно совмещать: репликацию на резервный сервер как основную линию защиты от катастрофы, и файловый бэкап отдельных критичных путей — как вторую линию с более гибким восстановлением по отдельным файлам.

Нужен сервер под эту задачу?

Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.

Арендовать сервер

Нужны сами нейросети для контента?

Генерируйте изображения, видео и озвучку нейросетями на falapi.io — десятки моделей в одном окне. Оплата картой РФ и по СБП.

Частые вопросы

Можно ли реплицировать несколько датасетов одной командой?

Да, флаг -R у zfs send рекурсивно захватывает датасет вместе со всеми дочерними и их снапшотами. Полезно для целых пулов с вложенной структурой, но проверяйте объём — рекурсивная передача может оказаться заметно больше, чем ожидалось.

Что делать, если передача оборвалась на середине из-за обрыва сети?

Современный OpenZFS поддерживает resumable send — на приёмнике сохраняется токен незавершённого приёма (zfs get receive_resume_token), и передачу можно продолжить с прерванного места флагом zfs send -t <token>, не начиная заново с нуля. Это отдельная от -i механика, стоит проверить поддержку в конкретной версии ZFS перед тем, как полагаться на неё в критичном сценарии.

Нужно ли останавливать сервисы перед снапшотом для консистентности?

Обычно нет — ZFS-снапшот атомарен на уровне файловой системы и фиксирует согласованное состояние на диске в момент создания. Для баз данных со своим WAL/журналом это как правило достаточно для восстановления без потери целостности, но для критичных СУБД стоит свериться с их собственными рекомендациями по бэкапу поверх снапшотов.

Чем zfs receive отличается от простого восстановления файлов из архива?

Тем, что восстанавливается не набор файлов, а весь датасет как объект ZFS — со своими снапшотами внутри потока, свойствами (при -p) и деревом. Это ближе к операции «подключить копию диска», чем к распаковке архива.

Что будет, если на приёмнике уже есть более новый снапшот, чем тот, что вы пытаетесь принять?

zfs receive откажется принимать поток, если состояние датасета на приёмнике разошлось с ожидаемой базой — потребуется либо откатить датасет на приёмнике до нужного снапшота (zfs rollback), либо начинать репликацию заново с полной передачи.

Обсудить статью, задать вопрос или начать новую тему

Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.

Перейти в сообщество →