Зеркалируем форумы по-взрослому

Постановка задачи.


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

Итак, имеем два сервера.
Один стоит у нас здесь, под боком, второй в мастерхосте (ахтунг, реклама!) - очень качественный хостинг провайдер, который за всего то пару сотен американских рублей согласился приютить наш сервер. В нашей деревне трафик этого сервера стоил бы в разы дороже. :(
Первый сервер назовем сервер1, второй сервер (кто догадается?, правильно) сервер2.
Теперь самое главное - сервер1 - это сервер с апачем и форумом, причем форум для определенности - Инвижн, база соответственно MySQL.
Хотим чтобы местные клиенты (чей трафик нам дешев или вообще бесплатен) - ходили на сервер1, а всякие москвичи и другие японцы - на мастерхостовский сервак (раз у них трафик такой дешевый :) ).
Сложно? начинаем думать про всякие "hosts - сказать клиентам чтоб поправили", "копировать базу раз в день - типа будет почти онлайн" ?
НИФИГА! Это не есть путь правильного пертса!
Наш ответ - правильно настроенный DNS и репликация баз.
Ну вот, все сказал, теперь даже неинтересно :).

 

Решение задачи


1. Правильно настроенный днс



Пусть у нас форумы для определенности имеют адрес www.forum.ru. Нам нужно чтобы для местных клиентов выдавался адрес сервера1, а для всех остальных адрес сервера2
Идем на http://www.isc.org и находим по адресу http://www.isc.org/sw/bind/arm93/ сцылку на доки. Внимательно внимательно читаем, и понимаем - ключевое слово - view !
То есть это механизм который позволяет в зависимости от ип-адреса клиента который делает днс-запрос выдавать разную инфу
Пример - пусть наши местные сетки это адреса 10.0.0.0/8 172.16.0.0/12 192.168.0.0/16 и, например, 85.234.0.0/19. Им надо отдавать в качестве адреса форумов адрес сервера1, всем остальным - адрес сервера2.
Вот так примерно выглядит /etc/named.conf
options {
        directory "/var/named";
};


acl trusted {
        trusted_dns_serv_ip1;
        trusted_dns_serv_ip2;
        trusted_dns_serv_ip3;
//      ...		если надо - добавляем столько сколько надо ип-адресов серверов 
//                      кому будет разрешено схавать с нас зону полностью - ну т.е. описываем слейвы
        10.100.100.2; //по поводу этого адреса хинт позже - запомните его!
};

view "local_clients" {
		    //	вью для местных клиентов
    match-clients {
        10.0.0.0/8;
        172.16.0.0/12;
        192.168.0.0/16;
//      ... 		по желанию можно добавить еще местных клиентов
        85.234.0.0/19;
    };
    recursion yes;  //	разрешаем кроме всего прочего местным делать рекурсивные запросы, 
		    //	а всем остальным - только запросы в зоны которые мы сами и обслуживаем, ничего лишнего


    zone "." IN {   //	для местных клиентов чтобы рекурсивные запросы работали - описываем 
		    //	самое что ни на есть начало интернета, то бишь рутовые днс-сервера :)
        type hint;
        file "caching-example/named.ca";
    };		    

    zone "forum.ru" IN {
        type master;
        file "master/forum.ru-local";
        allow-update { none; };
        allow-transfer { trusted; };	//  разрешаем полностью забирать зону только доверенным ип-адресам
    };		    //	в файлике /var/named.master/forum.ru-local - будет описание зоны forum.ru для местных клиентов

    include "/etc/named.common.conf";
		    //	в файле /etc/named.common.conf будут описания всех зон,
		    //	 которые одинаково описываются и для "местных" и для "чужих"
};

view "outside_clients" {
    		    //	настройка "вью" для всех остальных клиентов
    match-clients { any; };
		    //	сюда попадут все остальные что не попали в предыдущие вью (причем view ведь может быть и не два, а больше :) )
    recursion no;
		    //	рекурсивные запросы от "чужих" выполнять не будем - у них свои днс сервера для этого должны быть, 
		    //	потому в этом вью и зону с рутовыми днс-серверами не описываем

    zone "forum.ru" IN {
        type master;
        file "master/forum.ru-outside";
        allow-update { none; };
        allow-transfer { trusted; };
    };		    //	в файлике /var/named.master/forum.ru-outside - будет описание зоны forum.ru уже для "чужих" товарищей

    include "/etc/named.common.conf";
		    //	здесь уже понятно - в /etc/named.common.conf будут описания всех зон,
		    //	которые одинаково описываются и для "местных" и для "чужих"
};

