вторник, 8 сентября 2009 г.

Резервирование и восстановление ZFS I

Резервное копирование файловых систем - первая задача, с которой сталкивается каждый системный администратор. Shit happens, носители информации имеют тенденцию выходить из строя по самым различным причинам, начиная от частых отключений питания и перегревов в серверных помещениях и заканчивая акцидентальным удалением пьяным администратором собственных файловых систем, с выносом всего и вся командами наподобие патча Бармина.

И первый сюрприз, подстерегающий системных администраторов с ZFS это - а где, собственно, zfsdump и zfsrestore?

Это, однако, не самый суровый сюрприз. Второй по значимости, но не менее впечатляющий - где FLAR-архивы для некорневых пулов? Нет, идея засунуть /export/home на root pool - она не лишена привлекательности, но как, скажем, писать имиджи для bare-metal restore в случае, если ее туда не засунули? Ибо, лично меня, например, как-то не впечатляет идея терабайтного root pool (хотя некоторые самоделкины ухитряются проделать и такое).

Второй сюрприз требует некоторых пояснений. Если на UFS flar собирает в архив все смонтированные на / файловые системы, то, в случае с ZFS, если вы, к примеру, высадили /export/home на отдельный пул, то обнаружите, что внутри flar-архива теперь находится только и исключительно root pool и больше ничего.

Намедни нарвался я на одну проблему, как раз описанную выше. Если за пределы root pool вынесены какие-либо другие данные отдельными пулами, то, даже с учетом того, что они смонтированы на /, они не включаются в состав flar-архива.

Соответственно, как выполнять bare-metal restore, в случае чего (чего не приведи бог, конечно),

Для начала некоторый поиск привел к данной статье. Статья зело интересная и поучительная, особенно с учетом того, что flar все еще с ZFS не слишком дружит. Правда, для адаптации описанного решения к повседневным задачам большинства из нас нужно сильно помахать напильником и поскрипеть мозгом. Головным. :)

Но, в действительности, ввиду особенностей ZFS, все же следует обратить свой взгляд в сторону функциональности zfs send и zfs receive.

Дабы гарантировать целостность любых данных, будем иметь дело со снапшотами.

Первое приближение приводит нас к однострочникам для cron:

# Automated data backup job with zfs
# Running weekly at 01:00 Saturday
0 1 * * 6 /sbin/zfs destroy -r data@snap > /dev/null 2>&1;/bin/rm -f /export/home/archives/data.zfs.gz && /sbin/zfs snapshot -r data@snap && /sbin/zfs send -R data@snap | /bin/gzip -9 > /export/home/archives/data.zfs.gz && /sbin/zfs destroy -r data@snap > /dev/null 2>&1

# Automated system backup job with zfs
# Running weekly at 02:00 Saturday
0 2 * * 6 /sbin/zfs destroy -r rpool@snap > /dev/null 2>&1;/bin/rm -f /export/home/archives/rpool.zfs.gz && /sbin/zfs snapshot -r rpool@snap && /sbin/zfs send -R rpool@snap | /bin/gzip -9 > /export/home/archives/rpool.zfs.gz && /sbin/zfs destroy -r rpool@snap > /dev/null 2>&1

Обратите внимание на следующий факт. Пулы резервируются посредством рекурсивных снапшотов, это гарантирует целостность и полноту данных. Датастримы сжимаются посредством gzip с максимальной компрессией (во-первых, можно легко разобрать архивы руками, во-вторых - сильно экономится пространство, правда, с поправкой на природу данных пулов). Собственно, датастримы совершенно спокойно сохраняются в файлы, и, при необходимости, и отдельные файлы, и датасеты и целиком пулы сравнительно легко извлечь и положить на место функциональностью клонирования, promote и rename.

Однако хотелось бы написать нечто, подобное данной утилите для создания flar-архивов нулевого уровня.

Собственно, взяв за основу вышеозначенную утилиту, удалось менее, чем за сутки родить следующий скрипт:

#!/sbin/sh

#
# ZFS filesystem(s) compressed backup.
#
# ZFS archives uses for bare-metal restore
# and systems cloning.
#
# Archive names will be incremental, like that:
# [hostname].[pool|dataset].n.zfs<.gz>, n=0,1,2...
# Note: Do not rename archive files! Filenames will
# use to recovery purposes.
#
# Version 1.0 (C) 2009 Y.Voinov
#
# If you specify pool/dataset
# and destination directory in command line,
# script will run in non-interactive mode.
#
# ident "@(#)zfs_backup.sh 1.0 09/07/09 YV"
#

#############
# Variables #
#############

