Мне поступила задача организовать отказоустойчивость веб-приложения из двух серверов. Веб-приложение включает в себя статические файлы и данные в СУБД MySQL.
Основное требование заказчика — веб-приложение должно быть всегда доступно и в случае сбоя в течении 5 минут сбой должен быть восстановлен.
2 сервера, территориально разнесенные в разных ЦОДах, должны удовлетворить данное требование.

Для решения данной задачи мной было выбрана ОС Debian Squeeze, т.к. разработчики веб-приложения используют Debian. Логику отказоустойчивости я решил делать через DNS, т.е. есть доменное имя test.ru. В качестве NS-серверов выступают мои 2 сервера, т.е. вся информация о зоне хранится локально. Если случается сбой с основным сервером, то ДНС переписывается и А-запись указывает на резервный сервер.
Помимо ДНС, встала проблема с синхронизацией файлов, причем необходима двунаправленная, т.к. на резервный сервер, в момент простоя основного, может быть залита информация. Для решения это задачи я использую пакет Unison.
Для синхронизации баз MySQL используется стандартная MySQL-репликация двунаправленная Мастер-Мастер. При однонаправленной репликации («master-slave») в случае записи в подчиненную базу данных база данных данные могут оказаться противоречивыми, что может привести к ошибке репликации. В случае двунаправленной репликации базы данных будут находиться в согласованном состоянии.
Арбитром всей этой системы будет выступать самописный скрипт, который я приведу ниже.
Имеем чистую систему Debian с установленными пакетами apache2, mysql-server. Их установку я расписывать не буду, благо в инете полно информации.
Главный сервер — master IP=10.1.0.1, Резервный сервер — slave IP=10.2.0.2
Файлы веб-приложения находятся в каталоге /site/web/. Apache на обоих серверах уже должен быть настроен на этот каталог.
Далее, для синхронизации файлов, нам необходимо сделать безпарольный доступ по SSH между серверами, для этого сгенерируем пару ключей:
и аналогично на втором сервере:
После этого необходимо убедиться, что данные в каталогах /site/web/ на обоих серверах идентичные и устанавливаем Unison:
создаем файл конфигурации /root/.unison/web.prf со следующим содержимым:
Теперь можно запустить синхронизацию следующей командой на главном сервере:
Чтобы сделать автоматическую синхронизацию раз в 5 минут, необходимо использовать следующий скрипт:
Сохраняем этот скрипт в файл /root/bin/sync.sh, даем право на запуск
И добавляем задание в CRON «crontab -e»:
Запуск данного скрипта каждые 5 минут.
Для репликации будем использовать пользователя MySQL replication с паролем some_password.
На главном master-сервере редактируем файл /etc/mysql/my.cnf. Вставляем следующие строки в раздел, относящийся к репликации:
и в этом же файле изменяем переменную bind-address, чтобы мускул был доступен на любом интерфейсе:
Заходим в MySQL под пользователем root и даем право пользователю replication подключаться к нашему серверу с резервного slave-сервера:
Переходим ко второму slave-серверу. Редактируем файл /etc/mysql/my.cnf. Вставляем следующие строки в раздел, относящийся к репликации:
и в этом же файле изменяем переменную bind-address, чтобы мускул был доступен на любом интерфейсе:
Заходим в MySQL под пользователем root и даем право пользователю replication подключаться к нашему серверу с главного master-сервера:
На обоих серверах проверяем, что процесс репликации запущен. Для этого выполняем следующее:
В выведенной информации нас интересуют три параметра:
Если указанные параметры на обоих серверах соответствуют указанным выше, то все замечательно, репликация настроена. Если нет, смотрим логи.
Для того, чтобы управлять доменной зоной test.ru, в настройках домена необходимо его делегировать на наши сервера. Пусть в глобальной сети наши сервера имеют уже доменные имена ns.master.my.com для главного сервера и ns.slave.my.com для резервного.
Только после делегирования ДНС-зоны на наши сервера мы сможем ей управлять.
На обоих серверах устанавливаем пакет bind9:
Добавляем в конфиг /etc/bind/named.conf строку для указания своих зон:
И создаем свой конфиг-файл /etc/bind/my-zones.conf со следующим содержимым:
На первом главном master-сервере создаем свою базу в файле /etc/bind/db.test.ru со следующим содержимым:
где ns.master.my.com — доменное имя этого сервера (домен my.com вымышленный)
Ключевой здесь является А-запись и время обновления — 10 секунд.
На втором резервном slave-сервере создаем свою базу в файле /etc/bind/db.test.ru со следующим содержимым:
Отличия от главного в SOA-записи.
Далее перегружаем bind на обоих серверах
И с какого-нибудь внешнего компьютера пробуем проверить А-запись домена test.ru
Например при помощи nslookup test.ru. Должен выдать адрес 10.1.0.1.
Если что-то не так, то смотрим логи.
Переходим к самому интересному — арбитраж, который будет все это разруливать и управлять ДНС-зоной.
За основу я принял работу сервисов SSH, DNS, HTTP и MySQL. Критическими, в данном случае, являются сервисы HTTP и MySQL, т.к. если хотя бы один из этих сервисов не работает на главном сервере, то необходимо все запросы отправить на резервный, дополнительно уведомить администратора о проблеме по электронной почте.
Чтобы проверить работоспособность сервис я использую скрипт, который при помощи TELNET проверяет доступность порта и выставляет значение в специальный файл-состояния.
Сначала опрашивается главный master-сервер и результаты заносятся в файл, затем опрашивается резервный slave-сервер, результаты также заносятся в файл; затем проверяется состояние критически важных сервисов на главном сервере, в случае проблем проверяется состояние на резервном сервере и, в зависимости от статуса, производится перезапись ДНС-зоны. Всю логику описывать не буду, она представленна в скриптах.
Для начала необходимо создать на обоих серверах специальный каталог:
Все скрипты хранятся в /root/bin/
первый скрипт /root/bin/master.sh отвечает за проверку главного master-сервера по сервисам DNS, SSH, HTTP, MySQL.
Второй скрипт /root/bin/slave.sh отвечает за проверку сервисов на резервном slave-сервере
Эти два файла идентичны, за исключением двух переменных $HOST и $SEMAFOR, в принципе можно было бы сделать один и использовать цикл, но я решил сделать их отдельными файлами.
Третий файл /root/bin/compare.sh служит для сравнения статусов сервисов на серверах и производит перезапись зоны ДНС.
И наконец собираем все эти скрипты воедино в один файл /root/bin/dnswrite.sh
Добавляем права на запуск данных скриптов
и добавляем задание в CRON чтобы запускался каждую минуту «crontab -e»:
Все готово.
Теперь мы имеем полностью автоматизированный комплекс отказоустойчивого веб-приложения!
Буду рад выслушать комментарии и принять замечания.
Основное требование заказчика — веб-приложение должно быть всегда доступно и в случае сбоя в течении 5 минут сбой должен быть восстановлен.
2 сервера, территориально разнесенные в разных ЦОДах, должны удовлетворить данное требование.

