zfs send и receive: репликация на другой сервер
Локальный ZFS-снапшот — это откат «я случайно удалил файл пять минут назад», а не защита от того, что сервер сгорит, у хостера отвалится диск или кто-то по ошибке снесёт весь пул. Снапшот живёт в том же пуле, что и данные, и погибает вместе с ним. Пара zfs send и zfs receive решает именно эту проблему: превращает снапшот в поток байт, который можно передать по сети на физически другой сервер и там развернуть в полноценный датасет. Дальше — как это устроено, какие команды реально работают и на каких граблях спотыкаются в первую неделю.
Содержание
- Как устроены zfs send и zfs receive — принцип потока данных
- Первая полная передача: разворачиваем датасет на другом сервере
- Инкрементальная передача: разница между двумя снапшотами
- Обязательное условие инкремента: базовый снапшот должен существовать на обеих сторонах
- Автоматизация: cron-скрипт для регулярной инкрементальной репликации
- Сжатие потока на медленном канале
- Практическая архитектура защиты: снапшот плюс репликация, а не что-то одно
Как устроены 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 ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.
Перейти в сообщество →