Статья
Версия для печати
Обсудить на форуме
Резервное копирование утилитой dump.




Вступление.

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


Возможности dump.

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.

Код: (Bash)
dump -0 -f /mnt/backup/0/root- -uMB 700000 -j6 /


Работа с restore.

Восстановление из дампа выполняет команда restore. Досконально разобраться с ее параметрами я еще не успел. Рекомендую начать изучение мануала (man restore) раньше, чем потребуется восстанавливать данные! Расскажу, как восстанавливал директорию из дампа.

Экспортировал список файлов, выделил нужные и поместил полные пути к ним в файл в текущей директории.
Код: (Bash)
restore -t -M -f /mnt/backup/0/var- | \
        grep '^/var/www/virtuals/mysite/www/data' | \
        awk '{ print $2; }' > list

Потом восстановил их из дампа в текущую директорию.
Код: (Bash)
restore -x -M -f /mnt/backup/0/var- -X list


Стратегия резервного копирования.

Сперва надо выбрать стратегию резервного копирования. В различных книгах и статьях в выборе стратегии, как правило, опираются на стриммерные ленты и предлагают их периодически менять. Если стриммера нет или доступ к серверу затруднен, то сбрасывать придется на локальный диск. Можно сбрасывать и по сети, но для скорости лучше сбрасывать локально, а потом копировать в удаленное хранилище.
Я выбрал для сервера такую стратегию:
  • Раз в квартал или раз в год (по вкусу и потребности) сбрасывается дамп нулевого уровня. Он самый большой и содержит всю информацию с раздела. Поэтому, чтобы не мучаться с многогигабайтными дампами, стоит делать его не часто.
  • Несколько чаще (раз в месяц) сбрасывается дамп первого уровня. Причем, чтобы он не совпал по времени с нулевым дампом, сделаем его 15-го числа. Он будет содержать все накапливающиеся изменения и со временем будет расти до очередного нулевого дампа. Это тоже массивный дамп (зависит от скорости изменения данных на диске).
  • Раз в неделю сбрасывается дамп второго уровня. Он собирает в один дамп изменения за неделю, накопленные более высокими уровнями дампов. Его нужно регулярно копировать в удаленное хранилище.
  • На остальные дни недели - инкрементально - более высокие уровни: 3, 4, 5, 6, 7 и 8. Они небольшие и каждый содержит изменения только за один день. Копировать их или нет - вопрос сохранности данных. Лучше будет копировать - иначе смысла в этих дампах будет мало.
  • Для оперативного сброса вручную оставим 9-й уровень. Это может пригодиться при "неожиданных" ошибках администратора, когда надо будет восстановить старые версии конфигов или случайно удаленные файлы, и нужно чтобы они были не из ночного бекапа, а как можно свежее.
Сброс дампов лучше производить ночью, в то время, когда сервер не нагружен и не занят другими запланированными задачами. Оптимально подходит период с 3-х до 5-и утра. Можно выбрать любой другой момент, определив на практике период наименьшей нагрузки.


Автоматизация и оптимизация dump.

Для запланированного запуска я использовал 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:
Код: (Bash)
#!/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

Тогда запуск бекапа производится так:
Код: (Bash)
/root/bin/dump.sh 0

Если в системе есть база данных, то ее файлы, как правило, изменяются каждый день, и размеры они могут иметь приличные. Для целостности базы данных правильно делать резервные копии средствами самой базы, а не простым копированием файлов. Так что базу надо исключить из дампа. Еще в системе есть различные логи - в дамп их тоже нет смысла помещать. То же самое с почтовыми очередями и кешами различных служб, типа named и squid.
Так как dump работает с inode, а не именами файлов, то для исключения нужно передать команде список inode, которые не нужно помещать в дамп. Делается это ключами -e или -E. Первый ключ задает список inode прямо в командной строке, а второй ключ задает имя текстового файла, содержащего список inode (по одному на строку). Если inode принадлежит директории, то в дамп не попадет сама директория и все вышележащие файлы и директории.

Номер inode для файла или директории можно получить с помощью команды stat:
Код: (Bash)
stat -c %i /etc/dumpdates

Таким способом можно сделать список inode и добавить их в строку с dump, но как-то не удобно и не гибко это. Например, лучше будет, если в нулевой дамп войдут все файлы и директории, а в более высокие прописать исключения. Надо только автоматизировать процесс.


Пример скрипта.

В конечном итоге у меня получилась такая программа на Perl. В ней еще не хватает возможности работы с шаблонами в именах файлов (wildcards).

Скрипт dumper.pl:
Код: (Perl)
#!/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;

# ------------------------------

Модуль Config::INI можно взять в библиотеке Perl-модулей CPAN.
Устанавливается он легко:
  • Распаковать и зайти в получившуюся директорию
  • ./Makefile.PL
  • make install

Конфиг dumper.conf:
Код: (INI)
[*]
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 так:
Код: (Bash)
/root/bin/dumper.pl 0



Вопросы можно задать на нашем форуме в разделе Unix.
Вопросы по программированию на Perl можно задать в соответствующем разделе форума.

Чернышов Роман (rxl@mail.ru)
14.05.2008
Версия для печати
Обсудить на форуме