Для решения данной задачи мной было выбрана ОС Debian Squeeze, т.к. разработчики веб-приложения используют Debian. Логику отказоустойчивости я решил делать через DNS, т.е. есть доменное имя test.ru. В качестве NS-серверов выступают мои 2 сервера, т.е. вся информация о зоне хранится локально. Если случается сбой с основным сервером, то ДНС переписывается и А-запись указывает на резервный сервер.
Помимо ДНС, встала проблема с синхронизацией файлов, причем необходима двунаправленная, т.к. на резервный сервер, в момент простоя основного, может быть залита информация. Для решения это задачи я использую пакет Unison.
Для синхронизации баз MySQL используется стандартная MySQL-репликация двунаправленная Мастер-Мастер. При однонаправленной репликации («master-slave») в случае записи в подчиненную базу данных база данных данные могут оказаться противоречивыми, что может привести к ошибке репликации. В случае двунаправленной репликации базы данных будут находиться в согласованном состоянии.
Арбитром всей этой системы будет выступать самописный скрипт, который я приведу ниже.
Подготовка системы
Имеем чистую систему Debian с установленными пакетами apache2, mysql-server. Их установку я расписывать не буду, благо в инете полно информации.
Главный сервер — master IP=10.1.0.1, Резервный сервер — slave IP=10.2.0.2
Синхронизация файлов
Файлы веб-приложения находятся в каталоге /site/web/. Apache на обоих серверах уже должен быть настроен на этот каталог.
Далее, для синхронизации файлов, нам необходимо сделать безпарольный доступ по SSH между серверами, для этого сгенерируем пару ключей:
ssh-keygen -t rsa (passphrase не указываем) scp /root/.ssh/id_rsa.pub root@10.2.0.2:/root/.ssh/authorized_keys2
и аналогично на втором сервере:
ssh-keygen -t rsa (passphrase не указываем) scp /root/.ssh/id_rsa.pub root@10.1.0.1:/root/.ssh/authorized_keys2
После этого необходимо убедиться, что данные в каталогах /site/web/ на обоих серверах идентичные и устанавливаем Unison:
apt-get install unison
создаем файл конфигурации /root/.unison/web.prf со следующим содержимым:
# Определяем список директорий, которые будут синхронизированы root = /site/web root = ssh://root@10.2.0.2//site/web # Указываем сохранять права доступа и владельца owner = true times = true batch = true # Сохраняем лог с результатами работы в отдельном файле log = true logfile = /var/log/unison_sync.log
Теперь можно запустить синхронизацию следующей командой на главном сервере:
unison web
Чтобы сделать автоматическую синхронизацию раз в 5 минут, необходимо использовать следующий скрипт:
#!/bin/sh # проверка, не запущена ли уже синхронизация if [ -f /var/lock/sync.lock ] then echo lockfile exists! exit 1 fi /usr/bin/touch /var/lock/sync.lock /usr/bin/unison test /bin/rm /var/lock/sync.lock #End
Сохраняем этот скрипт в файл /root/bin/sync.sh, даем право на запуск
chmod +x /root/bin/sync.sh
И добавляем задание в CRON «crontab -e»:
*/5 * * * * /root/bin/sync.sh > /dev/null 2>&1
Запуск данного скрипта каждые 5 минут.
MySQL репликация
Для репликации будем использовать пользователя MySQL replication с паролем some_password.
На главном master-сервере редактируем файл /etc/mysql/my.cnf. Вставляем следующие строки в раздел, относящийся к репликации:
server-id = 1 log_bin = /var/log/mysql/mysql-bin.log expire_logs_days = 10 max_binlog_size = 100M binlog_ignore_db = mysql binlog_ignore_db = test master-host = 10.2.0.2 #ip-адрес резервного slave-сервера master-user = replication #пользователь для репликации master-password = some_password #пароль пользователя master-port = 3306
и в этом же файле изменяем переменную bind-address, чтобы мускул был доступен на любом интерфейсе:
bind-address = 0.0.0.0
Заходим в MySQL под пользователем root и даем право пользователю replication подключаться к нашему серверу с резервного slave-сервера:
mysql -u root -p Enter password: Вводим пароль для пользователя root, заданный во время установки >grant replication slave on *.* to 'replication'@'10.2.0.2' identified by 'some_password'; >flush privileges; >quit; /etc/init.d/mysql restart
Переходим ко второму slave-серверу. Редактируем файл /etc/mysql/my.cnf. Вставляем следующие строки в раздел, относящийся к репликации:
server-id = 2 log_bin = /var/log/mysql/mysql-bin.log expire_logs_days = 10 max_binlog_size = 100M binlog_ignore_db = mysql binlog_ignore_db = test master-host = 10.1.0.1 #ip-адрес главного master-сервера master-user = replication #пользователь для репликации master-password = some_password #пароль пользователя master-port = 3306
и в этом же файле изменяем переменную bind-address, чтобы мускул был доступен на любом интерфейсе:
bind-address = 0.0.0.0
Заходим в MySQL под пользователем root и даем право пользователю replication подключаться к нашему серверу с главного master-сервера:
mysql -u root -p Enter password: Вводим пароль для пользователя root, заданный во время установки >grant replication slave on *.* to 'replication'@'10.1.0.1' identified by 'some_password'; >flush privileges; >quit; /etc/init.d/mysql restart
На обоих серверах проверяем, что процесс репликации запущен. Для этого выполняем следующее:
#mysql —u root —p Enter password: Вводим пароль для пользователя root, заданный во время установки >show slave status \G
В выведенной информации нас интересуют три параметра:
Slave_IO_State: Waiting for master to send event Slave_IO_Running: Yes Slave_SQL_Running: Yes
Если указанные параметры на обоих серверах соответствуют указанным выше, то все замечательно, репликация настроена. Если нет, смотрим логи.
ДНС — сервера
Для того, чтобы управлять доменной зоной test.ru, в настройках домена необходимо его делегировать на наши сервера. Пусть в глобальной сети наши сервера имеют уже доменные имена ns.master.my.com для главного сервера и ns.slave.my.com для резервного.
Только после делегирования ДНС-зоны на наши сервера мы сможем ей управлять.
На обоих серверах устанавливаем пакет bind9:
apt-get install bind9
Добавляем в конфиг /etc/bind/named.conf строку для указания своих зон:
echo 'include "/etc/bind/my-zones.conf";' >> /etc/bind/named.conf
И создаем свой конфиг-файл /etc/bind/my-zones.conf со следующим содержимым:
zone "test.ru" { type master; file "/etc/bind/db.test.ru"; };
На первом главном master-сервере создаем свою базу в файле /etc/bind/db.test.ru со следующим содержимым:
$ORIGIN test.ru. $TTL 10 @ IN SOA ns.master.my.com. admin.my.com. ( 2 ; Serial 10 ; Refresh 10 ; Retry 10 ; Expire 10 ) ; Negative Cache TTL IN NS ns.master.my.com. IN NS ns.slave.my.com. ; @ IN A 10.1.0.1
где ns.master.my.com — доменное имя этого сервера (домен my.com вымышленный)
Ключевой здесь является А-запись и время обновления — 10 секунд.
На втором резервном slave-сервере создаем свою базу в файле /etc/bind/db.test.ru со следующим содержимым:
$ORIGIN test.ru. $TTL 10 @ IN SOA ns.slave.my.com. admin.my.com. ( 2 ; Serial 10 ; Refresh 10 ; Retry 10 ; Expire 10 ) ; Negative Cache TTL IN NS ns.master.my.com. IN NS ns.slave.my.com. ; @ IN A 10.1.0.1
Отличия от главного в SOA-записи.
Далее перегружаем bind на обоих серверах
/etc/init.d/bind9 restart
И с какого-нибудь внешнего компьютера пробуем проверить А-запись домена test.ru
Например при помощи nslookup test.ru. Должен выдать адрес 10.1.0.1.
Если что-то не так, то смотрим логи.
Арбитраж
Переходим к самому интересному — арбитраж, который будет все это разруливать и управлять ДНС-зоной.
За основу я принял работу сервисов SSH, DNS, HTTP и MySQL. Критическими, в данном случае, являются сервисы HTTP и MySQL, т.к. если хотя бы один из этих сервисов не работает на главном сервере, то необходимо все запросы отправить на резервный, дополнительно уведомить администратора о проблеме по электронной почте.
Чтобы проверить работоспособность сервис я использую скрипт, который при помощи TELNET проверяет доступность порта и выставляет значение в специальный файл-состояния.
Сначала опрашивается главный master-сервер и результаты заносятся в файл, затем опрашивается резервный slave-сервер, результаты также заносятся в файл; затем проверяется состояние критически важных сервисов на главном сервере, в случае проблем проверяется состояние на резервном сервере и, в зависимости от статуса, производится перезапись ДНС-зоны. Всю логику описывать не буду, она представленна в скриптах.
Для начала необходимо создать на обоих серверах специальный каталог:
mkdir /var/lock/sync/
Все скрипты хранятся в /root/bin/
первый скрипт /root/bin/master.sh отвечает за проверку главного master-сервера по сервисам DNS, SSH, HTTP, MySQL.
#!/bin/bash # Скрипт для провверки доступности портов вашего сервера # This script is licensed under GNU GPL version 2.0 or above # --------------------------------------------------------------------- ### Этот скрипт проверяет порты 22, 53, 80 и 3306 ### ### После двух неудачных проверок будет послано уведомление на email ### ###### Эта секция может модифицироваться###### WORKDIR="/root/bin/" SEMAFOR="/var/lock/sync/master.sem" MAILFILE="/root/bin/master_server_problem.txt" # адрес главного master-сервера HOST="10.1.0.1" HTTP="80" SSH="22" MYSQL="3306" DNS="53" PROTOCOLS="SSH HTTP MYSQL DNS" ### Мыло админа### EMAIL="admin@my.com" ########## ############ ###### В эту секцию не нужно вносить изменения##### ### Binaries ### TELNET=$(which telnet) ###Change dir### cd $WORKDIR ###Check if already notified### if [ -f $MAILFILE ]; then rm -rf $MAILFILE fi # Если файла со статусами нет, то создаем его if [ -f $SEMAFOR ]; then A=1 else echo "\ DNS 0 SSH 0 HTTP 0 MYSQL 0" > $SEMAFOR fi ###Проверяем СЕРВИСЫ### for PROTO in $PROTOCOLS do Num_PROTO=`cat $SEMAFOR | grep $PROTO | awk {'print $2'}` ( echo "quit" ) | $TELNET $HOST ${!PROTO} | grep Connected > /dev/null 2>&1 if [ "$?" -ne "1" ]; then #Ok echo "$PROTO PORT CONNECTED" if [ $Num_PROTO -ne "0" ]; then # !=0 if [ $Num_PROTO = "3" ]; then # ==3 echo "$PROTO PORT CONNECTING, AVALIBLE on server $HOST \n" >> $MAILFILE fi OLD_Line="$PROTO $Num_PROTO" NEW_Line="$PROTO 0" sed -i -e "s/$OLD_Line/$NEW_Line/g" $SEMAFOR fi else #Connection failure if [ $Num_PROTO -ne "3" ]; then if [ $Num_PROTO = "2" ]; then # ==2 send notification echo "$PROTO PORT NOT CONNECTING, FAILED on server $HOST \n" >> $MAILFILE fi OLD_Line="$PROTO $Num_PROTO" NEW_Line="$PROTO $(($Num_PROTO+1))" sed -i -e "s/$OLD_Line/$NEW_Line/g" $SEMAFOR fi fi done ###Send mail notification after 2 failed check### # я использую MUTT для отправки почты через свой SMTP-сервер 10.6.6.6 без авторизации # эту секцию можете модифицировать if [ -f $MAILFILE ]; then /usr/bin/mutt -x -e "set smtp_url=smtp://10.6.6.6" -e "set from="admin@my.com"" -s "Server problem" $EMAIL < $MAILFILE fi
Второй скрипт /root/bin/slave.sh отвечает за проверку сервисов на резервном slave-сервере
#!/bin/bash # Скрипт для провверки доступности портов вашего сервера # This script is licensed under GNU GPL version 2.0 or above # --------------------------------------------------------------------- ### Этот скрипт проверяет порты 22, 53, 80 и 3306 ### ### После двух неудачных проверок будет послано уведомление на email ### ###### Эта секция может модифицироваться###### WORKDIR="/root/bin/" SEMAFOR="/var/lock/sync/slave.sem" MAILADMIN=0 MAILFILE="/root/bin/slave_server_problem.txt" HOST="10.2.0.2" HTTP="80" SSH="22" MYSQL="3306" DNS="53" PROTOCOLS="SSH HTTP MYSQL DNS" ### Мыло админа### EMAIL="admin@my.com" ########## ###### В эту секцию не нужно вносить изменения##### ### Binaries ### TELNET=$(which telnet) ###Change dir### cd $WORKDIR ###Check if already notified### if [ -f $MAILFILE ]; then rm -rf $MAILFILE fi if [ -f $SEMAFOR ]; then A=1 else echo "\ DNS 0 SSH 0 HTTP 0 MYSQL 0" > $SEMAFOR fi ###Проверяем SSH### for PROTO in $PROTOCOLS do Num_PROTO=`cat $SEMAFOR | grep $PROTO | awk {'print $2'}` ( echo "quit" ) | $TELNET $HOST ${!PROTO} | grep Connected > /dev/null 2>&1 if [ "$?" -ne "1" ]; then #Ok echo "$PROTO PORT CONNECTED" if [ $Num_PROTO -ne "0" ]; then # !=0 if [ $Num_PROTO = "3" ]; then # ==3 echo "$PROTO PORT CONNECTING, AVALIBLE on server $HOST \n" >> $MAILFILE fi OLD_Line="$PROTO $Num_PROTO" NEW_Line="$PROTO 0" sed -i -e "s/$OLD_Line/$NEW_Line/g" $SEMAFOR fi else #Connection failure if [ $Num_PROTO -ne "3" ]; then if [ $Num_PROTO = "2" ]; then # ==2 send notification echo "$PROTO PORT NOT CONNECTING, FAILED on server $HOST \n" >> $MAILFILE fi OLD_Line="$PROTO $Num_PROTO" NEW_Line="$PROTO $(($Num_PROTO+1))" sed -i -e "s/$OLD_Line/$NEW_Line/g" $SEMAFOR fi fi done ###Send mail notification after 2 failed check### # я использую MUTT для отправки почты через свой SMTP-сервер 10.6.6.6 без авторизации # эту секцию можете модифицировать if [ -f $MAILFILE ]; then /usr/bin/mutt -x -e "set smtp_url=smtp://10.6.6.6" -e "set from="admin@my.com"" -s "Server problem" $EMAIL < $MAILFILE fi
Эти два файла идентичны, за исключением двух переменных $HOST и $SEMAFOR, в принципе можно было бы сделать один и использовать цикл, но я решил сделать их отдельными файлами.
Третий файл /root/bin/compare.sh служит для сравнения статусов сервисов на серверах и производит перезапись зоны ДНС.
#!/bin/bash # Скрипт для провверки доступности портов вашего сервера # This script is licensed under GNU GPL version 2.0 or above # --------------------------------------------------------------------- FILE_MASTER="/var/lock/sync/master.sem" FILE_SLAVE="/var/lock/sync/slave.sem" HOST_MASTER="10.1.0.1" HOST_SLAVE="10.2.0.2" DNSFILE="/etc/bind/db.test.ru" LOG="/var/log/dns_rewrite.log" PROTOCOLS="HTTP MYSQL" MASTER_COL=0 SLAVE_COL=0 COL=0 for PROTO in $PROTOCOLS do COL=$(($COL + 1)) Master_PROTO=`cat $FILE_MASTER | grep $PROTO | awk {'print $2'}` MASTER_COL=$(($MASTER_COL + $Master_PROTO)) Slave_PROTO=`cat $FILE_SLAVE | grep $PROTO | awk {'print $2'}` SLAVE_COL=$(($SLAVE_COL + $Slave_PROTO)) done MAX_COL=$(($COL * 3)) if [ $MASTER_COL = $MAX_COL ]; then # ==6 if [ $SLAVE_COL = "0" ]; then #==0 # Переписываем ДНС на Slave grep $HOST_MASTER $DNSFILE if [ "$?" -ne "1" ]; then #ok, rewrite sed -i -e "s/$HOST_MASTER/$HOST_SLAVE/g" $DNSFILE echo "Rewrite DNS to $HOST_SLAVE" >> $LOG /etc/init.d/bind9 restart fi fi else # check master if [ $MASTER_COL = "0" ]; then #==0 grep $HOST_SLAVE $DNSFILE if [ "$?" -ne "1" ]; then #ok, rewrite sed -i -e "s/$HOST_SLAVE/$HOST_MASTER/g" $DNSFILE echo "Rewrite DNS to $HOST_MASTER" >> $LOG /etc/init.d/bind9 restart fi else if [ $SLAVE_COL = "0" ]; then #==0 # Переписываем ДНС на Slave grep $HOST_MASTER $DNSFILE if [ "$?" -ne "1" ]; then #ok, rewrite sed -i -e "s/$HOST_MASTER/$HOST_SLAVE/g" $DNSFILE echo "Rewrite DNS to $HOST_SLAVE" >> $LOG /etc/init.d/bind9 restart fi fi fi fi
И наконец собираем все эти скрипты воедино в один файл /root/bin/dnswrite.sh
#!/bin/bash # Запускам проверку МАСТЕР /root/bin/master.sh # Запускам проверку SLAVE /root/bin/slave.sh # Запускам проверку баллов на перезапись ДНС /root/bin/compare.sh
Добавляем права на запуск данных скриптов
chmod +x /root/bin/*.sh
и добавляем задание в CRON чтобы запускался каждую минуту «crontab -e»:
*/1 * * * * /root/bin/dnswrite.sh /dev/null 2>&1
Все готово.
Теперь мы имеем полностью автоматизированный комплекс отказоустойчивого веб-приложения!
Буду рад выслушать комментарии и принять замечания.
