Git, mysql, бэкапы и очумелые ручки

Что-то давно мы тут не изобретали бессмысленных велосипедов.

Итак, ситуация: есть сервачок, на нём крутятся десяток сайтиков средней отстойности. Их базы данных весят в районе нескольких мегабайт и не слишком быстро наполняются.

Хотя сайтики и так себе, но потерять данные и разбираться с их заказчиками всё равно не хотелось бы. Поэтому нужно делать резервные копии.

Ничего из файловой системы сохранять не надо, базы небольшие, ставить и настраивать навороченные системы бэкапов лениво, а руки чешутся сделать что-нибудь бессмысленное и беспощадное. Ну так возьмём и сделаем это.

База для примера

Стандартная ситуация: сайт сделали, назабивали туда каких-то текстов побольше и другой чепухи. Потом он висит сам по себе и только пару раз в месяц кто-то добавляет какую-нибудь новость. Да посетители местами камменты пишут.

Итого, изначальный вес базы около, допустим, 3 Мб, а прирост и изменения где-то килобайт на 20 в день.

Бэкапим

Пишем простой скриптик для начала:

$params = array(
    'dbname'   => 'test',
    'username' => 'test',
    'password' => 'test',
);
 
$filename = 'dump-'.date('Y-m-d').'.sql';
$cmd = 'mysqldump -u'.$params['username'].' -p'.$params['password'].' '.$params['dbname'].' > '.$filename;
system($cmd);

Ставим на крон и каждое утро получаем свеженький дымыщийся файлик. Не забываем периодически выкачивать куда-нибудь.

Получаем в папке на каждый день по файлу. Изначальный размер — 1,7 Мб. Размер растёт примерно на 20К ежедневно.

Проблема, что база изменяется слабо, но каждый день у нас новый файл на 1,7 М.

Ну, понятно, добавляем:

system('gzip -9 '.$filename);

И теперь у нас файлы вдвое меньше, но всё равно 800К на каждый день уходит.

Git

Прошло, допустим, 100 дней. Теперь у нас есть папка забитая кучей файлов, которые надо бы как-то чистить или ротировать. Объём всех файлов достиг 100 Мб.

А мы возьмём и положим его в git-репу. Каждый день нам нужно не создать новый файл, а заменить старый и закоммитить.

$params = array(
    'dbname'   => 'test',
    'username' => 'test',
    'password' => 'test',
);
 
$dir = __DIR__.'/storage';
$filename = $dir.'/dump.sql';
 
$cmd = 'mysqldump -u'.$params['username'].' -p'.$params['password'].' '.$params['dbname'].' | gzip -9c > '.$filename.'.gz';
system($cmd);
 
$cmd = array();
$cmd[] = 'cd '.$dir;
$cmd[] = 'unset GIT_DIR';
if (!is_dir($dir.'/.git')) {
    $cmd[] = 'git init';
}
$cmd[] = 'git add .';
$cmd[] = 'git commit -m "'.date('Y-m-d').'"';
 
system(implode(' && ', $cmd));

Теперь у нас один файл, который, если нужно, можно откатить на любой день. И ещё у нас репа, которую легко можно выкачивать из любого места через git pull.

Но размер репы (каталога «.git«) составляет 62 Мб. Все файлы, что у нас были, git, как есть, пихает в своё хранилище.

Слияние файлов

Дампы то у нас каждый раз почти одинаковые, но как-то по-человечески слить их проблематично, так как мы их сжимаем. А с бинарными архивами особенно ничего не сделаешь.

А мы их не будем сжимать:

$cmd = 'mysqldump -u'.$params['username'].' -p'.$params['password'].' '.$params['dbname'].' > '.$filename;

Сами файлы теперь больше, но git может их оптимально слить.

Смотрим размер каталога .git: опять больше 60 Мб. Всё потому, что git сильно не парится с оптимизацией в процессе работы, чтобы не затормаживать процесс. А чтобы запарился, нужно запустить git gc.

Запускаем, и, о чудо! Итоговый размер репы становится 3 Мб.

А заодно теперь можно в каком-нибудь GUI для git сравнивать дампы по дням.

ignore-table

Не забываем игнорировать таблицы с ненужными логами, сессиями и всем подобным. Это не только лишний объём, но и значительно более хреновый мерж.

extended-insert

По умолчанию mysqldump формирует дамп с так называемыми «расширенными» INSERT’ами. То есть вся вставка данных таблицы происходит одним запросом:

INSERT INTO `table` VALUES ('id1', 'one'), ('id2', 'two'), ('id3', 'three')

Это не здорово, так как diff’ить файлы лучше по строкам, а тут при изменении данных изменяется вся единственная строка.

Отключить это можно с помощью опции --extended-insert=FALSE:

$cmd = 'mysqldump --extended-insert=FALSE -u'.$params['username'].' -p'.$params['password'].' '.$params['dbname'].' > '.$filename;

Теперь сами файлики должны стать больше, но, возможно, git сможет их оптимальнее слить.

Как показывает эксперимент, в плане объёма это не помогает — всё те же 3 Мб в итоге.