Теперь сами файлы /var/named.master/forum.ru-local и /var/named.master/forum.ru-outside Шапки опускаю, перехожу сразу к описанию хостов. Файлик /var/named.master/forum.ru-local
$ORIGIN forum.ru.
www                    86400   IN      A       ип_адрес_сервера1
И описание файлика /var/named.master/forum.ru-outside
$ORIGIN forum.ru.
www                    86400   IN      A       ип_адрес_сервера2

Ну вот собственно и все... Казалось бы :)
Это мы настроили мастера. А нужно ведь настраивать и слейв. НО!!! Если тупо прописать его как обычный слейв - он ведь возьмет только ту зону которая описана в соответсвующем для него вью, т.е. если его ип попадает в описание view "local_clients" он сольет зону которая лежит в forum.ru-local и соответственно если попадает в view "local_clients" то подхватится описание зоны из forum.ru-local.
Конечно же, ничего не мешает точно так же сделать несколько вью, и ручками скопировать недостающий файлик forum.ru-outside или forum.ru-local, и затем, при каждом изменении в зоне на мастере его редактировать и на слейве, но ведь это не путь настоящего пертса? Я ведь прав?
Для этого делаем такой финт - поднимем туннельный интерфейс с ип-адресом из другого view. :)
Не поняли? Обьясняю.
Пусть ип-адрес слейва не попадает в эти диапазоны для "local_clients" (допустим Слейв_ИП)
    match-clients {
        10.0.0.0/8;
        172.16.0.0/12;
        192.168.0.0/16;
        85.234.0.0/19;
    };

Заводим простейший туннель. На слейве
modprobe ipip
iptunnel add tunl1 mode ipip remote Мастер_ИП local Слейв_ИП
ifconfig tunl1 10.200.200.2 netmask 255.255.255.252
А на мастере соответственно
modprobe ipip
iptunnel add tunl2 mode ipip remote Слейв_ИП local Мастер_ИП
ifconfig tunl1 10.200.200.1 netmask 255.255.255.252

Т.е. выбран простой тип туннеля - "ipip", для его поддержки подгружаем модуль (если что - необходимо подгрузить его один раз :) ) Номера туннелей могут не совпадать. Более подробно я конечно хотел рассказать в статьях про недетский рутинг в линуксе... Но забыл/забил...
На этот интерфейс навесили адрес, который уже попадает в диапазоны для "local_clients". И соответственно при таком /etc/named.conf на слейве
...	// мышь отгрызла
view "local_clients" {
    match-clients {
        10.0.0.0/8;
        172.16.0.0/12;
        192.168.0.0/16;
        85.234.0.0/19;
    };

    zone "forum.ru" IN {
        type slave;
        file "slave/forum.ru-local";
        masters {
                10.200.200.1;
        };
    };

    include "/etc/named.common.conf";

};

view "outside_clients" {

    match-clients { any; };
    recursion no;

    include "/etc/named.common.conf";

    zone "forum.ru" IN {
        type slave;
        file "slave/forum.ru-outside";
        masters {
                Мастер_ИП;
        };
    };

};

все изменения на мастере будут применятся и на слейве, причем тоже в файлах для разных вью. Если адреса мастера и слейва у вас относятся к другим диапазонам - нужно если надо поменять порядок вью, или даже добавить нужный отдельный вью друг для друга для днс-серверов. Помните что порядок вью имеет значение и первый применяется первым.

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

2. Репликация баз данных



