Давным-давно, в далекой-предалекой галактике, в 2009м году, я начал писать о резервировании и восстановлении ZFS: здесь и здесь.
С тех пор утекло немало воды, сам Solaris поменял место жительства. ZFS отныне поддерживается FreeBSD. Появилось множество коммерческих, тяжелых и дорогих, решений по резервированию и восстановлению. И складывается впечатление, что это очень сложно, страшно дорого и ужасно больно.
На самом деле это не так. Дабы доказать это, вытащим для всеобщего обозрения и применения финальную версию того решения, на начало которого я сослался выше и которое началось в далеком 2009м году.
Итак, сперва утилита zfsbackup. Просто и со вкусом написанная на шелле.
#!/sbin/sh
#
# ZRescue - ZFS Backup/Restore
#
# ZFS archives uses for bare-metal restore
# and systems cloning.
#
# Archive names will be incremental, like that:
# [hostname].[pool|pool%dataset].n.zfs<.gz>, n=0,1,2...
# Note: Do not rename archive files! Filenames will
# use to recovery purposes.
#
# Version 2.0 (C) 2009,2016 Y.Voinov
#
#pragma ident "@(#)zfsbackup.sh 2.0 10/22/09 YV"
#
#############
# Variables #
#############
# Environment variables
PATH=/usr/local/bin:/sbin:/usr/bin:$PATH
# Copyright and version
PKG_NAME="ZRescue"
PKG_VERSION="2.0"
COPY="Copyright (C) 2009, Yuri Voinov"
# Archive file suffix
ARC_SUFFIX=".gz"
# GZip default compression level
COMP_LEVEL="9"
# Default archive extension
ext="zfs"
# Initial archive sequence
initial_arc_seq="0"
# OS utilities
BASENAME=`which basename`
CUT=`which cut`
DATE=`which date`
ECHO=`which echo`
EXPR=`which expr`
GETOPT=`which getopt`
GZIP=`which gzip`
HOSTNAME=`which hostname`
ID=`which id`
PRINTF=`which printf`
SED=`which sed`
SSH=`which ssh`
UNAME=`which uname`
WHOAMI=`which whoami`
ZFS=`which zfs`
OS_VER=`$UNAME -r|$CUT -f2 -d"."`
OS_NAME=`$UNAME -s|$CUT -f1 -d" "`
OS_FULL=`$UNAME -sr`
# System name
system=`$HOSTNAME`
# Snapshots extension by default
# Correct it if these snapshots already exists in the system
SNAP_EXT="$system""_""$PKG_NAME""-""$PKG_VERSION""_snapshot"
###############
# Subroutines #
###############
check_os ()
{
# Check OS
$PRINTF "Checking OS... "
if [ "$OS_NAME" = "SunOS" -a "$OS_VER" -lt "10" ]; then
$ECHO "ERROR: Unsupported OS: $OS_FULL"
$ECHO "Exiting..."
exit 1
else
$ECHO "$OS_FULL"
fi
}
check_root ()
{
# Check if user root
$PRINTF "Checking super-user... "
if [ -f /usr/xpg4/bin/id ]; then
WHO=`/usr/xpg4/bin/id -n -u`
elif [ "`$ID | $CUT -f1 -d" "`" = "uid=0(root)" ]; then
WHO="root"
else
WHO=$WHOAMI
fi
if [ ! "$WHO" = "root" ]; then
$ECHO
$ECHO "ERROR: You must be super-user to run this script."
exit 1
fi
$ECHO "$WHO"
}
archive_exists ()
{
# Check archive file exist
if [ "$compress" = "1" -a -f "$file.gz" ]; then
$ECHO "1"
elif [ "$compress" = "0" -a -f "$file" ]; then
$ECHO "1"
else
$ECHO "0"
fi
}
set_file ()
{
# Check archive name exists
# and create new name if needful
remote_host_arg=$1
attempt=0
while [ "`archive_exists`" = "1" ]; do
file=`$ECHO $file|$CUT -f2 -d"."`
if [ -z "$remote_host_arg" ]; then
# Localhost file name
file="$dest/$system.$file.$attempt.$ext"
else
# Remote file name
file="$dest/$remote_host_arg.$file.$attempt.$ext"
fi
if [ "`archive_exists`" != "1" ]; then
break
fi
attempt=`expr $attempt + 1`
done
}
check_fs_exists ()
{
# Check filesystem exists
arg_fs=$1
ret=`$ZFS list -H -o name $arg_fs > /dev/null 2>&1; $ECHO $?`
if [ "$ret" != "0" ]; then
$ECHO "ERROR: ZFS pool/dataset $arg_fs does not exist."
$ECHO " Please specify another ZFS."
$ECHO "Exiting..."
exit 1
fi
}
check_dest_dir ()
{
# Check directory exist and it writable
arg_dest=$1
if [ ! -d "$arg_dest" or ! -w "$arg_dest" ]; then
$ECHO "ERROR: Directory $arg_dest does not exist"
$ECHO " or you haven't permissions to write."
$ECHO "Exiting..."
exit 1
fi
}
copyright_and_version ()
{
# Print package and copyright info
$ECHO
$ECHO "$PKG_NAME $PKG_VERSION $COPY"
$ECHO
}
usage_note ()
{
copyright_and_version
$ECHO "Usage: `$BASENAME $0` [-v] [-n] [-r] [-f] [pool|dataset] [/mntpoint|local fs] [host]"
$ECHO
$ECHO "Note: Compression will use if GZip installed,"
$ECHO " both for local archives or remote streams transfer."
$ECHO "Beware: GZip must be installed in both nodes in case of remote backup."
exit 1
}
destroy_fs ()
{
# Destroy filesystem(s) recursively
arg_fs=$1
$ZFS destroy -r $arg_fs > /dev/null 2>&1
# Check exit code
if [ "`$ECHO $?`" != "0" ]; then
$ECHO "WARNING: Filesystem $arg_fs does not exists."
fi
}
create_snap ()
{
# Create snapshot recursively
arg_filesys=$1
$ZFS snapshot -r "$arg_filesys@$SNAP_EXT"
}
zfs_send ()
{
# Send filesystem to the destination
arg_filesys=$1
arg_dest=$2
arg_host=$3
# Verbose output flag set
if [ "$verbose" = "1" ]; then
verb="v"
fi
# When use compression, send fs with archiver
if [ "$compress" = "1" ]; then
if [ "$remote_mode" = "1" -a "$remote_file_archive" != "1" ]; then
# Send from remote with compression
$SSH $arg_host "/sbin/zfs snapshot -r $arg_filesys@$SNAP_EXT &&\
/sbin/zfs send -R$verb $arg_filesys@$SNAP_EXT|/bin/gzip -c -$COMP_LEVEL"|\
$GZIP -c -d -|$ZFS receive -dF$verb $arg_dest
elif [ "$remote_mode" = "1" -a "$remote_file_archive" = "1" ]; then
# Send from remote to file with compression
$SSH $arg_host "/sbin/zfs snapshot -r $arg_filesys@$SNAP_EXT &&\
/sbin/zfs send -R$verb $arg_filesys@$SNAP_EXT|/bin/gzip -c -$COMP_LEVEL">$file$ARC_SUFFIX
else
# Send to local with compression
$ZFS send -R"$verb" "$arg_filesys@$SNAP_EXT"|$GZIP -c "-$COMP_LEVEL">"$file$ARC_SUFFIX"
fi
else
if [ "$remote_mode" = "1" -a "$remote_file_archive" != "1" ]; then
# Send from remote without compression
$SSH $arg_host "/sbin/zfs snapshot -r $arg_filesys@$SNAP_EXT &&\
/sbin/zfs send -R$verb $arg_filesys@$SNAP_EXT"|\
$ZFS receive -dF$verb $arg_dest
elif [ "$remote_mode" = "1" -a "$remote_file_archive" = "1" ]; then
# Send from remote to file without compression
$SSH $arg_host "/sbin/zfs snapshot -r $arg_filesys@$SNAP_EXT &&\
/sbin/zfs send -R$verb $arg_filesys@$SNAP_EXT">$file$ARC_SUFFIX
else
# Send to local without compression
$ZFS send -R"$verb" "$arg_filesys@$SNAP_EXT">"$file"
fi
fi
# In remote mode destroy target snapshot on local machine and on remote machine
if [ "$remote_mode" = "1" ]; then
destroy_fs "`/sbin/zfs list -H -o name -t snapshot|/bin/grep $arg_dest`"
$SSH $arg_host "/sbin/zfs destroy -r `/sbin/zfs list -H -o name -t snapshot|\
/bin/grep $arg_filesys`> /dev/null 2>&1"
fi
}
##############
# Main block #
##############
# Checking OS
check_os
# Checking root
check_root
# Check command-line arguments
if [ "x$*" = "x" ]; then
# If arguments list empty, show usage note
usage_note
else
arg_list=$*
# Parse command line
set -- `$GETOPT fFrRnNvVhH: $arg_list` || {
usage_note
}
# Read arguments
for i in $arg_list
do
case $i in
-f|-F) remote_file_archive="1";;
-r|-R) remote_mode="1";;
-n|-N) compress_off="1";;
-v|-V) verbose="1";;
-h|-H|\?) usage_note;;
*) shift
filesystem=$1
dest=$2;
remote_host=$3
break;;
esac
shift
done
# Remove trailing --
shift `$EXPR $OPTIND - 1`
fi
# Check filesystem exists
check_fs_exists $filesystem
$ECHO "*** BEGIN: ZFS backup for $filesystem at `$DATE`."
# Check archiver
if [ ! -f "$GZIP" -a ! -x "$GZIP" -o "$compress_off" = "1" ]; then
$ECHO "INFO: Compression will NOT be used. GZip not found or compression disabled."
compress="0"
elif [ -f "$GZIP" -a -x "$GZIP" -a "$compress_off" != "1" ]; then
$ECHO "INFO: Data will be compressed with gzip -$COMP_LEVEL."
compress="1"
fi
if [ "$remote_mode" != "1" ]; then
# Check destination directory when file archive operation
check_dest_dir $dest
# Set initial archive file name
# Replase slashes with % if dataset specified in non-remote mode
file="$dest/$system.`$ECHO $filesystem | $SED -e 's/\//%/g'`.$initial_arc_seq.$ext"
# Set file name if incremental naming using
# (if archive with the same name already exists)
set_file
else
# Replase slashes with % if dataset specified in non-remote mode
file="$dest/$remote_host.`$ECHO $filesystem | $SED -e 's/\//%/g'`.$initial_arc_seq.$ext"
set_file $remote_host
fi
# First destroy snapshots recursively if it exists
destroy_fs "$filesystem@$SNAP_EXT"
# Second create recursive snapshots
if [ "$remote_mode" != "1" ]; then
create_snap $filesystem
fi
# Third send all snapshots recursively to archive
zfs_send $filesystem $dest $remote_host
# Finally destroy all snapshots recursively
if [ "$remote_mode" != "1" ]; then
destroy_fs "$filesystem@$SNAP_EXT"
fi
$ECHO "*** DONE: ZFS backup for $filesystem at `$DATE`."
Небольшая цитата из readme:
Пакет предназначен для создания компрессированных резервных
копий ZFS-пулов (включая root pool) и отдельных датасетов
с целью последующего восстановления bare-metal, для целей
ординарного резервирования и восстановления и для
клонирования систем (в том числе с целью создания физических
standby-систем).
Он реализован в виде двух программ, zfsbackup и
zfsrestore, могущих использоваться как интерактивно, так
и в режиме командной строки (в том числе из cron), как
на автономных системах, так и в составе комплексных
решений.
По умолчанию, и при наличии в системе архиватора GZip,
архивы файловых систем создаются компрессированными
с максимальной степенью компрессии, для экономии
пространства хранения и уменьшения объема передачи данных.
Пакет может резервировать данные либо локально или на NFS (в
виде файлового архива, компрессированного или нет, целого
пула данных либо отдельного датасета), либо c удаленного
бэкап-сервера. Восстановление файловых систем также
выполняется либо из файлового архива, взятого с локальной
системы либо NFS, либо с удаленного сервера резервных копий.
Вы также можете использовать пакет для резерврирования
принятой файловой системы на сервере резервного копирования
далее, в файловый архив.
Также вы можете создавать бэкап удаленной файловой системы на
сервер резервного копирования непосредственно в архивный файл
на диске или ленте.
Как этим пользоваться? Снова readme:
Команда zfsbackup создает рекурсивный снапшот ZFS от
уровня иерархии файловой системы, заданной в виде аргумента
(интерактивно или в командной строке) и создает на его
основе компрессированный с максимальной степенью сжатия
архив (стрим) (если находит в системе установленный
архиватор, в данной версии GZip) на локальной точке
монтирования или на NFS.
Архив может быть распакован (в случае необходимости,
например, использования архива ZFS root pool при выполнении
flash-установок) вручную с использованием gzip или gzcat.
В удаленном режиме (опция -r) снапшот посылается посредством
SSH с заданного удаленного хоста на сервер резервного
копирования в заданную файловую систему (пул).
Стрим,посылаемый по сети, так же может быть
компрессирован или нет, как и в случае локальной архивации.
Команда zfsrestore используется при восстановлении
архивов или удаленных файловых систем, созданных
программой zfsbackup . Она может выполнять
восстановление как пулов целиком (в первоначальном
состоянии), так и отдельных датасетов, в первоначальные или
произвольно заданные файловые системы.
Команда zfsrestore распознает вид архива
(компрессированный или без компрессии) в локальном
режиме, и, в соответствие с видом архива использует
соответствующий метод восстановления. В удаленном режиме
(опция -r) ранее посланная файловая система
восстанавливается на удаленной машине.
Команды могут использоваться как на автономных системах
с периодическим сохранением резервных копий на внешних
носителяХ, так и в системах масштаба предприятия, при
использовании ленточных носителей или NFS.
Резервное копирование
---------------------
Для резервного копирования ZFS-пулов и датасетов (из любого
уровня иерархии) используется команда zfsbackup.
Команда может выполняться в двух режимах:
1. Резервирование в точку монтирования (локальную или NFS).
2. Резервирование с удаленной машины на сервер резервного
копирования.
Команда автоматически компрессирует данные (стрим)
в случае обнаружения утилиты GZip с мааксимальной степенью
компрессии (gzip -9).
Команда выполняет создание полного рекурсивного
снапшота ZFS-пула либо датасета, с последующей
автоматической компрессией и записью файла в целевую
точку монтирования (локальную либо NFS), в которую
суперпользоваль имеет право записи либо посылает сжатый
стрим с удаленной машины на резервный сервер в заданную
файловую систему или файловый архив через SSH.
По умолчанию имена архивов образуются по инкрементальному
принципу в следующем формате:
[hostname].[pool|pool%dataset].n.zfs<.gz>, n=0,1,2...
где hostname - имя хоста, на котором выполняется
архивирование, pool|dataset - имя ZFS-пула или датасета, при
этом для датасетов, с целью корректного формирования имен
файлов, осуществляется замена символов "/" на "%".
Некомпрессированные ZFS-стримы имеют расширение .zfs,
компрессированные - .gz .
Первый архив с одинаковым расширением в целевом каталоге
будет иметь номер 0, последующие, соответственно, 1,2,3
и так далее. Нумерация архивов с разными расширениям (.gz
или .zfs) будет выполняться независимо, от текущего номера
каждого типа архивов.
ВНИМАНИЕ! Не переименовывайте архивные файлы, поскольку
имена архивов используются командой zfsrestore для
восстановления в случае, если не задано имя целевой файловой
системы.
Выполнение резервного копирования производится вызовом
команды в локальном режиме:
# zfsbackup [-v] [-n] local_fs /mntpt
или в удаленном режиме:
# zfsbackup [-v] [-n] -r [-f] remote_fs /mntpt|local_fs \
[user@][host]
В обеих случаях аргументы являются обязательными. Первым
аргументом является локальный или удаленный архивируемый
ZFS-пул либо датасет, вторым - локальная директория или
точка монтирования NFS с правами записи либо локальная
файловая система для приема стрима, третий аргумент
в удаленном режиме представляет имя машины с которой
выполняется резервное копирование пула/датасета.
Команда выполняет протоколирование начала и окончания
резервного копирования, а также (в verbose режиме)
протоколирует работу команды zfs send. Все ошибки,
возникающие в процессе резервного копирования, также
направляются в STDOUT и могут быть перенаправлены
в журнальный файл.
Возможно также включение вызова команды в cron, как показано
на примере:
# Automated data backup job with zfs
# Running weekly at 00:00 Saturday
0 0 * * 6 [ -x /usr/local/bin/zfsbackup ] && \
rm -f /backup/*.data.* > /dev/null 2>&1; \
/usr/local/bin/zfsbackup data /backup >> /var/log/backup.log
# Automated system backup job with zfs
# Running weekly at 01:00 Saturday
0 1 * * 6 [ -x /usr/local/bin/zfsbackup ] && \
rm -f /backup/*.rpool.* > /dev/null 2>&1; \
/usr/local/bin/zfsbackup -r rpool root@backup backup \
>> /var/log/backup.log
Как видите, никаких сложностей, никаких гуёв, никаких чудес. Простой и обыкновенный UNIX-way. Только штатные средства и никаких лицензий за тысячи долларов. Просто немного работы системного администратора. Ну и сервер резервного копирования, разумеется, который штатно строится из обычной машины Solaris с достаточным объемом СХД.
О восстановлении из созданных бэкапов мы поговорим в следующей статье.