Вы здесь

Создание отказоустойчивого кластера для биллинговой системы UTM5 на базе FreeBSD

В статье рассматривается создание отказоустойчивого кластера для работы с биллинговой системой NetUP UTM на базе двух физических серверов. В качестве операционной системы используется FreeBSD. База данных mysql. Создание отказоустойчивого кластера для биллинговой системы на базе Gentoo Linux подробно рассматривается в соответствующей статье на сайте компании NetUP.

Рисунок 1. Схема кластера.

Поскольку у меня объем трафика довольно большой, для избежания потерь данных о трафике, NetFlow поток передается и принимается в отдельной подсети и выделенных под это отдельных физических интерфейсах. Физические интерфейсы em0,em1 и em2 имеют общие IP-адреса по которым доступен кластер. В случае выходя из строя сервера, либо проблем на любом из этих трех интерфейсах, резервный сервер забирает на себя функции мастера и соответствующие IP-адреса назначаются его интерфейсам.

Данная система построена на базе протокола CARP (Common Address Redundancy Protocol — протокол избыточности общего адреса)

Каждый сервер укомплектован четырьмя сетевыми интерфейсами стандарта Gigabit Ethernet.
Интерфейсы em0 поддерживают работу службы авторизации на базе протокола RADIUS и имеют общий IP 1.2.3.243 (сервер utm1 IP-1.2.3.244, utm2 IP-1.2.3.245)
Интерфейсы em1 предназначены для сбора статистики по протоколу NetFlow и имеют общий IP 172.31.31.2 (сервер utm1 IP-172.31.31.3, utm2 IP-172.31.31.4)
Интерфейсы em2 предназначены для доступа к ядру из интерфейсов администратора, дилера и кассира, а также сервиса http для доступа клиентов к «Личному кабинету».
Интерфейсы имеют общий IP 1.2.3.99 (сервер utm1 IP-1.2.3.108, utm2 IP-1.2.3.109). Интерфейсы em3 на обоих серверах соединяются кроссоверным патчкордом и используются для репликации БД.
В настройках UTM соответствующие сервисы настроены на работу на CARP IP. MySQL запущен только на IP интерфейса em3
Настройку кластера начнем с включения поддержки CARP ядром ОС. Поскольку CARP портирован во FreeBSD из OpenBSD, ключ carpdev не поддерживается. Это влечет за собой то, что CARP IP-адресс и IP-адреса интерфейсов для данного IP должны находиться в одной подсети. Для включения CARP, необходимо добавить в конфигурационный файл строку

device carp

Но, прежде чем пересобрать ядро, рекомендую пропатчить файл ip_carp.c Нужно это для того, чтобы можно было перехватывать события, происходящие с интерфейсами CARP при помощи devd. Если вы не планируете управлять кластером по событиям CARP, а запускать проверку по cron, патч можно не ставить.

sys/netinet/ip_carp.c.orig	2008-08-18 02:49:50.000000000 +0400
+++ sys/netinet/ip_carp.c	2008-08-18 02:16:50.000000000 +0400
@@ -35,6 +35,7 @@
#include
#include
#include
+#include
#include
#include
#include
@@ -2108,6 +2109,7 @@
static void
carp_set_state(struct carp_softc *sc, int state)

+	char vhid_buf[15], *dev_msg;

if (sc->sc_carpdev)
CARP_SCLOCK_ASSERT(sc);
@@ -2119,15 +2121,20 @@
switch (state) {
case BACKUP:
SC2IFP(sc)->if_link_state = LINK_STATE_DOWN;
+		dev_msg = "CARP_SLAVE";
break;
case MASTER:
SC2IFP(sc)->if_link_state = LINK_STATE_UP;
+		dev_msg = "CARP_MASTER";
break;
default:
SC2IFP(sc)->if_link_state = LINK_STATE_UNKNOWN;
+		dev_msg = "CARP_UNKNOWN";
break;
}
rt_ifmsg(SC2IFP(sc));
+	snprintf(vhid_buf, sizeof(vhid_buf), "vhid=%d", sc->sc_vhid);
+	devctl_notify("IFNET", SC2IFP(sc)->if_xname, dev_msg, vhid_buf);
}

После применения патча и добавления в ядро поддержки CARP, необходимо пересобрать ядро:

# cd /usr/src
# make buildkernel KERNCONF=UTM
# make installkernel KERNCONF=UTM

