понедельник, 23 февраля 2015 г.

Отказоустойчивый, масштабируемый кластер приложения с балансировкой нагрузки, часть 1

Заголовок броский, я бы даже сказал, слегка желтоват (желтая IT пресса - более унылой вещи трудно представить). Но, тем не менее, это правда, хотя, как известно, все скрывается в деталях. В данном случае детали - это, собственно, возможность самого ПО делить нагрузку или обеспечивать отказоустойчивость. Но хватит лирики, перейдем к постановке задачи.

Мне нужен почтовый релей, забегая вперед, скажу, что с таким же успехом это может быть любое другое приложение, например, web сервер. Могу показаться банальным занудой, но хочу еще раз повторить: каждый сервис требует отдельного анализа, и не факт, что желаемое удастся реализовать. Далее отразим заголовок статьи: релей должен быть отказоустойчивым и иметь возможность масштабирования. Читатель статьи может возразить, что для почтового релея все вышесказанное можно реализовать, добавив несколько MX записей в доменной зоне. К сожалению, я ограничен одним, уже существующим ip адресом релея, таким образом, вариант с MX не годится. Подведем итог: нужно несколько почтовых релеев, обеспечивающих отказоустойчивость сервиса, разделяющих нагрузку и использующих один ip адрес.

В плане выбора ПО все просто и традиционно - я буду использовать дистрибутив oracle linux 7 (почти аналогичны ему centos 7 или rhel 7), в качестве smtp сервиса - postfix. Для балансировки будет использоваться решение LVS, поскольку ее частью является подсистема linux ядра - ipvs, быстрый и надежный вариант балансировки трафика на 4-м уровне сетевой модели OSI. 

В качестве управляющего ПО кластером используется keepalived. Данное ПО использует протокол VRRP для обеспечения высокой доступности (high avialability) нод кластера, а также позволяет организовать мониторинг доступности приложения.

Оставшиеся вводные данные:
Используемая сеть: 192.168.3.0/24
IP серверов по порядку: 192.168.3.[101-105]
Общий IP: 192.168.3.100

Обычно в стандартной практике делают отдельный кластер балансировки и группу серверов приложения, к примеру, рекомендуемое решение от Red Hat. Особенность данной реализации заключается в том, что каждый сервер является сервером приложения и балансировщиком трафика одновременно. Один из балансировщиков является активным, остальные резервные. В моем случае используются четыре релея, расположенные по два в двух географически разнесенных ЦОДах. Ниже приведен рисунок, иллюстрирующий прохождение трафика в кластере, к которому мы будем возвращаться при описании процесса работы.


Посмотрите, что происходит: входящее соединение попадает на активную ноду (кластерный mac не используется), активная нода в соответствии с правилами балансировки обрабатывает соединение самостоятельно или передает его на одну из соседних нод, т.е. в обработке принимают участие все ноды кластера, затем нода отвечает клиенту. Таким образом весь входящий трафик проходит через одну активную ноду, а соответствующий исходящий трафик отправляется напрямую клиенту.

Перейдем к установке и настройке. Все написанное ниже справедливо для всех нод кластера, если не указано обратное. Установку и настройку postfix рассматривать не будем, убедитесь только что postfix использует все доступные сетевые интерфейсы:

# grep "inet_interfaces" /etc/postfix/main.cf | grep -v "^#"
inet_interfaces = all

Отключим firewalld, его можно будет настроить позднее, после того как будете твердо уверены, что кластер функционирует должным образом:

# systemctl disable firewalld.service
# systemctl stop firewalld.service

Установим keepalived, ipvsadm и модуль python ipaddr, модуль нужен для скрипта:

# yum -y install keepalived ipvsadm python-ipaddr

Разберем конфигурационный файл keepalived:

# Секция определения общих параметров
global_defs {
   notification_email {
      # Список почтовых адресов, по которым будут приходить оповещения о
      # недоступности нод или сервисов
      unix_adm@domain.ru
   }
   # Email от которого будет рассылаться почта
   notification_email_from keepalived@domain.ru
   # Адрес smtp сервера и время, в течение которого будет производится попытка
   # отправки почты
   smtp_server 192.168.3.1
   smtp_connect_timeout 30
   # Идентификатор сервера
   router_id server01.domain.ru
}

# Секция описания экземпляров VRRP (перемещаемые IP)
vrrp_instance mail_relay {
    # Начальное состояние при запуске, на остальных серверах должно быть BACKUP
    state MASTER
    # Интерфейс, на котором будет работать экземпляр VRRP
    interface eth0
    # Идентификатор экземпляра VRRP, должен быть идентичен на всех нодах.
    virtual_router_id 11
    # Приоритет при выборе MASTER'а, нода с большим приоритетом становится
    # активной.
    priority 100
    # Интервал уведомлений о состоянии, которые рассылает MASTER нода.
    # При отсутствии уведомлений произойдут перевыборы.
    advert_int 2
    # Секция аутентификации, пароль должен быть идентичен на всех нодах,
    # используется первые 8 символов.
    authentication {
        auth_type PASS
        auth_pass mrelay
    }
    # Список адресов, которые будут добавлены на ноду при выборе ее MASTER
    virtual_ipaddress {
        192.168.3.100
    }
    # Произвести миграцию состояния MASTER при появлении ноды с более высоким
    # приоритетом. В случае установки опции в nopreempt при появлении ноды с 
    # более высоким приоритетом MASTER нодой останется прежняя нода.
    preempt
    # Скрипт, который надо выполнить при событии, когда нода становится MASTER
    notify_master "/etc/keepalived/bypass_ipvs.py -r 192.168.3.100"
    # Скрипт, который надо выполнить при событии, когда нода становится SLAVE
    notify_backup "/etc/keepalived/bypass_ipvs.py -a 192.168.3.100"
    # Скрипт, который надо выполнить при событии ошибки смены роли ноды
    notify_fault "/etc/keepalived/bypass_ipvs.py -a 192.168.3.100"
}

