О необходимости резервного копирования говорить смысла нет: если системный администратор не понимает этого, то после первого же сбоя или взлома сервера или просто при случайном удалении ценной информации задумается о нем. Хочу поделиться своим, пусть и не богатым, опытом в этой области.
Сохранять данные можно по-разному: только "полезную" информацию, отдельные директории (без разбора содержимого на полезное) или целые разделы (можно всю систему скопировать). Стратегию и тактику каждый выбирает сам для себя. Для web-сервера я выбрал полное копирование системы, чтобы иметь возможность быстро и без дополнительных настроек восстановить работоспособность сервера при выходе из строя всего RAID-массива, взломе системы или еще каком катаклизме. При условии затрудненного доступа к серверу это видится мне правильным решением: пришел, восстановил всю систему из бекапа до нужного дня, и можно спать спокойно.
Систем резервного копирования придумано много, но не все одинаково полезны в условиях затрудненного доступа к серверу: программа востановления должна умещаться на загрузочном сменном носителе и работать с него без установки в систему.
dump - традиционная утилита резервного копирования для unix-подобных операционных систем. Она может работать с десятью уровнями инкрементальных дампов, записывать их на стриммерные ленты, на любые блочные устройства и просто в файлы на другом разделе. Восстанавливать можно как раздел целиком, так и отдельные файлы и директории.
Работает утилита непосредственно с блочным устройством, содержащим файловую систему, и оперирует деревом узлов (inode). Она может копировать данные как с демонтированного утройства, так и с используемого - смонтированного. Для инкрементального бекапа используется время модификации inode.
В файле /etc/dumpdates хранится информация о времени проведения каждого уровня инкрементального копирования для каждого раздела. Для конкретного устройства информация там появляется только после создания дампа нулевого уровня и обновляется при каждом последующем сбросе дампа любого уровня.
Dump может архивировать данные на лету, используя различные алгоритмы сжатия. Какие именно - лучше справиться в мануале (man dump) на свою ОС. Для Linux это: zlib, bzlib и lzo. Zlib и bzlib поддерживают 9 уровней сжатия и, соответственно, разную скорость сброса дампа. Lzo работает быстрее. Одновременное использование компрессии и стриммера возможно, если стриммер поддерживает блоки переменной длины.
Сбрасываемый дамп может быть разделен на тома - автоматически, по заполнению принимающего устройства, или на куски указанной длины.
Пример создания дампа: сбрасывается дамп нулевого уровня, сжимается библиотекой bzlib на шестом уровне компрессии, нарезается на тома по 700000 блоков по 1кБ (каждый файл помещается на одном CD или по 6 файлов на DVD), тома автоматически именуются с префиксом root- и складываются в директорию /mnt/backup/0 (в точке /mnt/backup подмонтирован отдельный раздел). По окончании обновляется запись в файле /etc/dumpdates.
dump -0 -f /mnt/backup/0/root- -uMB 700000 -j6 /
Восстановление из дампа выполняет команда restore. Досконально разобраться с ее параметрами я еще не успел. Рекомендую начать изучение мануала (man restore) раньше, чем потребуется восстанавливать данные! Расскажу, как восстанавливал директорию из дампа.
Экспортировал список файлов, выделил нужные и поместил полные пути к ним в файл в текущей директории.
restore -t -M -f /mnt/backup/0/var- | \
grep '^/var/www/virtuals/mysite/www/data' | \
awk '{ print $2; }' > list
Потом восстановил их из дампа в текущую директорию.
restore -x -M -f /mnt/backup/0/var- -X list
Сперва надо выбрать стратегию резервного копирования. В различных книгах и статьях в выборе стратегии, как правило, опираются на стриммерные ленты и предлагают их периодически менять. Если стриммера нет или доступ к серверу затруднен, то сбрасывать придется на локальный диск. Можно сбрасывать и по сети, но для скорости лучше сбрасывать локально, а потом копировать в удаленное хранилище.
Я выбрал для сервера такую стратегию:
- Раз в квартал или раз в год (по вкусу и потребности) сбрасывается дамп нулевого уровня. Он самый большой и содержит всю информацию с раздела. Поэтому, чтобы не мучаться с многогигабайтными дампами, стоит делать его не часто.
- Несколько чаще (раз в месяц) сбрасывается дамп первого уровня. Причем, чтобы он не совпал по времени с нулевым дампом, сделаем его 15-го числа. Он будет содержать все накапливающиеся изменения и со временем будет расти до очередного нулевого дампа. Это тоже массивный дамп (зависит от скорости изменения данных на диске).
- Раз в неделю сбрасывается дамп второго уровня. Он собирает в один дамп изменения за неделю, накопленные более высокими уровнями дампов. Его нужно регулярно копировать в удаленное хранилище.
- На остальные дни недели - инкрементально - более высокие уровни: 3, 4, 5, 6, 7 и 8. Они небольшие и каждый содержит изменения только за один день. Копировать их или нет - вопрос сохранности данных. Лучше будет копировать - иначе смысла в этих дампах будет мало.
- Для оперативного сброса вручную оставим 9-й уровень. Это может пригодиться при "неожиданных" ошибках администратора, когда надо будет восстановить старые версии конфигов или случайно удаленные файлы, и нужно чтобы они были не из ночного бекапа, а как можно свежее.
Сброс дампов лучше производить ночью, в то время, когда сервер не нагружен и не занят другими запланированными задачами. Оптимально подходит период с 3-х до 5-и утра. Можно выбрать любой другой момент, определив на практике период наименьшей нагрузки.
Для запланированного запуска я использовал cron (вряд ли в Unix стоит выбирать другой планировщик). Задания должны запускаться от имени root-а. Поэтому они могут быть занесены: или root-ом утилитой crontab, или записаны в файле /etc/crontab.
Пример - фрагмент /etc/crontab:
05 5 * * 6 root dump -8 -f /mnt/backup/0/root- -uMB 700000 -j6 /
05 5 * * 5 root dump -7 -f /mnt/backup/0/root- -uMB 700000 -j6 /
05 5 * * 4 root dump -6 -f /mnt/backup/0/root- -uMB 700000 -j6 /
05 5 * * 3 root dump -5 -f /mnt/backup/0/root- -uMB 700000 -j6 /
05 5 * * 2 root dump -4 -f /mnt/backup/0/root- -uMB 700000 -j6 /
05 5 * * 1 root dump -3 -f /mnt/backup/0/root- -uMB 700000 -j6 /
05 5 * * 7 root dump -2 -f /mnt/backup/0/root- -uMB 700000 -j6 /
05 4 15 * * root dump -1 -f /mnt/backup/0/root- -uMB 700000 -j6 /
35 3 1 1,4,7,10 * root dump -0 -f /mnt/backup/0/root- -uMB 700000 -j6 /
И так надо повторить для каждого копируемого раздела. Это неудобно, громоздко и есть вероятность, что процессы копирования наложатся друг на друга по времени и тем самым только замедлятся. Пора что-нибудь написать для удобства и автоматизации.
Скрипт dump.sh:
#!/bin/sh
# Проверить, что параметер - число от 0 до 9. Можно и погуманее метод взять.
if [ _$1 != _0 ] && [ _$1 != _1 ] && [ _$1 != _2 ] && \
[ _$1 != _3 ] && [ _$1 != _4 ] && [ _$1 != _5 ] && \
[ _$1 != _6 ] && [ _$1 != _7 ] && [ _$1 != _8 ] && \
[ _$1 != _9 ];
then
echo "System backup: invalid argument!"
echo "Usage: dump.sh <dump level>"
exit 1;
fi
DL=$1
echo "Start system backup. Level $DL."
# Старые дампы, с текущего уровня и выше, можно удалить, чтобы не занимали место.
for N in `seq ${DL} 9`
do
rm -f /mnt/backup/${N}/*
done
# Выполняем сброс для каждого раздела.
dump -f /mnt/backup/${DL}/root- -${DL}uMB 700000 -j6 /
dump -f /mnt/backup/${DL}/home- -${DL}uMB 700000 -j6 /home
dump -f /mnt/backup/${DL}/var- -${DL}uMB 700000 -j2 /var
echo "System backup complete."
exit 0
Тогда запуск бекапа производится так:
/root/bin/dump.sh 0
Если в системе есть база данных, то ее файлы, как правило, изменяются каждый день, и размеры они могут иметь приличные. Для целостности базы данных правильно делать резервные копии средствами самой базы, а не простым копированием файлов. Так что базу надо исключить из дампа. Еще в системе есть различные логи - в дамп их тоже нет смысла помещать. То же самое с почтовыми очередями и кешами различных служб, типа named и squid.
Так как dump работает с inode, а не именами файлов, то для исключения нужно передать команде список inode, которые не нужно помещать в дамп. Делается это ключами -e или -E. Первый ключ задает список inode прямо в командной строке, а второй ключ задает имя текстового файла, содержащего список inode (по одному на строку). Если inode принадлежит директории, то в дамп не попадет сама директория и все вышележащие файлы и директории.
Номер inode для файла или директории можно получить с помощью команды stat:
stat -c %i /etc/dumpdates
Таким способом можно сделать список inode и добавить их в строку с dump, но как-то не удобно и не гибко это. Например, лучше будет, если в нулевой дамп войдут все файлы и директории, а в более высокие прописать исключения. Надо только автоматизировать процесс.
В конечном итоге у меня получилась такая программа на Perl. В ней еще не хватает возможности работы с шаблонами в именах файлов (wildcards).
Скрипт dumper.pl:
#!/usr/bin/perl
use strict;
use Config::INI::Reader;
my $CONFIG = '/etc/dumper.conf';
# по умолчанию пусть будет 9-й уровень.
my $level = 9;
# Дефолтные параметры.
my %config = (
'dump-dir' => '/mnt/back/backups/system',
);
my ($fd, $ini);
# ------------------------------
# Наш единственный параметер - должен быть цифрой.
if ($ARGV[0] =~ m/^\d$/)
{
$level = $ARGV[0];
}
# ------------------------------
# Разбираем список файлов, разделенных двоеточиями, и создаем для них список inode.
sub get_inode_list($)
{
my ($file, @list);
for $file (split ':', $_[0])
{
next unless ( -e $file);
push @list, (stat($file))[1];
}
return @list;
}
# ------------------------------
# Чтобы не писать свой парсер конфига, я выбрал формат ini и установил соответствующий модуль.
$ini = Config::INI::Reader->read_file($CONFIG);
# В разделе "*" указаны общие параметры.
if (!exists $ini->{'*'})
{
print STDERR "*** Config error!\n";
exit 1;
}
# Замещаем дефолтные параметры значениями из конфига.
%config = (%config, %{$ini->{'*'}});
if (!exists $config{'jobs'})
{
print STDERR "*** Job's list not found!\n";
exit 1;
}
print "*** Requested dump level is '$level'.\n";
print "*** Delete old backups: ", join(', ' , ($level..9)), ".\n";
for my $n ($level..9)
{
my @files = glob(join('/', $config{'dump-dir'}, $n, '*'));
next unless (@files);
my @cmd = ('rm', '-f', @files);
print join(' ', @cmd), "\n";
system @cmd;
}
print "*** System backup begun.\n";
# В параметре "jobs" перечислены имена "задач".
# На каждую задачу в конфиге надо создать одноименный блок с параметрами.
L_jobs: for my $job (split ' ', $config{'jobs'})
{
unless (exists $ini->{$job} && exists $ini->{$job}{'fs'} && exists $ini->{$job}{'dump'})
{
print STDERR "*** Job '$job': not properly configured!\n";
next L_jobs;
}
unless ( -e $ini->{$job}{'fs'})
{
print STDERR "*** Job '$job': wrong fs assign!\n";
next L_jobs;
}
my @args = ("-${level}", '-f', join('/', $config{'dump-dir'}, $level, $ini->{$job}{'dump'}));
if (exists $ini->{$job}{'params'})
{
push @args, split ' ', $ini->{$job}{'params'};
}
# Параметр вида "exc#" - на каждый уровень дампа. Содержит список исключений из бекапа.
if (exists $ini->{$job}{"exc$level"})
{
my @inodes = get_inode_list($ini->{$job}{"exc$level"});
if (@inodes != 0)
{
push @args, '-e', join(',', @inodes);
}
}
my @cmd = ('dump', @args, $ini->{$job}{'fs'});
print join(' ', @cmd), "\n";
system @cmd;
}
print "*** System backup complete.\n";
exit 0;
# ------------------------------
Устанавливается он легко:
- Распаковать и зайти в получившуюся директорию
- ./Makefile.PL
- make install
Конфиг dumper.conf:
[*]
dump-dir = /mnt/backup
jobs = root home var
[home]
fs = /home
dump = home-
params = -uMB 700000 -j6
[root]
fs = /
dump = root-
params = -uMB 700000 -j6
exc0=
exc1=/tmp
exc2=/tmp
exc3=/tmp:/root/tmp ; Эксперименты root-а в бекап помещать не надо
exc4=/tmp:/root/tmp
exc5=/tmp:/root/tmp
exc6=/tmp:/root/tmp
exc7=/tmp:/root/tmp
exc8=/tmp:/root/tmp
exc9=/tmp:/root/tmp
[var]
fs = /var
dump = var-
params = -uMB 700000 -j2
exc0=
exc1=/var/spool/postfix ; почту бекапить не нужно
exc2=/var/spool/postfix:/var/lib/mysql:/var/named/chroot/var/named/data ; базы и другие динамические данные
exc3=/var/spool/postfix:/var/lib/mysql:/var/named/chroot/var/named/data:/var/log:/var/www/virtuals/logs ; всевозможные логи тоже
exc4=/var/spool/postfix:/var/lib/mysql:/var/named/chroot/var/named/data:/var/log:/var/www/virtuals/logs
exc5=/var/spool/postfix:/var/lib/mysql:/var/named/chroot/var/named/data:/var/log:/var/www/virtuals/logs
exc6=/var/spool/postfix:/var/lib/mysql:/var/named/chroot/var/named/data:/var/log:/var/www/virtuals/logs
exc7=/var/spool/postfix:/var/lib/mysql:/var/named/chroot/var/named/data:/var/log:/var/www/virtuals/logs
exc8=/var/spool/postfix:/var/lib/mysql:/var/named/chroot/var/named/data:/var/log:/var/www/virtuals/logs
exc9=/var/spool/postfix:/var/lib/mysql:/var/named/chroot/var/named/data:/var/log:/var/www/virtuals/logs
Запускается dumper так:
/root/bin/dumper.pl 0