Однако, теперь намного приятнее смотреть diff всего этого — сразу видно, какие строки добавились, какие изменились.

Разбить по таблицам

Сохраним дамп не одним файлом, а каждую таблицу отдельно:

$tables = getTablesFromDB();
 
$options = array(
    '--user='.$params['username'],
    '--password='.$params['password'],
    '--extended-insert=FALSE',
    '--dump-date=FALSE',
);
$options = implode(' ', $options);
 
foreach ($tables as $table) {
    $filename = $dir.'/'.$table.'.sql';
    $cmd = 'mysqldump '.$options.' '.$params['dbname'].' '.$table.' > '.$filename;
    echo $cmd.\PHP_EOL;
    system($cmd);
}
 
// git commit

Плюсы такого подхода:

  • Некоторые таблицы могут изменять далеко не каждый день. В этом случае в хранилище git’а будет храниться один объект.
  • Сливать отдельные файлы быстрее
  • diff нагляднее
  • Объём хранилища сократился с 3 до 1,8 Мб

Не забываем указать опцию --dump-date=FALSE, а лучше сразу --skip-comments. Иначе каждый раз к дампу будет добавляться комментарий вида «Dump completed on 2013-04-23 17:09:02» и файл будет обновляться всегда.

Применимость

Базы объёмом до 100 Мб и не слишком резво изменяющиеся/разрастающиеся, данный способ обрабатывает вполне стабильно и достаточно быстро.

Реализация

Не следует использовать подобные кривые велосипеды! Поставьте себе лучше какую-нибудь систему.

А вообще вот: Backuper на githab’е.

12 комментариев »

  • Я вот так делаю в MySQL:

    MYSQLDUMP_STRUCTURE_ARGS = (
    ‘—skip-opt’,
    ‘—skip-comments’,
    ‘—add-drop-table’, # for add DROP TABLE before CREATE TABLE (p.s. NOT USE —compact!)
    ‘—create-options’, # add current AUTO_INCREMENT and other options
    ‘—no-autocommit’, # for acceleration
    ‘—routines’, # add stored procedures
    ‘—triggers’, # add triggers
    )
    MYSQLDUMP_DATA_ARGS = (
    ‘—skip-opt’,
    ‘—skip-comments’,
    ‘—disable-keys’, # creating indexes after load data (for acceleration)
    ‘—extended-insert’, # for acceleration
    ‘—insert-ignore’, # for ignoring double rows
    ‘—no-autocommit’, # for acceleration
    ‘—skip-triggers’,
    ‘—default-character-set=utf8’,
    ‘—set-charset’,
    )

    И, если у тебя InnoDB (точнее есть транзакции), то лучше пользоваться флагом «—single-transaction» (создает дамп в виде одной транзакции), бекап будет идти долго (если БД активно используется и т.п.), но зато целостность будет гарантированна и не помешает работе СУБД.

    —single-transaction avoids locking tables. That’s the whole point: it opens a new transaction, which, hopefully, avoids locking.
    I say «hopefully» because this depends on the implementation. InnoDB is such an implementation, and, indeed, it avoids locking.
    Please be aware, though, that —single-transaction only works for TRANSACTIONAL tables (e.g. InnoDB) and does NOT work the way you expect it to on non-transactional tables (e.g. MyISAM).
    So if you have a hybrid schema, with both InnoDB and MyISAM, safest for you is use —lock-tables instead of —single-transaction.
    If you’re using transactional tables only, —single-transaction is advisable in that it avoids locking. The database is available for read/write in that time, though you may see considerable loss of performance.

    Ну и рекомендую http://adw0rd.com/2009/6/7/mysqldump-and-cheat-sheet/

    adw0rd, 23.04.2013, 23:38

  • Забыл сказать что для структуры я добавляю «—no-data», а для данных «—no-create-info», но тебе это не важно, так как ты все в одном хранишь

    adw0rd, 23.04.2013, 23:40

  • adw0rd, сенк. А что дальше с дампами делаешь?

    vasa_c, 24.04.2013, 14:06

  • По разному, некоторые храню в гите, другие просто ротейтю по дате. Но разделение дампов надо для развертывания тестового окружения, просто иногда нужны только данные, а иногда только структура

    adw0rd, 24.04.2013, 15:21

  • Олег, чё с пыхой?

    kostyl, 22.06.2013, 21:56

  • Да, Олег, что с пыхой?

    adw0rd, 23.06.2013, 11:13

  • Питонисты убили пыху.
    Питон — рак убивающий пыху.

    vasa_c, 23.06.2013, 20:07

  • Питон — змея, рак это php, на котором сейчас работает нерабочий форум :-D

    adw0rd, 23.06.2013, 23:01

  • :D

    kostyl, 24.06.2013, 18:40

  • Ждем когда змея родит новую пыху? Или чо?

    artoodetoo, 6.07.2013, 7:44

  • Надо не ждать, а делать, а всем пофиг… http://new.pyha.ru/

    adw0rd, 6.07.2013, 12:07

  • adw0rd, никто не умеет

    vasa_c, 7.07.2013, 19:36

Leave a comment