«Правильный» backup. Несколько реализаций

Резервирование данных никогда не было тривиальной задачей и решений соответственно несчетное количество. Каждый использует различные методы и реализации. Мне повезло: подвернулся большой проект, где резервирование получилось на удивление гибким. Разные данные резервировались с разной периодичностью и полнотой. Вся система резервирования разделена на несколько частей. Каждую попробую описать максимально подробно. Каждая из реализаций может понадобиться отдельно, но у меня они используются все сразу и являют собой совершенную систему резервирования. Это мне так кажется 🙂
Поехали.
Для начала попробуем просто сформулировать общие требования резервирования.
Первое и главное что бы мы имели понятную и логичную структуру расположения данных. Для этого будем все данные складывать в именные папки по дням.
Кроме того у нас будут данные поделены по периодичности: резервирование за каждый день и за каждый месяц. Соответственно в родительском каталоге у нас будет 2 папки: daily и monthly
В каждой из них было бы не плохо создавать каталоги которые означали бы дату резервирования, к примеру 2013-05-22 или 2013-05-23. Понятно, что тут хранятся данные за 22 мая 2013 года и за 23 мая 2013 года соответственно. Очень удобно, как мне кажется, не находите? А уже в них все, что нам нужно. Забегая чуточку вперёд скажу, что у меня несколько скриптов, в которых я использую одинаковые переменные.
Резервировать я планирую базы данных и просто файло скопом. Для удобства и их тоже разнесём внутри каждой папки в отдельные каталоги: DB для баз данных и sites для контента сайтов. Кроме того, у нас есть основная база, данные с которой мы будем складывать в отдельный каталог, и ещё несколько менее важных, но тем не менее резервировать их тоже будем но в общий каталог. И для этого в папке DB создадим каталог для основной базы. Пусть дальше она будет называться как maindb.
теперь немного попробуем реализовать это в виде скрипта. Сначала простая задача: скрипт для полного резервирования только базы maindb, учитывая наши требования.

#!/bin/bash
### Данные для соединения с MySQL сервером ###
MUSER="root" # пользователь базы данных
MPASS="mysqlrootpassword" # пароль пользователя
MDB="maindb" # имя базы, которую будем резервировать
### Системные переменные ###
DIR="/home/backup/daily" # Корневой каталог, куда будут резервирования
DAY=$(date +"%Y-%m-%d") # переменная, которой присваиваем нынешнюю дату в формате гггг-мм-дд
### Сами команды ###
mkdir -p $DIR/$DAY/DB/maindb # создаем каталог. Посмотрим на переменные понимаем, что он будет /home/backup/daily/гггг-мм-дд/DB/maindb. Ровно так, как нам и хотелось. Можно было указать и mkdir -p $DIR/$DAY/DB/$MDB - это ничего бы не изменило, переменные они такие.
cd $DIR/$DAY/DB/maindb # переходим в этот каталог
echo "Резервное копирование базы данных $MDB начато $(date +"%d %B %Y") года в $(date +"%H:%M:%S")" # ставим так называемый таймстамп. Грубо говоря время начала дампа. 
mysqldump -u$MUSER -p$MPASS $MDB > full.sql # сливаем сам дамп. Указываем переменные. Не зря мы их задавали в начале скрипта
echo "Резервное копирование базы данных $MDB закончено $(date +"%d %B %Y") года в $(date +"%H:%M:%S")" # временная отметка окончания работы дампа.