Итак, если бы у нас был статический контент - все было бы уже решено, что уж статические хтмл-ки то не зазеркалить? :)
Но у нас форум, причем инвижновский. Который все данные хранит в базе. MySQL.
Ага! скажут самые ленивые, давай мы скрипты форумов настроим на одну и ту же базу, выберем например на сервере2 и все.
Но нет, строго ответим мы. Какой толк от такого решения - трафик то не сэкономится! Представьте, куча локальных клиентов делают запрос странички с сервера1 (днс то мы им правильно настроили :) )
Сервер1 обращается N раз (по кол-ву запросов) к мускульной базе сервера2 - а это между прочим то же трафик и трафик нехилый, и только после этого сформированные странички с сервера1 попадут к местным клиентам.
Что же делать? (кто виноват вы и так все знаете - Билл Гейтс! :) )
Надо очень хитро синхронизировать базы между собой - может даже придумать какой-то хитрый механизм. Вы удивитесь, но такой механизм был придуман уже давно :). Хотя в мускуле появился _относительно_ недавно :)
Называется он репликация.
Вот про нее я и распишу, относительно конкретной конечно же задачи - задаче про зеркалирование форумов.
Тем более схема, которую я реализовывал на тот момент, в доке была описана нехотя, более того, говорилось вроде даже что типа работать может и будет, но болеть будет часто. Сейчас мускул уже подрос, стал сильным, 5 ветка уже пошла, даже триггеры хранимые процедуры и т.п. - все в общем как у взрослых появилось. Но я настраивал на 4.0.x, а начинал вообще на 3.23.x
Для начала опять-таки очень очень рекомендую почитать доки http://dev.mysql.com/doc/refman/4.1/en/replication.html
Идея очень простая - один из серверов будет у нас работать и как мастер и одновременно как слейв
На Сервере1 выполняем командочки
mysql> GRANT REPLICATION SLAVE ON invb_db.* TO 'user_for_repl_slave_on_serv1'@'ИП_Сервера2' IDENTIFIED BY 'pass_for_repl_slave_on_serv1';
mysql> flush privileges;
обьяснять где выполняем эти команды надеюсь не надо :)
и дорисовываем такую конфигу в /etc/my.cnf
server-id       = 2
master-host     = сервер2
master-user     = user_for_repl_slave_on_serv2
master-password = pass_for_repl_slave_on_serv2

log-bin
replicate-do-db=invb_db
slave-skip-errors=1062,1053

На Сервере2 соответственно выполняем
mysql> GRANT REPLICATION SLAVE ON invb_db.* TO 'user_for_repl_slave_on_serv2'@'ИП_Сервера1' IDENTIFIED BY 'pass_for_repl_slave_on_serv2';
mysql> flush privileges;

и дорисовываем аналогичную Серверу1 конфигу в /etc/my.cnf
server-id       = 3
master-host     = сервер1
master-user     = user_for_repl_slave_on_serv1
master-password = pass_for_repl_slave_on_serv1

log-bin
replicate-do-db=invb_db
slave-skip-errors=1062,1053

log-bin - означает что все меняющие (INSERT UPDATE DELETE) базу операции будут писаться в специальный лог-файл, который как раз будет читать слейв и применять к своей копии базы.
При этом, эти операции на слейве по умолчанию не попадают в собственный log-bin (туда попадают только "собственные" операции). За включение отвечает опция --log-slave-updates
Я намеренно не говорю что будут записыватся и другие операции, типа CREATE DROP ALTER и т.п. - будем считать что структура базы у нас не меняется и таких операций просто не будет. А если и будет - в принципе у меня работало :)
replicate-do-db=invb_db - Этой командой мы указываем реплицировать только одну базу (если у нас их много и вдруг проскочит команда для неизвестной слейву таблицы - слейв остановит процесс репликации)
slave-skip-errors=1062,1053 - Этим мы указываем игнорировать ошибки вида "Duplicate entry '%s' for key %d" и "Server shutdown in progress"
Т.е. первый тип ошибки - это дублированные данные. А кто сказал что будет легко? :). Все делается на грани фола :). Поскольку инвижн частенько пишет во всякие "временные" таблицы состояния пользователей - для одного ид будет разные записи :). Тут дело в том что инвижн использует ключи для таблицы "кто в онлайне" которые могут пересекатся. На таблице постов все ок. Зато появляется следующая фича, когда заходите на сервер и смотрите кто в онлайне на форумах - всегда видно юзеров с вашего же сервера :)
Теперь надо озаботиться чтобы на обоих серверах была одинаковая копия базы - как это сделать, описывать не буду, это в принципе простая задача, либо если MyISAM - остановить базу и тупо скопировать бинарные файлы, если InnoDB - чуток сложнее, закрыть сначала базу для изменений
mysql> FLUSH TABLES WITH READ LOCK;
Затем слить через mysqldump или LOAD DATA FROM MASTER - кому как удобнее
Ну а дальше все просто, запускаем мускул на обоих серверах, смотрим что показывает show master status; show slave status
Если на сервере2 что-то вроде этого
mysql> show master status;
+-------------------+----------+--------------+------------------+
| File              | Position | Binlog_do_db | Binlog_ignore_db |
+-------------------+----------+--------------+------------------+
| сервер2-bin.051   | 12913646 |              |                  |
+-------------------+----------+--------------+------------------+
1 row in set (0.00 sec)