Где UTM — имя моего конфигурационного файла ядра.
Перед перезагрузкой можно сразу поправить файл /etc/rc.conf добавив в него: Сервер utm1:

cloned_interfaces="carp0 carp1 carp2"
ifconfig_em0="inet 1.2.3.244 netmask 255.255.255.240"
ifconfig_em1="inet 1.2.3.108 netmask 255.255.255.240"
ifconfig_em2="inet 172.31.31.3 netmask 255.255.255.240"
ifconfig_em3="inet 172.30.30.172 netmask 255.255.255.248"
ifconfig_carp0="vhid 1 pass secret 1.2.3.242 netmask 255.255.255.240"
ifconfig_carp1="vhid 2 pass secret 1.2.3.99 netmask 255.255.255.240"
ifconfig_carp2="vhid 3 pass secret 172.31.31.2 netmask 255.255.255.240"

Сервер utm2:

cloned_interfaces="carp0 carp1 carp2"
ifconfig_em0="inet 1.2.3.245 netmask 255.255.255.240"
ifconfig_em1="inet 1.2.3.109 netmask 255.255.255.240"
ifconfig_em2="inet 172.31.31.3 netmask 255.255.255.240"
ifconfig_em3="inet 172.30.30.173 netmask 255.255.255.248"
ifconfig_carp0="vhid 1 pass secret 1.2.3.242 netmask 255.255.255.240"
ifconfig_carp1="vhid 2 pass secret 1.2.3.99 netmask 255.255.255.240"
ifconfig_carp2="vhid 3 pass secret 172.31.31.4 netmask 255.255.255.240"

Все сетевые соединения должны быть подключены.
Перегружаем сервера и смотрим:

#utm1 carp0: flags=49 metric 0 mtu 1500
carp: MASTER vhid 1 advbase 1 advskew 0
carp1: flags=49 metric 0 mtu 1500
carp: MASTER vhid 2 advbase 1 advskew 0
carp2: flags=49 metric 0 mtu 1500
carp: MASTER vhid 3 advbase 1 advskew 0

#utm2 carp0: flags=49 metric 0 mtu 1500
carp: BACKUP vhid 1 advbase 1 advskew 100
carp1: flags=49 metric 0 mtu 1500
carp: BACKUP vhid 2 advbase 1 advskew 100
carp2: flags=49 metric 0 mtu 1500
carp: BACKUP vhid 3 advbase 1 advskew 100

Как видно, на сервере utm1 интерфейсы carp имеют статус MASTER, а на utm2 – BACKUP
Теперь, если отключить любой CARP интерфейс на сервере utm1, то все CARP интерфейсы на сервере utm2 перейдут в статус MASTER, а на сервере utm1 отключенный интерфейс будет иметь статус INIT, а остальные BACKUP.
После восстановления связности, сервер utm1 опять захватит роль MASTER сервера в кластере. При этом dmesg покажет следующее

em1: link state changed to DOWN
carp0: MASTER -> BACKUP (more frequent advertisement received)
carp2: MASTER -> BACKUP (more frequent advertisement received)
carp1: INIT -> BACKUP
em1: link state changed to UP
carp2: BACKUP -> MASTER (preempting a slower master)
carp0: BACKUP -> MASTER (preempting a slower master)

На этом оставим пока работу CARP и настроим репликацию БД.
Репликация будет осуществляться двухсторонняя, т.е. изменения, сделанные на основном сервере, будут передаваться на резервный и наоборот. Нужно это для того, чтобы при остановке основного сервера, не произошла рассинхронизация БД.
База данных должна быть типа innodb. Помимо того, что это рекомендовано NetUP, только с таким типом работает репликация.
Реплицировать будем только базу UTM5.
Останавливаем ядро UTM, если оно запущено, а также все приложения, которые имеют доступ к базе UTM5. На основном сервере делаем дамп базы и переносим его на резервный.
Выполняем на основном сервере

#utm1 mysql –u root -p
mysql> GRANT REPLICATION SLAVE ON UTM5.* TO 'utmuser'@'utm2.domain.ru' \
IDENTIFIED BY 'password';
mysql> flush privileges;
mysql> quit