сохраняем это дело в файл, к примеру, full_mysql_backup.sh
Пробуем запустить руками. У меня все прошло отлично и дамп создался. Иначе не писал бы сюда(: . Ну и ставим задание в крон. Я поставил на 22:00

0 22 * * * sh /root/full_mysql_backup.sh #Daily FULL MySQL maindb DB backup

С полным бекапом разобрались. Ничего сложного, подобных скриптов сотни и тысячи в интернете. Не удивлюсь, если абсолютно такой же уже у кого то есть.
Теперь немного усложним задачу. К примеру у нас есть таблицы, важность которых неимоверно зашкаливает. И нам каждый час необходимо делать их резервирование.
Не нарушая общей красоты будем каждый дамп с этими таблицами складывать в эту же папку и называть его в зависимости от времени по шаблону чч-мм-сс.sql. То есть 10-00-01.sql, 11-00-01.sql и т.д.
Поехали, скрипт не на много сложнее:

#!/bin/bash
### Данные для соединения с MySQL сервером ###
MUSER="root" # пользователь базы данных
MPASS="mysqlrootpassword" # пароль пользователя
MDB="maindb" # имя базы, которую будем резервировать
### Системные переменные ###
DIR="/home/backup/daily" # уже знаем
DAY=$(date +"%Y-%m-%d") # уже знаем
TIME=$(date +"%k-%M-%S") # установка временного штампа шаблона чч-мм-сс
### Сами команды ###
mkdir -p $DIR/$DAY/DB/maindb
cd $DIR/$DAY/DB/maindb
echo "Резервное копирование чудо таблиц начато $(date +"%d %B %Y") года в $(date +"%H:%M:%S")" 
mysqldump -u$MUSER -p$MPASS $MDB --tables table1 table2 table3 table4 table5 table6 tableN > $TIME.sql # Дамп необходимых таблиц, перечисляем через пробел. С именем все понятно. 
echo "Резервное копирование чудо таблиц закончено $(date +"%d %B %Y") года в $(date +"%H:%M:%S")"

Назовем его, допустим, hourly_mysql_backup.sh Ничего нового. Те же переменные, что и в предыдущем скрипте. Кроме того

man mkdir -p, —parents
Make any missing parent directories for each directory argument.The mode for parent directories is set to the umask modified by ‘u+wx’. Ignore arguments corresponding to existing directories.(Thus, if a directory /a exists, then ‘mkdir /a’ is an error, but ‘mkdir -p /a’ is not.)

что означает создание родительских директорий. Если таковые уже имеются — просто идем дальше. То есть если один скрипт создал необходимый нам каталог второй не будет его перезаписывать а просто использовать. Опять таки запускаем скрипт руцями дабы проверить на наличие корявостей.
Ну и запихиваем с крон и указываем необходимые параметры. Делать только в рабочие часы в каждую первую минуту.

1 9-18 * * * sh /root/hourly_mysql_backup.sh #Hourly dump of some awesome tables

Ну и что бы домучать уже базы данных сделаем резервирование всех остальных баз, кроме нашей mainbd
Сразу скрипт:

#!/bin/bash
### Данные для соединения с MySQL сервером ###
MUSER="root"
MPASS="mysqlrootpassword"
MYSQL="$(which mysql)" # размещение mysql
MYSQLDUMP="$(which mysqldump)" # размещение mysqldump
DEST="/home/backup/daily"
DAY=$(date +"%Y-%m-%d")
DBS="" # Список баз. Пока что пустой, дальше заполнится
# Пропускаем эти базы данных. 2 системные, одна у нас уже делается. 
EXCLUDE="information_schema
maindb
performance_schema"
# Получаем список баз
DBS="$($MYSQL -u$MUSER -p$MPASS -Bse 'show databases')"
for db in $DBS
do
 skipdb=-1
 if [ "$EXCLUDE" != "" ];
 then
 for i in $EXCLUDE
 do
 [ "$db" == "$i" ] && skipdb=1 || :
 done
 fi
if [ "$skipdb" == "-1" ] ; then
 echo "Резервное копирование базы данных $db начато $(date +"%d %B %Y") года в $(date +"%H:%M:%S")"
 $MYSQLDUMP -u $MUSER -p$MPASS $db > $DEST/$DAY/DB/$db.sql
 echo "Резервное копирование базы данных $db закончено $(date +"%d %B %Y") года в $(date +"%H:%M:%S")"
 fi
done

Мускул домучали. Все у нас прекрасно резервируется и красиво сложено по папочкам и знаем сколько занимает каждый отдельный бекап.
Теперь осталось только сливать контент. Изобретать велосипед не будем, заtarим всё это дело и сложим красиво по папкам.
Я назвал этот скрипт full_sites_backup.sh и засунул его в крон
30 22 * * * sh /root/full_sites_backup.sh #Daily matter content backup
Код скрипта:

#!/bin/bash
### Переменные ###
SITE1="/srv/http/site1/"
SITE2="/srv/http/site2/"
SITE3="/srv/http/site3/"
SITE4="/srv/http/site4/"
TAR="$(which tar)"
DAY=$(date +"%Y-%m-%d")
SITEDIR="/home/backup/daily/$DAY/sites"
### Сама процедура ###
mkdir -p $SITEDIR
cd $SITEDIR
#site1 backup
echo "Резервное копирование директории $SITE1 начато $(date +"%d %B %Y") года в $(date +"%H:%M:%S")"
$TAR -czf $SITEDIR/site1.tar.gz $SITE1
echo "Резервное копирование директории $SITE1 закончено $(date +"%d %B %Y") года в $(date +"%H:%M:%S")"
#site2 backup
echo "Резервное копирование директории $SITE2 начато $(date +"%d %B %Y") года в $(date +"%H:%M:%S")"
$TAR -czf $SITEDIR/site2.tar.gz $SITE2
echo "Резервное копирование директории $SITE2 закончено $(date +"%d %B %Y") года в $(date +"%H:%M:%S")"
#site3 backup
echo "Резервное копирование директории $SITE3 начато $(date +"%d %B %Y") года в $(date +"%H:%M:%S")"
$TAR -czf $SITEDIR/site3.tar.gz $SITE3 --exclude=logs
echo "Резервное копирование директории $SITE3 закончено $(date +"%d %B %Y") года в $(date +"%H:%M:%S")"
#site4 backup
echo "Резервное копирование директории $SITE4 начато $(date +"%d %B %Y") года в $(date +"%H:%M:%S")"
$TAR -czf $SITEDIR/site4.tar.gz $SITE4 --exclude=tmp --exclude=temp --exclude=logs
echo "Резервное копирование директории $SITE4 закончено $(date +"%d %B %Y") года в $(date +"%H:%M:%S")"

Всё это мы уже проходили, пояснения не требуются. Меняйте значения на свои, добавляйте и убирайте директории.
В итоге мы имеем хорошо организованный бекапчик который ещё и имеет очень понятную структуру хранения данных.
Для резервирования данных раз в месяц специально ничего не писал, так как скрипты будут те же абсолютно, только в cron-е нужно сменить время выполнения.
Дополнение. Что бы можно было спать спокойно очень долгое время я решил немного эти бекапы подчищать. Все, которые недельной или больше давности удалять. И место освобождается и актуальность прошлогодних данных стремится к нулю. Для этого в крон добавил ещё строку

45 23 * * * find /home/backup/daily/ -ctime +7 -type d | xargs rm -rf #Remove old data older then 7 days

в 23:45 запускается поиск, который ищет каталоги, дата создания которых больше 7 дней назад (-ctime +7 как можно было догадаться) и удаляет их (xargs rm -rf)
данные из папки /home/backup/monthly не трогаю. Они там раз в месяц делаются. Через год посмотрю, что и как. Сомневаюсь, что вообще любой из них пригодится.

И теперь завершающий штрих. Дабы не хранить все яйца в одной корзине после завершения резервирования всех данных будем отправлять их на удаленный ФТП-сервер. У меня уже есть статья, как это сделать для Windows. Но там отправляется один заранее созданный файл-архив. А у нас тут немного сложнее будет. Ну ничего, глаза боятся, а руки пишут скрипты.
Самый неприятный момент заключается в том, что ftp не может просто так взять и отправить рекурсивно всё на удаленный сервер. Благо у нас все данные структурированы и гуманно распределены по каталогам. По-этому нам придётся лишь малосто наговнокодить. Зато работает!
Методом проб и ошибок пришёл к вот такому коду:

#!/bin/bash
DAY=$(date +"%Y-%m-%d")
WORKDIR="/home/backup/daily/$DAY"
FTP="ftp.server.com"
USER="ftp_user"
PASS="ftp_password"
ftp -n $FTP <<EOF
user $USER $PASS
binary
prompt
cd /DB/maindb
lcd $WORKDIR/DB/maindb
mput *.sql
cd ..
lcd $WORKDIR/DB
mput *.sql
cd ../sites
lcd $WORKDIR/sites
mput *.tar.gz
quit
EOF

Внимание:
1) На удаленном FTP необходимо единоразово создать нужные нам каталоги что бы не создавать каждый раз при запуске скрипта и избежать ошибок. По аналогии с имеющейся структорой архивирования создадим DB и sites. В DB создадим папку maindb
2) по неизвестным причинам конструкция