# Snapshots extension by default
# Correct it if these snapshots already exists in the system
SNAP_EXT="snapshot"
# 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
CUT=`which cut`
DATE=`which date`
ECHO=`which echo`
GZIP=`which gzip`
HOSTNAME=`which hostname`
ID=`which id`
PRINTF=`which printf`
RM=`which rm`
SED=`which sed`
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`

###############
# 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 "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
attempt=0

while [ "`archive_exists`" = "1" ]; do
file=`$ECHO $file|$CUT -f2 -d"."`
file="$dest/$system.$file.$attempt.$ext"
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
}

check_non_interactive ()
{
# Check if script runs in non-interactive mode
arg1=$1
arg2=$2

# If script command-line argument not specify,
# then run in interactive mode
if [ "x$arg1" = "x" -a "x$arg2" = "x" ]; then

# Set interactive mode flag
interactive="1"

$ECHO "---------------------------------------"
$ECHO "$system ZFS backup archive creation"
$ECHO "---------------------------------------"
$ECHO
$ECHO ">>> Press to continue or"
$ECHO ">>> Press to cancel operation."
$ECHO
read p

# Read pool/dataset to archive
$ECHO "Input pool/dataset name to backup"
$PRINTF "and press enter: "
read filesystem

# Read directory/mount point to send
$ECHO "Input archive destination mount point"
$PRINTF "and press enter: "
read dest

elif [ "x$arg1" = "x/?" -o "x$arg1" = "x/h" -o "x$arg1" = "x/help" -o "x$arg1" = "xhelp" ]; then

# Set interactive mode flag
interactive="0"

$ECHO "Usage: $0 calls script in interactive mode."
$ECHO " or"
$ECHO " $0 [pool/dataset] [destination]"
$ECHO " calls script in non-interactive mode."
$ECHO
$ECHO "Note: Archives will be compressed if GZip installed."
exit 0

else
# If script command-line argument specified,
# run in non-interactive mode
filesystem=$arg1
dest=$arg2
fi

# Check filesystem exists
check_fs_exists $filesystem
# Check destination directory
check_dest_dir $dest
}

destroy_fs ()
{
# Destroy filesystem(s) recursively
filesys=$1

$ZFS destroy -r $filesys > /dev/null 2>&1

# Check exit code
if [ "`$ECHO $?`" != "0" ]; then
$ECHO "WARNING: Filesystem $filesys does not exists."
fi
}

create_snap ()
{
# Create snapshot recursively
filesys=$1

$ZFS snapshot -r "$filesys@$SNAP_EXT"
}

zfs_send ()
{
# Send filesystem to the destination
filesys=$1

# Verbose output flag set in interactive mode
if [ "$interactive" = "1" ]; then
verb="v"
fi

if [ "$compress" = "1" ]; then
$ZFS send -R"$verb" "$filesys@$SNAP_EXT" | $GZIP "-$COMP_LEVEL" > "$file$ARC_SUFFIX"
else
$ZFS send -R"$verb" "$filesys@$SNAP_EXT" > $file
fi
}

##############
# Main block #
##############

# Checking OS
check_os

# Checking root
check_root

# Check non-interactive mode
check_non_interactive $1 $2

$ECHO "*** BEGIN: ZFS backup for $filesystem at `$DATE`."

# Check archiver
if [ ! -f "$GZIP" -a ! -x "$GZIP" ]; then
$ECHO "WARNING: Compression will NOT be used. GZip not found."
compress="0"
else
$ECHO "Archive will be compressed with gzip -$COMP_LEVEL."
compress="1"
fi

# Set initial archive file name
# Replase slashes with % if dataset specified
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

# First destroy snapshots recursively if it exists
destroy_fs "$filesystem@$SNAP_EXT"

# Second create recursive snapshots
create_snap $filesystem

# Third send all snapshots recursively to archive
zfs_send $filesystem

# Finally destroy all snapshots recursively
destroy_fs "$filesystem@$SNAP_EXT"

$ECHO "*** DONE: ZFS backup for $filesystem at `$DATE`."

В общем-то, все достаточно просто. Скрипт создает архивированные с максимальной компрессией (если находит в системе GZip) файловые образы рекурсивных снапшотов для заданных ZFS-пулов и датасетов, и является основой для (скоро будет написана) утилиты восстановления из данных архивов. Не хватило времени написать и ее тоже. ;)

Данные архивы легко могут быть использованы для рукопашного восстановления (если слегка подумать и покурить маны).

В качестве точки назначения можно указывать ленту, точку монтирования локальной системы или NFS (собственно, целью данной работы и было написать нечто, приближенное к ufsdump и/или flar).

Маленькое замечание. При указании датасетов для архивирования при сохранении архива, имя которого формируется на основе имени датасета, выполняется замена слэшей (/) на символ "%", в противном случае создается не файл, а поддерево директорий с коротким файлом в самой нижней директории. Для восстановления руками это не имеет решающего значения, и сделано с умыслом для удобства использования в обратной утилите zfs_restore.sh (которая в данный момент находится в процессе написания).

Если сравнивать GZip -9 c compress, посредством которого сжимаются flar-архивы, то очевидно, что GZip сжимает значительно лучше:

root @ host /export/home/archives # ls -al
total 6835661
drwxr-xr-x 2 root root 5 Sep 8 16:56 .
drwxr-xr-x 17 root root 17 Sep 7 11:17 ..
-rw-r--r-- 1 root root 3410629509 Sep 8 16:12 host.data.0.zfs.gz
-rw-r--r-- 1 root root 1446917194 Sep 8 16:40 host.rpool.0.zfs.gz
-rw-r--r-- 1 root root 2136622740 Sep 8 17:04 host0.flar

Flar-архив в данном примере и host.rpool.0.zfs.gz записаны с одной и той же файловой системы со следующим заполнением:

rpool 9.98G 21.5G 41.5K /rpool
rpool/ROOT 3.97G 21.5G 18K /rpool/ROOT
rpool/ROOT/zfsBE_s10_509 3.97G 21.5G 3.97G /
rpool/dump 2.00G 21.5G 2.00G -
rpool/swap 4G 25.5G 98K -

Чистый объем root pool в данной системе составляет чуть менее 4 Гб, и архивируется, соответственно, в 2 Гб flar и 1,3 Гб компрессированного стрима ZFS.

Так что, как видно из приведенного примера, компрессирование стримов ZFS имеет смысл в любом случае. Навскидку приведу пару публикаций на данную тему:

Пруфлинк 1
Пруфлинк 2

Умные мысли посещают умные головы одновременно. ;)

PS. При передаче снапшотов ZFS по сети компрессирование также имеет смысл. Меньше передаваемой информации, меньше загрузка сети (правда, большая загрузка CPU, однако нам хорошо известно, что законы сохранения для IT действуют ;)).