Думаю никому из присутствующих не нужно объяснять важность резервного копирования.
Проблема в том, что из десятков готовых решений ни одно толком не удовлетворяет моим требованиям standalone *NIX-сервера на колокейшене.
Чего же хотелось от резервного копирования?
1) ежедневного полного бакапа всех данных. Никаких incremental-бакапов.
2) максимально быстрого восстановления отдельно взятого файла. Архиваторы (tar/gzip/bzip2/rar) отпадают
3) быстрого мониторинга «кто именно залил вчера на сервер 156Гб?!!!»
4) резервные копии хочется хранить максимально долго, насколько хватает свободного места на дисках.
5) хочется не заботиться об ручном удалении старых копий если место на диске всё-таки уже кончилось
Если в двух словах — то мне захотелось реализовать функционал MAC OS TimeMachine на Linux-сервере.
И я начал писать скрипт.
Вы уже, наверное, догадались, что в основе TimeMachine лежит комбинация из cp -al + rsync
На пальцах это выглядит примерно так:
скопирует хардлинками вчерашние файлы в сегодняшние. Такая копия почти не занимает реального места на диске. Потом запускается
просканирует различия между копией вчерашнего backup и если найдет различия — удалит хардлинк и запишет новую версию файла; создаст/скопирует новые файлы и директории; удалит несуществующие более файлы. При этом все файлы во вчерашнем backup останутся на своих местах.
Понятно что для того чтобы сделать нормальный рабочий скрипт backup этих двух команд маловато. Я решил написать скрипт в 2 файлах — один стандартный для всех серверов с набором функций, второй — коротенький, индивидуальный для каждого сервера, который и запускается непосредственно через cron.
Итак, вот эти скрипты: (можно скачать отсюда )
backup_functions.sh
и backup.sh:
Предполагается что backup_functions.sh размещен в /usr/local/sbin. Если в другой директории — поправьте соотв.строчку в backup.sh
Как видно, скрипт обильно пользуется глобальными переменными. Например
RESERVE_G — резерв (в гигабайтах) который скрипт будет обеспечивать, удаляя самые старые копии, до того как начать сегодняшний backup
BACKUP_EXPIRES_DAYS — через сколько дней удалять старый backup даже если место на диске еще есть. если BACKUP_EXPIRES_DAYS==0 то это не делается.
BACKUP_MAIN_DIR — директория, в которую пишется backup. Внутри создаются папки типа «2011-11-15», куда и пишется backup.
VERIFY_BACKUP_MOUNTED — проверять или не проверять, смонтирован ли отдельный раздел диска под backup. Надо понимать что если выделенный раздел по каким-то причинам размонтировался, то без этой проверки backup ляжет тяжелым грузом на rootfs или другой не подходящую для этого раздел диска. Однако, бывают ситуации когда backup явно и специально пишется именно на rootfs или другой неспециализированный раздел. Тогда нужно указать
VERIFY_BACKUP_MOUNTED=«no»
короткий скрипт backup.sh, который можно уже запускать непосредственно cron-ом, вызывает несколько самодельных функций, а именно:
prepare_for_backup — функция подготавливает всё перед непосредственно копированием. Грубо говоря она делает cp -al + всякие проверки не закончилось ли свободное место на диске.
make_backup (с параметрами) — функция собственно делает копию
make_backup_with_delta (с такими же параметрами) — функция делает копию и дополнительно в директорию 2011-11-15/_delta сливает хардлинками все новые файлы. Заглянув в эту директорию можно легко и быстро узнать кто же залил вчера на сервер 156Гб.
send_email_report — функция отсылает админу e-mail с ошибками (если таковые случились) при резервном копировании.
Помимо резервного копирования файлов еще я написал в довесок функцию backup-а для MySQL.
make_mysql_backup — функция вызывается без параметров, делает потабличный mysqldump (т.е. одна таблица — один .sql-файл), потом все .sql-файлы каждой database упаковывает в db_name.tar.gz
Извините за простыню. Надеюсь кому-то это пригодится.
UPDATE: если вы хотите делать резервные копии чаще чем раз в сутки — в первых строках backup_functions.sh замените строчку
CURR_DATE=`date +%F`
на
CURR_DATE=`date +%F_%R`
Если rsync не установлен — скрипт прерывается с соотв.сообщением.
Если установлен ionice — тогда rsync будет запущен с пониженным io-приоритетом (параметры можно менять в строке
[ -n "`which ionice`" ] && IONICE_CMD='ionice -c2 -n6'
Проблема в том, что из десятков готовых решений ни одно толком не удовлетворяет моим требованиям standalone *NIX-сервера на колокейшене.
Чего же хотелось от резервного копирования?
1) ежедневного полного бакапа всех данных. Никаких incremental-бакапов.
2) максимально быстрого восстановления отдельно взятого файла. Архиваторы (tar/gzip/bzip2/rar) отпадают
3) быстрого мониторинга «кто именно залил вчера на сервер 156Гб?!!!»
4) резервные копии хочется хранить максимально долго, насколько хватает свободного места на дисках.
5) хочется не заботиться об ручном удалении старых копий если место на диске всё-таки уже кончилось
Если в двух словах — то мне захотелось реализовать функционал MAC OS TimeMachine на Linux-сервере.
И я начал писать скрипт.
Вы уже, наверное, догадались, что в основе TimeMachine лежит комбинация из cp -al + rsync
На пальцах это выглядит примерно так:
cp -al вчерашний_backup сегодняшний_backup
скопирует хардлинками вчерашние файлы в сегодняшние. Такая копия почти не занимает реального места на диске. Потом запускается
rsync -a --del данные сегодняшний_backup
просканирует различия между копией вчерашнего backup и если найдет различия — удалит хардлинк и запишет новую версию файла; создаст/скопирует новые файлы и директории; удалит несуществующие более файлы. При этом все файлы во вчерашнем backup останутся на своих местах.
Понятно что для того чтобы сделать нормальный рабочий скрипт backup этих двух команд маловато. Я решил написать скрипт в 2 файлах — один стандартный для всех серверов с набором функций, второй — коротенький, индивидуальный для каждого сервера, который и запускается непосредственно через cron.
Итак, вот эти скрипты: (можно скачать отсюда )
backup_functions.sh
#!/bin/sh
export LC_ALL=en_US.utf8
DIR_PATTERN='20..-..-..'
CURR_DATE=`date +%F`
#CURR_DATE=`date +%F_%R`
RESERVE_G=5
BACKUP_MAIN_DIR='/backup'
BACKUP_TMP_DIR='/tmp'
BACKUP_DELTA=$BACKUP_TMP_DIR/backup.delta
BACKUP_ERRORS=$BACKUP_TMP_DIR/backup.err
BACKUP_REPORT=$BACKUP_TMP_DIR/backup.report
BACKUP_LOG_FACILITY='user.notice'
BACKUP_EXPIRES_DAYS=0
VERIFY_BACKUP_MOUNTED='no'
PID_FILE='/var/run/backup.pid'
BACKUP_MYSQL_DIR=$BACKUP_TMP_DIR/mysql_dump
MYSQL_DATA_DIR='/var/lib/mysql'
[ -z "`which rsync`" ] && { echo "RSYNC is not installed! backup will not work!"; exit; }
[ -n "`which ionice`" ] && IONICE_CMD='ionice -c2 -n6'
touch /etc/default/backup_exclude
rm $BACKUP_DELTA $BACKUP_ERRORS $BACKUP_REPORT 1>/dev/null 2>/dev/null
verify_backup_mounted() {
mount -a
[ -d "$BACKUP_MAIN_DIR" ] || { echo "BACKUP main directory does not exist!"; exit; }
str=`df "$BACKUP_MAIN_DIR" | tail -1 | grep ' /$'`
[ "$str" ] && { echo 'BACKUP partition is not mounted!!!!!!!!'; exit; }
return 0
}
prepare_for_backup() {
if [ -s "$PID_FILE" ] && [ `cat "$PID_FILE"` -ne $PPID ]
then
if [ "`ps ax | awk '{print $1;}' | grep -f \"$PID_FILE\"`" ]
then
echo -n "Previous BACKUP script is still running. PID = "; cat "$PID_FILE"; exit
else
logger -t BACKUP -p $BACKUP_LOG_FACILITY "Previous BACKUP ended unexpectly"
fi
fi
rm "$PID_FILE" 1>/dev/null 2>/dev/null
echo $PPID > "$PID_FILE"
old_dir=`pwd`
cd "$BACKUP_MAIN_DIR" || { echo "BACKUP main directory does not exist!"; exit; }
VERIFY_BACKUP_MOUNTED=`echo "$VERIFY_BACKUP_MOUNTED" | tr 'A-Z' 'a-z'`
[ "$VERIFY_BACKUP_MOUNTED" = "yes" ] && verify_backup_mounted
reserve_k=$(($RESERVE_G * 1048576))
mkdir -p $BACKUP_TMP_DIR 1>/dev/null 2>/dev/null
dirs_list=`ls | grep $DIR_PATTERN | sort`
if [ -n "$dirs_list" ]
then
while { free_k=`df -k .|grep -v Filesystem| sed -e "s/.\+ \([0-9]\+\) .\+/\1/"`
dirs_list=`ls | grep $DIR_PATTERN | sort`
free_pre=$free_k
[ $free_pre -lt $reserve_k ] ; }
do
dir_oldest=`echo $dirs_list | tr " " "\n" | head -1`
[ -d $dir_oldest ] && { logger -t BACKUP -p $BACKUP_LOG_FACILITY "Deleting old backup in $BACKUP_MAIN_DIR/$dir_oldest" ; rm -rf $dir_oldest; }
done
fi
[ "$VERIFY_BACKUP_MOUNTED" = "yes" ] && verify_backup_mounted
last_date=`ls | grep $DIR_PATTERN | sort | tail -1`
if [ -n "$last_date" -a \( "$CURR_DATE" != "$last_date" \) ]
then
logger -t BACKUP -p $BACKUP_LOG_FACILITY "Preparing. Copying $BACKUP_MAIN_DIR/$last_date -> $BACKUP_MAIN_DIR/$CURR_DATE"
mkdir $CURR_DATE 1>/dev/null 2>/dev/null
$IONICE_CMD cp -al "$last_date"/* $CURR_DATE 1>/dev/null 2>/dev/null
rm -rf $CURR_DATE/_delta 1>/dev/null 2>/dev/null
fi
mkdir $CURR_DATE/_delta 1>/dev/null 2>/dev/null
if [ $BACKUP_EXPIRES_DAYS -gt 0 ]
then
for expired_dir in `find "$BACKUP_MAIN_DIR" -maxdepth 1 -mtime +$BACKUP_EXPIRES_DAYS -type d | grep "$DIR_PATTERN"`
do
logger -t BACKUP -p $BACKUP_LOG_FACILITY "Deleting expired backup $expired_dir" ; rm -rf $expired_dir;
done
fi
cd $old_dir
return 0
}
make_backup() {
while [ -n "$1" ]
do
[ "$VERIFY_BACKUP_MOUNTED" = "yes" ] && verify_backup_mounted
src=$1
full_src=`echo $PWD/$1 | sed -e 's://:/:g'`
dst=`echo $BACKUP_MAIN_DIR/$CURR_DATE/$src | sed -e "s/\/\w\+$//"`
mkdir -p $dst 1>/dev/null 2>/dev/null
logger -t BACKUP -p $BACKUP_LOG_FACILITY "$full_src started"
$IONICE_CMD rsync -axW8 --del --exclude-from=/etc/default/backup_exclude $src $dst 2>>$BACKUP_ERRORS
sync
shift
done
return 0
}
make_backup_with_delta() {
while [ -n "$1" ]
do
[ "$VERIFY_BACKUP_MOUNTED" = "yes" ] && verify_backup_mounted
src=$1
full_src=`echo $PWD/$1 | sed -e 's://:/:g'`
dst=`echo $BACKUP_MAIN_DIR/$CURR_DATE/$src | sed -e "s/\/\w\+$//"`
mkdir -p $dst 1>/dev/null 2>/dev/null
rm $BACKUP_DELTA 1>/dev/null 2>/dev/null
logger -t BACKUP -p $BACKUP_LOG_FACILITY "$full_src (with delta) started"
$IONICE_CMD rsync -axW8i --del $src $dst --exclude-from=/etc/default/backup_exclude 2>>$BACKUP_ERRORS | grep "^>f" | cut -d ' ' -f 2- 1>$BACKUP_DELTA
old_dir=`pwd`
cd $BACKUP_MAIN_DIR/$CURR_DATE
dst=`echo $src | sed -e "s/\w\+$//"`
xargs -a $BACKUP_DELTA -r -n5 -d '\n' -I '{}' echo $dst{} | xargs -r -n10 -d '\n' cp -ul --parents -t _delta
rm $BACKUP_DELTA 1>/dev/null 2>/dev/null
cd $old_dir
sync
shift
done
return 0
}
send_email_report() {
if [ -s $BACKUP_ERRORS ]
then
logger -t BACKUP -p $BACKUP_LOG_FACILITY "Sending email report"
echo 'Content-type: text/plain; charset=utf-8' >> $BACKUP_REPORT
echo 'Content-Transfer-Encoding: 8bit' >> $BACKUP_REPORT
echo 'From: root@'`hostname --fqdn` >> $BACKUP_REPORT
echo 'To: root' >> $BACKUP_REPORT
echo 'Date:' `date` >> $BACKUP_REPORT
echo -e 'Subject: Cron <root@'`hostname --fqdn`'> BACKUP\n\n' >> $BACKUP_REPORT
cat $BACKUP_ERRORS >> $BACKUP_REPORT
cat $BACKUP_REPORT | sendmail root
fi
rm $BACKUP_DELTA $BACKUP_ERRORS $BACKUP_REPORT $PID_FILE 1>/dev/null 2>/dev/null
logger -t BACKUP -p $BACKUP_LOG_FACILITY "Finished"
return 0
}
make_mysql_backup() {
MYSQL_DATA_DIR='/var/lib/mysql'
mkdir $BACKUP_MAIN_DIR/$CURR_DATE/MySQL 1>/dev/null 2>/dev/null
rm -rf $BACKUP_MAIN_DIR/$CURR_DATE/MySQL/* 1>/dev/null 2>/dev/null
rm -rf $BACKUP_MYSQL_DIR 1>/dev/null 2>/dev/null
mkdir -p $BACKUP_MYSQL_DIR 1>/dev/null 2>/dev/null
cd $MYSQL_DATA_DIR
logger -t BACKUP -p $BACKUP_LOG_FACILITY "MySQL started"
for db_dir in `ls -p | grep '/' | tr -d '/'`
do
cd $MYSQL_DATA_DIR/$db_dir
db_name=`echo $db_dir | sed -e 's/@003d/=/g' -e 's/@002d/-/g'`
logger -t BACKUP -p $BACKUP_LOG_FACILITY "MySQL database '$db_name' started"
for table in `ls | grep '.frm' | sed -e 's/\.frm//' -e 's/ /:::/g'`
do
table=`echo $table | sed -e 's/:::/ /g' -e 's/@003d/=/g' -e 's/@002d/-/g'`
mysqldump $db_name "$table" --skip-lock-tables -Q -u $mysql_user -p$mysql_pass > "$BACKUP_MYSQL_DIR/$table.sql"
done
cd $BACKUP_MYSQL_DIR
tar czf $BACKUP_MAIN_DIR/$CURR_DATE/MySQL/$db_name.tar.gz * 1>/dev/null 2>/dev/null
rm $BACKUP_MYSQL_DIR/* 1>/dev/null 2>/dev/null
done
return 0
}
и backup.sh:
#!/bin/sh
. /usr/local/sbin/backup_functions.sh
BACKUP_EXPIRES_DAYS=365
# резерв 30Гб
RESERVE_G=30
BACKUP_MAIN_DIR='/backup'
VERIFY_BACKUP_MOUNTED='yes'
prepare_for_backup
cd /
make_backup etc boot home root opt srv usr/local
make_backup_with_delta var/spool var/lib var/www
BACKUP_MAIN_DIR='/backup'
mysql_user='root'
mysql_pass='jndey7hdFdfii7HN6ygdrarUh'
make_mysql_backup
send_email_report
Предполагается что backup_functions.sh размещен в /usr/local/sbin. Если в другой директории — поправьте соотв.строчку в backup.sh
Как видно, скрипт обильно пользуется глобальными переменными. Например
RESERVE_G — резерв (в гигабайтах) который скрипт будет обеспечивать, удаляя самые старые копии, до того как начать сегодняшний backup
BACKUP_EXPIRES_DAYS — через сколько дней удалять старый backup даже если место на диске еще есть. если BACKUP_EXPIRES_DAYS==0 то это не делается.
BACKUP_MAIN_DIR — директория, в которую пишется backup. Внутри создаются папки типа «2011-11-15», куда и пишется backup.
VERIFY_BACKUP_MOUNTED — проверять или не проверять, смонтирован ли отдельный раздел диска под backup. Надо понимать что если выделенный раздел по каким-то причинам размонтировался, то без этой проверки backup ляжет тяжелым грузом на rootfs или другой не подходящую для этого раздел диска. Однако, бывают ситуации когда backup явно и специально пишется именно на rootfs или другой неспециализированный раздел. Тогда нужно указать
VERIFY_BACKUP_MOUNTED=«no»
короткий скрипт backup.sh, который можно уже запускать непосредственно cron-ом, вызывает несколько самодельных функций, а именно:
prepare_for_backup — функция подготавливает всё перед непосредственно копированием. Грубо говоря она делает cp -al + всякие проверки не закончилось ли свободное место на диске.
make_backup (с параметрами) — функция собственно делает копию
make_backup_with_delta (с такими же параметрами) — функция делает копию и дополнительно в директорию 2011-11-15/_delta сливает хардлинками все новые файлы. Заглянув в эту директорию можно легко и быстро узнать кто же залил вчера на сервер 156Гб.
send_email_report — функция отсылает админу e-mail с ошибками (если таковые случились) при резервном копировании.
Помимо резервного копирования файлов еще я написал в довесок функцию backup-а для MySQL.
make_mysql_backup — функция вызывается без параметров, делает потабличный mysqldump (т.е. одна таблица — один .sql-файл), потом все .sql-файлы каждой database упаковывает в db_name.tar.gz
Извините за простыню. Надеюсь кому-то это пригодится.
UPDATE: если вы хотите делать резервные копии чаще чем раз в сутки — в первых строках backup_functions.sh замените строчку
CURR_DATE=`date +%F`
на
CURR_DATE=`date +%F_%R`
Если rsync не установлен — скрипт прерывается с соотв.сообщением.
Если установлен ionice — тогда rsync будет запущен с пониженным io-приоритетом (параметры можно менять в строке
[ -n "`which ionice`" ] && IONICE_CMD='ionice -c2 -n6'