cd /DB/maindb
mput $WORKDIR/DB/maindb/*.sql

не сработала. Вот и пришлось писать такие извращения.
Теперь немного описания, что же тут что
DAY — В виду того, что это нынешняя дата, то рекомендую скрипт этот в крон устанавливать незадолго до окончания суток и САМОЕ ГЛАВНОЕ что бы все предыдущие скрипты успели завершиться. Нарушение целостности резервных данных — хуже не придумаешь.
binary — устанавливаем бинарный метод передачи
prompt — отключаем запросы типа «Вы ходите скопировать файл $FILE на удаленный сервер?» Раз мы написали такой скрипт то конечно хотим, что бы оно все копирывалось
cd — переход в указанный каталог на FTP сервере.
lcd — (local cd) Переход на локальном сервере в папку, откуда будем вытаскивать файлы
mput — (multiply put) массовое копирование. В нашем случае по расширению.
Ну и собственно у нас таких блоков три. По путям не сложно разобраться что куда копируется.
В итоге имеем точно такую же иерархию файлов на удаленном FTP-сервере. Каждый день файлы перезаписываются(напомню, без запроса)
По правде говоря FTP очень не безопасный метод передачи данных. Это решение является временным. Да и как говорят опыт лишним не бывает.

Добавить комментарий