Что выполняет скрипт: при установке состояния BACKUP на ноде он добавляет правило в iptables, которое заменяет (NAT) общий IP в пакетах входящего трафика на локальный IP ноды, и наоборот - для ответного трафика:

-A PREROUTING -t nat -d 192.168.3.100/32 -p tcp -j REDIRECT

Таким образом реализуется совместная обработка трафика всеми нодами, посмотрите еще раз рисунок вверху. В случае, когда нода становится MASTER, скрипт убирает правило натирования, поскольку общий IP принадлежит этой ноде. Скачать скрипт можно здесь.

Несколько замечаний: 
  • state - если вы уверены в качестве сети, можно на всех нодах кластера выставить BACKUP, в этом случае мастер определится в результате выборов. 
  • priority - в случае равных приоритетов побеждает нода с большим IP.
  • nopreempt - при данной опции state должен быть установлен в BACKUP.
  • если установить опции state BACKUP, одинаковый priority и nopreempt на все ноды, мы получим идентичный конфигурационный файл, что очень удобно в системах централизованного сопровождения серверов. Но вместе с тем шансы возникновения ситуации "split brain" в результате каких либо проблем также увеличиваются.
Продолжим далее рассматривать keepalived.conf:

# Секция конфигурации политики балансировки (LVS) и описание серверов приложения
virtual_server 192.168.3.100 25 {
    # Период опроса серверов приложения
    delay_loop 2
    # Планировщик балансировки, wlc - Weighted Least-Connections Scheduling.
    lb_algo wlc
    # Метод передачи пакетов на сервера приложений, dr - Direct Routing
    lb_kind DR
    protocol TCP

    # Секция описание сервера приложения 192.168.3.101
    real_server 192.168.3.101 25 {
        # Вес сервера
        weight 1
        # Секция проверки доступности приложения, в данном случае выполняется
        # проверка доступности smtp приложения. 
        SMTP_CHECK {
            connect_timeout 5
            retry 3
            # Имя в команде smtp helo
            helo_name smtpchecker.domain.ru
        }
    }
    # Далее однотипные описания серверов для 192.168.3.102 и т.д.
}

Еще несколько замечаний:
  • lb_algo wlc - распределяет большее количество запросов серверам с наименьшим количеством активных подключений в соответствии с их весом.
  • lb_kind DR - маршрутизация пакета, в этом случае пакет с активной ноды передается на резервную следующим образом: производится замена MAC адреса в поле destination MAC в ethernet заголовке пакета с MAC адреса активной ноды на MAC адрес сервера приложения. IP адрес остается без изменения. В свою очередь сервер приложения (нода в состоянии BACKUP) получив пакет, натирует общий IP адрес в поле destination IP в свой собственный. 
  • со всеми вариантами балансировки, передачи пакетов, методах проверки доступности приложения можно ознакомиться на сайте проекта LVS.
Ссылка на файл keepalived.conf

Запустим keepalived и проверим что получилось:

# systemctl enable keepalived.service
# systemctl start keepalived.service

$ telnet 192.168.3.100 25
Trying 192.168.3.100...
Connected to 192.168.3.100.
Escape character is '^]'.
220 server01.domain.ru ESMTP Postfix
quit
221 2.0.0 Bye
Connection closed by foreign host.
[valentine@server ~]$ telnet 192.168.3.100 25
Trying 192.168.3.100...
Connected to 192.168.3.100.
Escape character is '^]'.
220 server02.domain.ru ESMTP Postfix
quit
221 2.0.0 Bye
Connection closed by foreign host.

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

Посмотреть логи keepalived:

# journalctl -u keepalived.service

Посмотреть, на какой ноде находится общий IP, привычная команда ifconfig его не показывает:

# ip addr show
...
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP qlen 1000
    link/ether 52:54:00:0d:5a:1a brd ff:ff:ff:ff:ff:ff
    inet 192.168.3.101/24 brd 192.168.3.255 scope global dynamic eth0
       valid_lft 70403sec preferred_lft 70403sec
    inet 192.168.3.100/32 scope global eth0
...

Или более современный вариант:

# nmcli device show eth0
...
IP4.ADDRESS[1]:                         ip = 192.168.3.101/24, gw = 192.168.3.1
IP4.ADDRESS[2]:                         ip = 192.168.3.100/32, gw = 192.168.3.1
...

Посмотреть статистику балансировки:

# ipvsadm -Ln
IP Virtual Server version 1.2.1 (size=4096)
Prot LocalAddress:Port Scheduler Flags
  -> RemoteAddress:Port           Forward Weight ActiveConn InActConn
TCP  192.168.3.100:25 wlc
  -> 192.168.3.101:25             Route   1      2          14         
  -> 192.168.3.102:25             Route   1      3          9      
...

Посмотреть текущие соединения:

# ipvsadm -Lnc
IPVS connection entries
pro expire state       source             virtual            destination
TCP 13:20  ESTABLISHED 192.168.3.3:59527  192.168.3.100:25   192.168.3.102:25
...

Если в сети несколько vrrp экземпляров, можно проверить номера идентификаторов

# tcpdump -nnn ip proto 112
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on eth0, link-type EN10MB (Ethernet), capture size 65535 bytes
21:11:36.244824 IP 192.168.3.101 > 224.0.0.18: VRRPv2, Advertisement, vrid 11, prio 100, authtype simple, intvl 1s, length 20

Удачных внедрений!

Upd: продолжение, для тех кому мало будет 4 ноды.