mysql> show slave status;
+-------------+------------------------------+-------------+---------------+-----------------+---------------------+-----------------------+---------------+
| Master_Host | Master_User                  | Master_Port | Connect_retry | Master_Log_File | Read_Master_Log_Pos | Relay_Log_File        | Relay_Log_Pos |
+-------------+------------------------------+-------------+---------------+-----------------+---------------------+-----------------------+---------------+
| ип_сервер1  | user_for_repl_slave_on_serv1 | 3306        | 60            | сервер1-bin.001 | 236602231           | сервер2-relay-bin.051 | 17609694      |
+-------------+------------------------------+-------------+---------------+-----------------+---------------------+-----------------------+---------------+

+-----------------------+------------------+-------------------+-----------------+---------------------+------------+------------+--------------+---------------------+-----------------+
| Relay_Master_Log_File | Slave_IO_Running | Slave_SQL_Running | Replicate_do_db | Replicate_ignore_db | Last_errno | Last_error | Skip_counter | Exec_master_log_pos | Relay_log_space |
+-----------------------+------------------+-------------------+-----------------+---------------------+------------+------------+--------------+---------------------+-----------------+
| сервер1 -bin.001      | Yes              | Yes               | invb_db         |                     | 0          |            | 0            | 236602231           | 17609694        |
+-----------------------+------------------+-------------------+-----------------+---------------------+------------+------------+--------------+---------------------+-----------------+

1 row in set (0.00 sec)

А на другом сервере1 что-то вроде этого
mysql> show master status;
+-----------------+-----------+--------------+------------------+
| File            | Position  | Binlog_do_db | Binlog_ignore_db |
+-----------------+-----------+--------------+------------------+
| сервер1-bin.001 | 236597467 |              |                  |
+-----------------+-----------+--------------+------------------+
1 row in set (0.00 sec)

mysql> show slave status;
+-------------+------------------------------+-------------+---------------+-----------------+---------------------+-----------------------+---------------+
| Master_Host | Master_User                  | Master_Port | Connect_retry | Master_Log_File | Read_Master_Log_Pos | Relay_Log_File        | Relay_Log_Pos |
+-------------+------------------------------+-------------+---------------+-----------------+---------------------+-----------------------+---------------+
| ип_сервер2  | user_for_repl_slave_on_serv2 | 3306        | 60            | сервер2-bin.051 | 12912968            | сервер1-relay-bin.001 | 127136254     |
+-------------+------------------------------+-------------+---------------+-----------------+---------------------+-----------------------+---------------+

+-----------------------+------------------+-------------------+-----------------+---------------------+------------+------------+--------------+---------------------+-----------------+
| Relay_Master_Log_File | Slave_IO_Running | Slave_SQL_Running | Replicate_do_db | Replicate_ignore_db | Last_errno | Last_error | Skip_counter | Exec_master_log_pos | Relay_log_space |
+-----------------------+------------------+-------------------+-----------------+---------------------+------------+------------+--------------+---------------------+-----------------+
| сервер2-bin.051       | Yes              | Yes               | invb_db         |                     | 0          |            | 0            | 12912968            | 127136254       |
+-----------------------+------------------+-------------------+-----------------+---------------------+------------+------------+--------------+---------------------+-----------------+

1 row in set (0.00 sec)

То это значит что теперь ваши форумы зеркалируются совсем по-взрослому. А если репликация еще и не сломается после первого поста ;) - то вообще все зашибись.

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