Некоторое время назад задумался о создании резервных копий баз данных MySQL на своём домашнем «сервере».
Сформировались следующие требования:
В результате пришёл к идее использования простой связки mysql/diff/patch в одном скрипте, который запускается cron'ом.
Основная хитрость заключается в создании инкрементального бэкапа. Для этого на время «собирается» предыдущая версия базы из полного и всех инкрементальных бэкапов, и с помощью diff создаётся новый инкрементальный бэкап. В остальном алгоритм скрипта тривиален.
Собственно, сам скрипт (/etc/cron.daily/mysql_backup):
Сформировались следующие требования:
- Бэкапы должны быть полными/инкрементальными, дабы занимать меньше места
- Бэкапы должны храниться в plain text, чтобы можно было по ним grep'ать и читать их при необходимости
В результате пришёл к идее использования простой связки mysql/diff/patch в одном скрипте, который запускается cron'ом.
Основная хитрость заключается в создании инкрементального бэкапа. Для этого на время «собирается» предыдущая версия базы из полного и всех инкрементальных бэкапов, и с помощью diff создаётся новый инкрементальный бэкап. В остальном алгоритм скрипта тривиален.
Собственно, сам скрипт (/etc/cron.daily/mysql_backup):
#!/bin/bash
PATH=/bin:/sbin:/usr/bin:/usr/sbin
BACKUP_DIR=/wd/backups/mysql
DATE=`date +%F.%H:%M:%S`
# databases to back up
DBS="mysql wikidb gnucash mytinytodo seahub-db seafile-db ccnet-db"
# MySQL credentials
DBU=root
DBP=XXXXXXXXX
# switches for mysqldump
MDUMP_OPTS="--ignore-table=mysql.event --user=${DBU} --password=${DBP} --allow-keywords --comments --skip-dump-date --skip-extended-insert --create-options --quote-names --routines"
################ FUNCTIONS
# create_full() - creates full mysql backup for database $DB
# takes backup file name as argument
function create_full {
local new_full=$1
mysqldump ${MDUMP_OPTS} ${DB} > "${new_full}"
}
# create_inc() - creates incremental mysql backup for database $DB
# arguments:
# $1 - new incremental backup file name
# $2 - previous full backup file name
# $3,4,.. - old inc backup files, ordered by creation date, starting from the oldest one
function create_inc {
local new_inc=$1
local old_full=$2
local old_full_fname=${old_full##*/}
local old_full_patched=/tmp/${old_full_fname}.patched.${DATE}
local tmp_full=/tmp/${DB}.fulltmp.sql.${DATE}
local inc
create_full "${tmp_full}"
# patch old full with old incs
cp -f "${old_full}" "${old_full_patched}"
shift; shift; inc=$1
while shift; do
patch --quiet "${old_full_patched}" "${inc}"
inc=$1
done
# create diff
diff "${old_full_patched}" "${tmp_full}" > "${new_inc}"
#clean up
rm -f "$old_full_patched" "$tmp_full"
}
################ MAIN
mkdir -p ${BACKUP_DIR}/tmp
for DB in $DBS; do
mkdir -p ${BACKUP_DIR}/$DB/{full,diff,inc}
done
if [ $(date +%u) == "1" ]; then # Full backup each Monday
for DB in $DBS; do
new_full=${BACKUP_DIR}/$DB/full/${DB}.full.sql.${DATE}
create_full "${new_full}"
done
else # Incremental backup every other day
for DB in $DBS; do
previous_full=$(ls -t ${BACKUP_DIR}/$DB/full/${DB}.full.sql.* 2>/dev/null | head -1)
previous_full_fname=${previous_full##*/}
previous_incs=$(ls -rt ${BACKUP_DIR}/$DB/inc/${previous_full_fname}.inc.* 2>/dev/null)
new_full=${BACKUP_DIR}/$DB/full/${DB}.full.sql.${DATE}
new_inc=${BACKUP_DIR}/$DB/inc/${previous_full_fname}.inc.${DATE}
if [ -z "$previous_full" ]; then # No previous full backup? Create it then
create_full $new_full
echo "$DB new full backup status $?, path $new_full"
else # create new incremental
create_inc $new_inc $previous_full $previous_incs
echo "$DB new inc backup status $?, path $new_inc"
fi
done
fi
#### Output is sent to administrator's email by cron.
echo "============= Backups ============="
ls -lh ${BACKUP_DIR}/*/{full,inc}/*
echo "============= Size ============="
du -sxh ${BACKUP_DIR}
du -sxh ${BACKUP_DIR}/*