MySQL LVM Update

Материал из OpenWiki
Перейти к: навигация, поиск

Моментальное обновление данных в СУБД MySQL с помощью LVM SNAPSHOT

ЛИРИЧЕСКОЕ ВСТУПЛЕНИЕ

Наверно не один я сталкивался с проблемой, когда добавление больших блоков данных в СУБД приводит к блокировке сервисов извлечения информации. В моем случае это система сетевого мониторинга. Туда периодически сливается" очередной часовой блок сырой статистики по сетевому трафику размером до нескольких сотен мегабайт. Далее следует дружный букет групповых команд типа "LOAD DATA ..." и "INSERT INTO ... SELECT FROM ...". В течение всего цикла, который длится несколько минут чтение данных из базы затруднительно (периодически блокируется доступ по чтению) и, более того, нежелательно (хранилище находится в несогласованном состоянии). Относительно недавно я решил эту проблему с помощью моментальных снимков (snapshot) в менеджере томов LVM. Результатом я остался доволен, поэтому и хочу поделиться идеей и ее реализацией.

ЗАМЕЧАНИЕ: описываемая технология предполагает, что большие обновляемые таблицы с данными имеют тип MyISAM. Это не относиться ко всему хранилищу, так, например, реестр пользователей может спокойно оставаться InnoDB.

ПРИНЦИП РАБОТЫ

В самых общих чертах все предельно просто:

 -- создаем снимок базы.
 -- все обновления делаем в исходной базе.
 -- извлечение информации делаем из снимка.
 -- периодически синхронизуем снимок с основной базой.

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

ГРАБЛИ

Грабли N1

Производить синхронизацию после каждого обновления слишком накладно, желательно делать это только после «тяжелых» обновлений. Но тогда «легкие» обновления (напр. изменение атрибутов учетной записи) тоже не будут оперативно актуализироваться. В этом случае существенные большие таблицы данных, из которых производится выборка лучше вынести в отдельную схему и именно с нее делать снимок. Получаем конфигурацию:

  1. mydb – исходная схема без таблиц данных.
  2. mydb_master – схема, куда вынесены таблицы данных.
  3. mydb_slave – схема-снимок с mydb_master.

Желательно, чтобы между mydb и mydb_maser не было сильных связей (типа внешних ключей).

Грабли N2

Народ, который экспериментировал со снимками LVM, обнаружил, что не все файловые системы для этого хорошо подходят. Отсылаю читателей к популярному блогу [1] -- с рекомендациями использовать в этом случае файловую систему xfs вместо ext2/3 (для Линукса).

Грабли N3

Типичная рекомендация как корректно сделать снимок базы данных MySQL приведена в коментариях к документации MySQL

 FLUSH TABLES WITH READ LOCK;
 SHOW MASTER STATUS;
 SYSTEM xfs_freeze -f /var/lib/mysql;
 SYSTEM YOUR_SCRIPT_TO_CREATE_SNAPSHOT.sh;
 SYSTEM xfs_freeze -u /var/lib/mysql;
 UNLOCK TABLES;
 EXIT;

... теперь она может не работать. Современные девайс-мапперы LVM выполняют функции xfs_freeze автоматически. Если наблюдается «вечная» блокировка, скорее всего так оно есть. Тогда просто удалите xfs_freeze.

Однако после FLUSH TABLES требуется системная команда sync – иначе данные в снимке могут портиться.

Что касается SHOW MASTER STATUS, глубокого смысла не понял и поэтому не использую.

 FLUSH TABLES WITH READ LOCK;
 SYSTEM /bin/sync;
 SYSTEM YOUR_SCRIPT_TO_CREATE_SNAPSHOT.sh;
 UNLOCK TABLES;
 EXIT;

Грабли N4

Поскольку снимок является точной копией оригинала, то xfs не даст вам его смонтировать просто так, за красивые глаза. Выручит опция nouuid.

mount -o nouuid,ro /dev/path-to-slave-device /path-to-mount-point

Грабли N5