Где utmuser – имя пользователя, который будет подключаться с резервного сервера для репликации, utm2.domain.ru имя сервера (или IP адрес) резервного сервера, password – пароль пользователя utmuser На резервном сервере выполняем тоже самое, заменив имя сервера на основной и при необходимости имя пользователя и пароль.
Останавливаем mysql на обоих серверах и вносим изменения в файл my.cnf (у меня это /var/db/mysql/my.cnf
На сервере utm1 добавляем:

log-bin
server-id=1
max_binlog_size = 1G
max_relay_log_size = 0
binlog-do-db=UTM5
master-host = 172.30.30.173
master-user = utmuser
master-password = password

На сервере utm2:

log-bin
server-id=2
max_binlog_size = 1G
max_relay_log_size = 0
binlog-do-db=UTM5
master-host = 172.30.30.172
master-user = utmuser
master-password = password

Запускаем mysql на основном сервере, затем на резервном. Заходим в mysql и проверяем:

mysql> show master status;
+-----------------+----------+--------------+------------------+
| File            | Position | Binlog_Do_DB | Binlog_Ignore_DB |
+-----------------+----------+--------------+------------------+
| nup2-bin.000003 |      106 | UTM5,UTM5    |                  |
+-----------------+----------+--------------+------------------+
1 row in set (0.00 sec)
mysql> show slave status;

Выводит таблицу состояния SLAVE, там куча ячеек, думаю, сами разберетесь.
Главное, на что стоит обратить внимание, значения полей File и Position в выводе show master status на первом сервере, должны совпадать со значениями полей Master_Log_File и Read_Master_Log_Pos на втором. И наоборот, значения полей File и Position в выводе show master status на втором сервере, должны совпадать со значениями полей Master_Log_File и Read_Master_Log_Pos на первом.
Если все так и есть, репликация работает нормально.
Для проверки, можно внести абонента на основном сервере и проверить, появилась ли соответствующая запись на втором.
Более подробно с настройкой репликации можно ознакомиться на сайте mysql.ru

Теперь переходим к процессу автоматизации.
Проблемой (а может быть и правильно, что так сделано) UTM, является то, что данные в БД предназначены для длительного хранения. Все последние изменения ядро хранит в ОЗУ и как следствие, запущенное на резервном сервере ядро не видит изменений, сделанных на основном сервере.
С другой стороны, согласен с разработчиками, что для корректной работы одновременно должно работать только одно ядро utm5_core.
Поэтому наша задача: контролировать статус сервер и в случае если SLAVE сервер берет на себя функции MASTER, запустить на нем ядро. И если MASTER сервер переходит в статус SLAVE, то гасить на нем ядро. Данную задачу можно реализовать через cron, периодически опрашивая состояние интерфейсов CARP. Ктомуже, это будет не лишним и в случае если по каким-либо причинам окажется, что на MASTER сервере ядро не запущено.
Скрипт проверяющий состояние сервера и запущены ли на нем необходимые модули приведен ниже:

#!/bin/sh
#Присвоение значения переменным
#Имя файла в который будут писаться сообщения о смене
#статуса и запуска процессов
log=/netup/utm5/log/_carp.log
#Имя директории с исполняемым скриптами UTM5
rc_dir=/usr/local/etc/rc.d
#Имя скрипта запуска ядра UTM5
core_script=utm5_core.sh
#Имя скрипта запуска RADIUS-сервера UTM5
#Если RADIUS-сервер не используется, закомментировать
radius_script=utm5_radius.sh
#Имя скрипта запуска RFW UTM5
#Если RFW-сервер не исползуется, закомментировать
#rfw_script=utm5_rfw.sh
#Включено ли логирование событий. 0-1
#Если включено, сто в файл определенный в переменной log будут писаться
#сообщения о смене состояния сервера и запуске или остановке сервисов UTM5
debug=1
DATE=`/bin/date +"%Y-%m-%d %H:%M:%S"`
CARP_STATUS="MASTER"

for CARP in `/sbin/ifconfig | /usr/bin/grep carp: | /usr/bin/awk \
'{print $2}'`
do
    if [ $CARP = "BACKUP" ] || [ $CARP = "INIT" ]; then
        CARP_STATUS="BACKUP";
    fi
done
if [ $core_script ];then
    UTM5_CORE=`/bin/ps ax | /usr/bin/grep utm5_core | /usr/bin/grep -v stop \
             | /usr/bin/grep safe | /usr/bin/awk '{print $1}'`
    if [ $CARP_STATUS = "BACKUP" ] && [ $UTM5_CORE ]; then
        $rc_dir/$core_script stop 1> /dev/null 2> /dev/null
        if [ $debug != 0 ];then
            /bin/echo $DATE "CARP is BACKUP && utm5_core is started. \
            Stop it." >> $log
        fi
    fi
    if [ $CARP_STATUS = "MASTER" ] && [ ! $UTM5_CORE ]; then
        $rc_dir/$core_script start 1> /dev/null 2> /dev/null
        if [ $debug != 0 ];then
            /bin/echo $DATE "CARP is MASTER && utm5_core is stoped. \
            Start it." >> $log
        fi
        sleep 5
    fi
fi
if [ $radius_script ];then
    UTM5_RADIUS=`/bin/ps ax | /usr/bin/grep utm5_radius \
              | /usr/bin/grep -v stop | /usr/bin/grep safe \
              | /usr/bin/awk '{print $1}'`
    if [ $CARP_STATUS = "BACKUP" ] && [ $UTM5_RADIUS ]; then
        $rc_dir/$radius_script stop 1> /dev/null 2> /dev/null
        if [ $debug != 0 ];then
            /bin/echo $DATE "CARP is BACKUP && utm5_radius is started. \
            Stop it." >> $log
        fi
    fi
    if [ $CARP_STATUS = "MASTER" ] && [ ! $UTM5_RADIUS ]; then
        $rc_dir/$radius_script start 1> /dev/null 2> /dev/null
        if [ $debug != 0 ];then
            /bin/echo $DATE "CARP is MASTER && utm5_radius is stoped. \
            Start it." >> $log
        fi
    fi
fi
if [ $rfw_script ];then
    UTM5_RFW=`/bin/ps ax | /usr/bin/grep utm5_rfw \
            | /usr/bin/grep -v stop | /usr/bin/grep safe \
            | /usr/bin/awk '{print $1}'`
    if [ $CARP_STATUS = "BACKUP" ] && [ $UTM5_RFW ]; then
        $rc_dir/$rfw_script stop 1> /dev/null 2> /dev/null
        if [ $debug != 0 ];then
            /bin/echo $DATE "CARP is BACKUP && utm5_rfw is started. \
            Stop it." >> $log
        fi
    fi
    if [ $CARP_STATUS = "MASTER" ] && [ ! $UTM5_RFW ]; then
        $rc_dir/$rfw_script start 1> /dev/null 2> /dev/null
        if [ $debug != 0 ];then
            /bin/echo $DATE "CARP is MASTER && utm5_rfw is stoped. \
            Start it." >> $log
        fi
    fi
fi

Данный скрипт проверяет, в каком состоянии находиться сервер:
Если сервер MASTER, то проверяется, запущенны ли сервисы utm5_core, utm5_radius и utm5_rfw (если какой то из сервисов не нужен, то нужно закомментировать строку определяющую имя запускающего данный сервис скрипта в начале файла. Если нужные сервисы не запущены, то дается команда на их запуск.
Если переменная debug=1 то информация о запуске или останове процессов пишется в файл определенный в переменной log
Для примера, сервер имеет статус MASTER но ядро UTM5 и RDIUS –сервер на нем не запущены. Дана команда на запуск этих сервисов.

2009-04-03 21:13:47 CARP is MASTER && utm5_core is stoped. Start it.
2009-04-03 21:13:47 CARP is MASTER && utm5_radius is stoped. Start it.

Сервер имеет состояние SLAVE но ядро и RADIUS на нем запущены, дана команда на их остановку

2009-04-03 21:10:22 CARP is BACKUP && utm5_core is started. Stop it.
2009-04-03 21:10:22 CARP is BACKUP && utm5_radius is started. Stop it.

На этом можно было бы и закончить, но пытливый читатель спросит, зачем мы патчили ip_carp.c в самом начале?
Отвечаю. Вариант с проверкой статуса сервера хоть и хорош, но выполнять его даже раз в минуту не очень хочется, а порой минута может стоить много денег и нервов. Поэтому, проверка состояния сервера и сервисов у меня выполняется раз в пять минут, а события вызываемые патченным сервисом carp обрабатываются налету при помощи демона devd.
Внесем изменения в файл /etc/devd.conf

notify 100 {
    match "system" "IFNET";
    match "type" "CARP_SLAVE";
    action "/usr/local/sbin/carp.sh $subsystem $vhid $type";
};

notify 100 {
    match "system" "IFNET";
    match "type" "CARP_MASTER";
    action "/usr/local/sbin/carp.sh $subsystem $vhid $type";
};
notify 100 {
    match "system" "IFNET";
    match "type" "CARP_UNKNOWN";
    action "/usr/local/sbin/carp.sh $subsystem $vhid $type";
};

Перезапускаем сервис devd:

# /etc/rc.d/devd stop
# /etc/rc.d/devd start

Внесенные изменения позволят запускать скрипты при наступлении определенных событий вызванных интерфейсами CARP.
Сделанный нами патч выдает три состояния CARP_MASTER – если carp интерфейс переходит в состояние MASTER, CARP_SLAVE – если carp интерфейс переходит в состояние SLAVE и CARP_UNCNOWN – если состояние интерфейса не удается определить.
При наступлении любого из этих событий, devd вызовет исполнение скрипта указанного в параметре action конфигурационного файла devd.conf для данного события. У меня это «/usr/local/sbin/carp.sh $subsystem $vhid $type», где $subsystem – имя carp интерфейса (carp0, carp1 и т.п.), $vhid – VHID интерфейса заданного в rc.conf, $type – тип события (CARP_SLAVE, CARP_MASTER, CARP_UNKNOWN).
По хорошему, нам нужно перехватывать только тип события, но в дальнейшем можно найти применения и для других параметров, пусть будут :)
Скрипт carp.sh

sleep 5
debug=1
log=/netup/utm5/log/_carp.log
bin_dir=/netup/utm5/bin
DATE=`/bin/date +"%Y-%m-%d %H:%M:%S"`
UTM5_CORE=`/bin/ps ax | /usr/bin/grep utm5_core | /usr/bin/grep -v stop \
         | /usr/bin/grep safe | /usr/bin/awk '{print $1}'`
TYPE=$3
if [ $TYPE = "CARP_MASTER" ] && [ ! $UTM5_CORE ]; then
    if [ $debug != 0 ];then
        /bin/echo "$DATE $TYPE ($1): Running \
        $bin_dir/check_utm5.sh" >> $log
    fi
    $bin_dir/check_utm5.sh
fi
if [ $TYPE = "CARP_SLAVE" ] && [ $UTM5_CORE ]; then
    if [ $debug != 0 ];then
        /bin/echo "$DATE $TYPE ($1):  Running \
        $bin_dir/check_utm5.sh" >> $log;
    fi
    $bin_dir/check_utm5.sh
fi

Данный скрипт проверяет только статус сервера и запущено ли ядро. Если сервер стал мастером и ядро не запущено или сервер стал ведомым и на нем запущено ядро, то выполняется скрипт check_utm5.sh описанный чуть выше.
Вобщемто, эта доработка позволяет сократить время поднятия сервисов в случае отказа основного сервера до 5-10 секунд.
Строка sleep 5 в начале файла нужна если у нас более чем один carp интерфейс. Поскольку при смене статуса одного из CARP интерфейсов, все остальные тоже меняют статус, соответсвенно и события вызывают данный скрипт несколько раз.
Так как запуск ядра UTM5 требует некоторого времени, чтобы ядро не запускалось несколько раз и введена данная задержка.
Кроме того, скрипт carp.sh пишет в логфайл время и тип наступившего события. Пример:

2009-04-03 21:12:09 CARP_MASTER (carp0): Running /netup/utm5/bin/check_utm5.sh
2009-04-03 21:12:09 CARP is MASTER && utm5_core is stoped. Start it.
2009-04-03 21:12:09 CARP is MASTER && utm5_radius is stoped. Start it.
2009-04-03 21:13:15 CARP_SLAVE (carp0):  Running /netup/utm5/bin/check_utm5.sh
2009-04-03 21:13:15 CARP is BACKUP && utm5_core is started. Stop it.
2009-04-03 21:13:15 CARP is BACKUP && utm5_radius is started. Stop it.

Скрипт carp.sh был запущен из devd с передачей типа CARP_MASTER. Скрипт поверил, что ядро не запущено и вызвал скрипт /netup/utm5/bin/check_utm5.sh, который в свою очередь проверил, что сервер имеет статус MASTER но на нем не запущено ядро и RADIUS сервер.
Дана команда на запуск данных сервисов.
Затем скрипт carp.sh был вызван с передачей типа CARP_SLAVE. Поскольку на сервере были запущены сервисы utm5_core и utm5_radius вызван скрипт check_utm5.sh который их остановил.

Категория: 
© 2009-2104 CTPAHHuK.RU