Как заменить версию mydb_slave? Проще всего переписать старую символическую ссылку на новую:

 mydb_slave -> /path-to-mysqldata/slaves/mydb_slave_0
 mydb_slave -> /path-to-mysqldata/slaves/mydb_slave_1

После этого обязательно запускаем SQL команду FLUSH TABLES. Она закрывает отрытые файлы таблиц. Новы данные при этом моментально акутализируются, а мы можем спокойно размонтировать старую базу /path-to-mysqldata/slaves/mydb_slave_0 не боясь получить по рогам системым сообщением "resource busy".

СВОДКА КОМАНД

Примерная сводка команд (в моем случае это OS CentOS) приведена ниже.

Предварительная подготовка

Файловая система xfs присутствует в стандратном дистрибутиве CentOS и при отсутствии ставиться стандартно.

yum install kmod-xfs xfsprogs

(Не забудьте переписать код приложения с учетом, что база данных mydb будет разбита на mydb, mydb_master и mydb_slave :).

 /etc/init.d/mysql stop
 cd /var/lib/mysql
 mkdir mydb_master
 # создаем логический том для мастера
 # используем префикс lv_, чтобы автоматический 
 # скрипт имел лишнюю возможность проверить, что
 # поданные ему на вход данные корректны.
 lvcreate -L 100G -n lv_mydb_master my_vg
 # форматируем его как xfs
 mkfs –t xfs /dev/my_vg/lv_mydb_master
 mount /dev/my_vg/lv_mydb_master /var/lib/mysql/mydb_master
 # переносим таблицы данных в mydb_master
 cd mydb
 mv table1.* table2.* ... ../mydb_master
 cd ..
 # готовим mydb_slave
 mkdir slave
 mkdir slave/mydb_slave_0
 ln -s slave/mydb_slave_0 mydb_slave
 lvcreate -s -L 25G -n lv_mydb_slave_0 my_vg/lv_mydb_master
 mount /dev/my_vg/lv_mydb_slave_0 /var/lib/mysql/slave/mydb_slave_0
 /etc/init.d/mysql start

Замена снимка

Вставки #SQL показвают команды SQL которые должны выполняться параллельно.

 #SQL: FLUSH TABLES WITH READ LOCK
 sync
 lvcreate -s -L 25G -n lv_mydb_slave_1 my_vg/lv_mydb_master
 mkdir /var/lib/mysql/slave/mydb_slave_1
 mount -o nouuid,ro /dev/my_vg/lv_mydb_slave_1 /var/lib/mysql/slave/mydb_slave_1
 ln -s /var/lib/mysql/slave/mydb_slave_1 /var/lib/mysql/mydb_slave_1
 mv -T mydb_slave_1 mydb_slave
 #SQL: FLUSH TABLES
 umount /dev/mapper/my_vg-lv_mydb_slave_0
 lvremove -f /dev/mapper/my_vg-lv_mydb_slave_0
 rmdir /var/lib/mysql/slave/mydb_slave_0
 #SQL: UNLOCK TABLES

КОД СКРИПТА

В заключение я приведу код скрипта на языке Perl, который работает на CentOS 5.3 и выполняет процедуру замены снимка автоматически. Для идентификации версий снимков здесь используется serial, который меняется с 0 на 1 и обратно. В предыдущей версии serial просто инкрементировался. Я решил, что это не очень хорошо, поскольку при возникновении проблемы с удалением старого снимка все доступное пространство в volume group можеть быстро "скушано" новыми версиями.


 #!/usr/local/bin/perl -w
 use DBI;
 use Cwd;
 use strict;
 
 my $DB_NAME = 'mydb';
 my $DB_USER = 'stat';
 my $DB_PASS = '123';
 
 my $master = "${DB_NAME}_master"; # name of master database
 my $slave  = "${DB_NAME}_slave";  # name of slave  database
 my $SNAP_SIZE = "25G";            # snaphost size
 my $datadir = "/non-existent-aaa"; # mysql datadir will be set bellow
 my $SLAVE_STORE = "slave";         # subdirectory in datadir to create
                                    # slave mount subdirectories
 
 $ENV{PATH} .= ":/bin:/usr/bin:/sbin:/usr/sbin:/usr/local/bin";
 
 # get device specification from database path
 # returns: {dev => <device>, vg => <volume group>, lv => <logical volume>}
 # WARNING: it may depend OS/device_mapper version
 sub get_dev($);
 
 # just print and exec system command
 # returns exit status
 sub exec_sh($);
 
 my $dbh;
 
 $dbh = DBI->connect("dbi:mysql:$DB_NAME", $DB_USER, $DB_PASS) || die "\n";
 
 my $sth = MakeQuery("SHOW GLOBAL VARIABLES");
 
 while(my ($k,$v) = $sth->fetchrow) {
     $datadir = $v if $k eq 'datadir';
 }
 
 $datadir =~ s/\/$//;
 
 chdir $datadir || die "cannot chdir to $datadir";
 
 $SLAVE_STORE = "$datadir/$SLAVE_STORE"; # make absolute path
 
 my $master_dev = get_dev("./$master");
 my $slave_dev = get_dev("./$slave");
 
 # parse volume name
 # it must have pefix "lv_" as additional prove that all runs ok
 my ($slave_pref, $serial) = $slave_dev->{lv} =~ /lv_(\S+)_(\d+)$/;
 
 die "cannot parse device $slave_dev->{lv}" unless $slave_pref;
 
 my $serial_new = $serial & 1 ^ 1;
 
 MakeQuery("FLUSH TABLES WITH READ LOCK");
 
 exec_sh "sync";
 
 my $newdev = $slave_dev->{dev};
 $newdev =~ s/_(\d+)$/_$serial_new/;
 
 my $slave_mount_dir     = "$SLAVE_STORE/${slave_pref}_${serial}";
 my $slave_mount_dir_new = "$SLAVE_STORE/${slave_pref}_${serial_new}";
 
 if (-e $slave_mount_dir_new) {
     my $real_path = Cwd::realpath($slave_mount_dir_new);
     if (`mount | grep $real_path`) {
         exec_sh "umount -f $slave_mount_dir_new";
     }
     exec_sh "rmdir $slave_mount_dir_new";
 }
 exec_sh "lvremove -f $newdev" if -e $newdev;
 exec_sh "lvcreate -s -L $SNAP_SIZE -n lv_${slave_pref}_${serial_new} $master_dev->{vg}/$master_dev->{lv}";
 exec_sh "mkdir $slave_mount_dir_new";
 exec_sh "mount -o nouuid,ro $newdev $slave_mount_dir_new";
 exec_sh "ln -s $slave_mount_dir_new $datadir/${slave}_$serial_new";
 exec_sh "mv -T ${slave}_$serial_new $slave";
 
 MakeQuery("FLUSH TABLES");
 
 exec_sh "umount $slave_dev->{dev}";
 exec_sh "lvremove -f $slave_dev->{dev}";
 exec_sh "rmdir $slave_mount_dir";
 
 sub exec_sh ($) {
     my $c = shift;
     print $c."\n";
     system($c) && exit(1);
 }
 
 sub MakeQuery {
     my $query = shift;
     my $qr = $query;
     my $sth;
     print $qr."\n";
     $sth = $dbh->prepare($query) || do {
         Die( "Can't make query '$qr': ",$dbh->errstr,"\n");
     };
     $sth->execute(@_) || do {
         Die( "Can't make query '$qr': ",$dbh->errstr,"\n");
     };
     return $sth;
 }
 
 sub get_dev ($) {
     my $dir = shift;
     open (DF, "df $dir |");
     $_ = <DF>;
     $_ = <DF>;
     my ($dev) = split(/\s+/,$_);
 
     die "wrong device $dev" unless $dev =~ m#/dev/mapper/(\S+)-(\S+)#;
 
     return {dev=>$dev, vg=>$1, lv=>$2};
 }