Построение высоко доступного web кластера с балансировкой нагрузки на базе linux

0. Intro

Надеюсь сия статья будет кому нибудь полезна. Свои "фи" | исправления | комментарии | просто благодарности направляйте на finger (at) evpanet (dot) com.

0.1 Почему кластер?

Хотя бы потому, что с ростом производительности системы цена на нее растет в несколько раз быстрее. В случае с кластером, практически линейная маштабируемость производительности за линейную маштабируемость денег. Мощность наращивается просто добавлением машины в кластер. Любая отдельная часть системы может быть отключена без потери доступности всего ресурса(например это может понадобиться при апгрэйде системы). Это не говоря уже об отказоустойчивости.

Прежде всего, давайте определимся с проблемой, которую будем решать.

1. Описание проблемы

"Мой сайт просто обязан быть доступен 24 часа в сутки, 7 дней в неделю. Система, обеспечивающая доступ к нему, должна позволять легко нарастить мощность без потери доступности." Решение - HA/LB Cluster.

Вот некоторые понятия о которых мы будем говорить в этой статье:
Cluster
некоторое количество однотипных объектов соединенных вместе, применительно к компьютерам - несколько компьютеров предоставляющих однотипные ресурсы
HA (High Availability)
система, которая при сбое одного из компонентов продолжает нормальную работу
LB (Load Balanced)
предоставляет сервис из множества точек с одинаковыми ресурсами
Значит нас интересует набор программных продуктов который позволит построить высоко доступный web сервис на кластерной основе. Но:
а) мы хотим иметь действительно балансировку нагрузки, а не просто Round Robin.
б) система должна правильно вычислять "старых" пользователей и отправлять их на "старый" сервер, если он доступен. Например мы хостим книжный интернет магазин. Приобретая книгу мы кладем ее в корзину. Сервер запомнил что находится у нас в корзине. При следующем запросе оказывается что другой сервер в этот момент времени менее загружен и LB отправляет запрос на него. Этого никак нельзя допустить. Второй сервер ничего не знает о нашей корзине.
в) web сервер должен быть доступен при крахе любой отдельной части кластера.
г) наконец, мы люди любящие халяву, а значит коммерческое решение нас не устраивает.

После всех поставленных условий выбор оказывается не велик. Я остановился на mod_backhand в качестве балансировщика нагрузки и Wackamol в качестве основы для построения отказоустойчивой системы.

2. Mod_backhand

Mod_backhand это модуль для web сервера apache 1.3.X. Он собирает информацию о загруженности каждой системы в кластере. Когда приходит запрос, mod_backhand сначала определяет какие действия он должен произвести с ним (если этот запрос попадает в так называемую зону действия). Зоны действия mod_backhand определяются директивами <Files>, <Directory> и <Location> в httpd.conf и .htaccess файлах. Если запрос входит в зону действия mod_backhand он скармливается каждой определенной для зоны функции выбора кандидата. Функция выбора кандидата может сортировать и полностью изменять список серверов-кандидатов на выполнение запроса, а так же менять способ доставки выбранному серверу запроса(HTTP Redirect или Proxy). После выполнения последней функции выбора кандидата, первый сервер в списке кандидатов получает запрос. По умолчанию он получает его методом proxy - т.е. сервер к которому пришел запрос сам просит выполнить его сервер который оказался наиболее удачным кандидатом, а потом отдает результат его выполнения клиенту. Возможен и другой метод - HTTP Redirect. Он использует стандартные средства протокола http для того чтобы сказать клиенту что ресурс нужно запросить у другого сервера.

2.1 Установка

Установить mod_backhand проще простого. Я предпочитаю делать это во время сборки apache:

# cd /var/tmp
# ls
apache_1.3.28.tar.gz
mod_backhand-1.2.2.tar.gz
# gzip -d < apache_1.3.28.tar.gz | tar xf -
# gzip -d < mod_backhand=1.2.2.tar.gz | tar xf -
# ls
apache_1.3.28
apache_1.3.28.tar.gz
mod_backhand-1.2.2
mod_backhand-1.2.2.tar.gz
# cd mod_backhand-1.2.2
# ./precompile ../apache_1.3.28/
Copying source into apache tree...
Copying sample cgi script and logo into htdocs directory...
Adding to auto mod_so inclusion to configure...
Adding libs to Apache's Configure...
Adding to Apache's Configuration.tmpl...
Nullifying extra shared libraries for Linux
Modifying httpd.conf-dist...
Updating Makefile.tmpl...

Now change to the apache source directory:
../apache_1.3.28
And do a ./configure...

Затем конфигурируем Apache с поддержкой mod_backhand, компилируем и устанавливаем. Например так:
$ cd /var/tmp/apache_1.3.28
$ ./configure --prefix=/usr/local/apache --enable-module=so \
--enable-module=rewrite --enable-shared=rewrite \
--enable-module=speling --enable-shared=speling \
--enable-module=info --enable-shared=info \
--enable-module=include --enable-shared=include \
--enable-module=status --enable-shared=status \
--enable-module=backhand --enable-shared=backhand
$ make
# make install

2.2 Информация о ресурсах

Страница на которой будет находится статистика собранная mod_backhand с других узлов кластера поределяется директивой apache SetHandler backhand-handler. Если вы устанавливали mod_backhand вкомпилированным в apache (методом описанным в этой статье), то в httpd.conf уже есть директива:
<Location "/backhand/">
    SetHandler backhand-handler
</Location>
Значит статистика будет доступна по адрессу http://имя_узла/backhand/.

mod_backhand stats

2.3 Функция выбора кандидата.

Функция выбора кандидата определяет какие серверы являются кандидатами на выполнение запроса и сортирует их в соответствии с правилами описанными в настройке mod_backhand.
Например, в файле конфигурации apache мы можем указать что все файлы заканчивающиеся на .php будут обрабатываться наиболее доступными и наименее загруженным сервером:
<Files ~ "\.php$">
    Backhand byAge
    Backhand byLoad
</Files>
Когда кто нибудь попросит apache страницу заканчивающуюся на .php, сначала функция выбора кандидата byAge отсеет все сервера которые не отвечали в течении поледних 5 секунд, затем функция byLoad отсортирует этот список от менее загруженных к более загруженным. Так как больше не определенно функций выбора кандидата, обрабатывать запрос будет доверено первому серверу из списка т.е. наименее загруженному.

Встроенные функции выбора кандидата:
off отключает использование mod_backhand для текущей "зоны покрытия"
addSelf добавляет локальный сервер в конец списка
byAge [время в секундах] выбрасывает из списка серверы, с которых мы не слышали привета в течении установленного времени. По умолчанию - 20 сек
byLoad [преимущество] сортирует список серверов от менее загруженных к более загруженным. Преимущество - величина которая позволяет завысить оценки для локального сервера. Она будет добавлена ко всем серверам кроме локального
byBusyChildren [преимущество] сортирует список кандидатов в соответствии количеством дочерних процессов Apache в состоянии SERVER_BUSY от меньшего к большему
byCPU удаляет все сервера кроме этого в соответствии с временем простоя CPU. Не используйте если вы действительно незнаете почему используете это
byLogWindow выбрасывает все сервера кроме первых log по основанию 2 от n. Если в списке 17 серверов, останется первых 4
byRandom случайно перемешивает список серверов
byCost эта функция пытается определить стоимость выполнения запроса для каждого сервера в кластере и выбирает наиболее дешевый. Метод определения стоимости обсуждается в документе "A cost-Benefit Framework for Online Managment of a Metacomputing System"
HTTPRedirectToIP заставляет mod_backhand использовать пере направление HTTP, вместо стандартного режима Proxy
bySession [идентификатор] эта функция будет пытаться найти печеньку(cookie) или переменную запроса именуемую идентификатором, декодировать первые 8 байт и получить IP адрес. Попробует найти IP адрес в списке серверов и если найдет то оставит в списке только его. Если что-то не получится список останется нетронутым. Эта функция очень полезна для скриптов исполняемых на стороне сервера и может быть использована для нормальной работы приложений использующих пользовательские сессии. По умолчанию идентификатор равен "PHPSESSID="

Можно и самому написать функцию работы с кандидатами, но здесь это обсуждать мы не будем.

2.4 Настройка mod_backhand

Все параметры настройки mod_backhand находятся в файле настроек apache httpd.conf. Их совсем немного, посему давайте остановимся на них подробней.
UnixSocketDir <dir> - директория в которой mod_backhand будет сохранять необходимую для работы информацию. Она должна быть доступна на запись для apache (nobody).
MulticastStats <IP-Addr>:<Port>[,<TTL>] - задает адрес на который сервер отправляет свое состояние. Может быть широковещательным, может уникальным. Разрешается использование нескольких адресов и/или сетей путем использования нескольких директив.
AcceptStats <IP-Addr>/<Netmask> - задает сеть из которой мы будем принимать информацию от других серверов.

2.5 Пример конфигурации

Давайте рассмотрим пример в котором у нас будет 5 серверов.
Один из них будет управляющим. Он не будет сам выполнять работу, а будет отдавать ее другим. Он будет иметь два интерфейса: eth0 - во внешний мир и eth1(ip - 195.5.3.183) - в сеть с кластером(ip - 192.168.10.1).
На остальные четыре сервера ложится основная нагрузка по обработке запросов. Они имеют ip адреса от 192.168.10.2 до 192.168.10.5.
Управляющий сервер будем называть Director, остальные www1 - www4.

cluster's schem

Все системы имеют одинаковые настройки mod_backhand.

<IfModule mod_backhand.c>
  UnixSocketDir /usr/local/apache/backhand
  MulticastStats 192.168.10.255:4445,1
  AcceptStats 192.168.10.0/24
                                                                               
  <Location "/backhand/">
    SetHandler backhand-handler
  </Location>
</IfModule>

Director помимо всего прочего имеет зону покрытия:
<Files ~ "*">
    backhand bySession
    backhand byAge
    backhand removeSelf
    backhand byLoad
</Files>

Все. Наш кластер с балансировкой нагрузки можно использовать.
Конечно если упадет Director то и весь наш кластер будет не доступен. Самое время рассмотреть Wackamole.

3. Wackamole

Wackamole позволяет сделать наш кластер высоко доступным.
Делается это за счет того что wackamole распределяет имеющийся пул виртуальных адресов между несколькими машинами. Таким образом если одна из машин "умрет" остальные тут же подхватят ее виртуальный IP, что позволяет быть каждому отдельному виртуальному IP быть доступным в любое время. Виртуальным IP называются потому что ни одна из машин не "владеют" этим адресом. Он может передоватся от одной к другой. В любой момент времени IP владеет не больше одной машины.

Это позволяет нам использовать множественные DNS RR записи, не беспокоясь о том что серверы могут быть недоступны. Если одна из машин "упадет" виртуальные IP адреса, которыми она владела, распределятся между другими машинами кластера.

3.1 Установка

Wackamole работает используя Spread toolkit. Взять его можно на http://www.pread.org/.
Собираем сначала Spread:

$ cd spread-src-3.17.1
$ ./configure --prefix=/usr/local/spread
$ make
# make install
Следует помнить, что для старта демона spread необходимо наличие пользователя и группы spread т.к. после старта он делает setuid, а также каталог /var/run/spread для chroot в целях безопасности.

А теперь wackamole:

$ cd ../wackamole-2.0.0
$ ./configure --prefix=/usr/local/wackamole --with-cppflags=-I/usr/local/spread/include \
  --with-ldflags=-L/usr/local/spread/lib
$ make all
# make install

Не забудьте, что вы должны обеспечить вызов динамических библиотек из /usr/local/spread/lib.

3.2 Настройка wackamole.conf

Spread - указывает где искать Spread демон. Значение 4803 означает что он будет искаться на локальной машине, на порту по умолчанию для spread.
SpreadRetryInterval - временной промежуток через который wackamole будет пытаться установить связь с Spread если она разорвалась.
Group - указывает Spread группу к которой wackamole присоединится. Эта группа должна быть своя для каждого кластера.
Control - указывает путь к Unix Domain сокету, используемый wackamole.
Arp-cache - указывает время через которое происходит обновление информации в ARP кэше.

Секция balance:
Interval - указывает длину раунда балансировки.
AcquisitionsPerRound - указывает количество виртуальных интерфейсов задействованных в раунде балансировки.

Секция VirtualInterfaces определяет все виртуальные интерфейсы, относящиеся к кластеру. Виртуальный интерфейс может иметь три вида:
1. ifname:ipaddr/mask
2. { ifname:ipaddr/mask }
3. {
    ifname:ipaddr/mask
    ifname:ipaddr/mask
    ...
   }
ifname - имя интерфейса из ifconfig -a, без каких либо виртуальных номеров.

Директива Prefer позволяет указать ip адреса предпочитаемые для этой машины.

Секция Notify описывает каким машинам должны посылаться arp-spoof'ы когда IP адрес передается машине. IP адреса в секции задаются подобно секции VirtualInterfaces.

3.2 Пример настройки

У нас есть четыре Linux сервера с запущенным Apache, содержащим картинки для большого web сайта. У них ip от 192.168.100.183 до 192.168.100.186. Для них выделены 4 виртуальных IP, по которым и будет происходить доступ к картинкам - 192.168.100.200 - 192.168.100.203. Все они должны откликаться на images.example.com. Spread установлен на всех машинах и висит на порту 4803. Каждая машина имеет по одному интерфейсу - eth0. Default route для всех - 192.168.100.1
Wackamole.conf на каждой машине должен выглядеть следующим образом:
Spread = 4803
SpreadRetryInterval = 5s
Group = wack1
Control = /var/run/wack.it

Mature = 5s
Balance {
    AcquisitionsPerRound = all
    Interval = 4s
}
Arp-Cache = 90s
Prefer none
VirtualInterfaces {
    { eth0:192.168.100.200/24 }
    { eth0:192.168.100.201/24 }
    { eth0:192.168.100.202/24 }
    { eth0:192.168.100.203/24 }
}

Notify {
    eth0:192.168.100.1/32
    arp-cache
}

4. А теперь все вместе

В заключение рассмотрим пример который объединяет все выше сказанное.
В нашем кластере будет участвовать 7 машин. Две из них будут:
1) распределять пул из четырех виртуальных IP (195.5.3.180 - 195.5.3.183) откликающихся на www.samplesite.com.
2) балансировать нагрузку между остальными пятью машинами, сами не выполняя запросов.
Остальные пять машин будут заниматься непосредственно предоставлением web ресурсов.

web cluster example scheme

На первых двух машинах установлен и wackamole и apache+mod_backhand, шлюз по умалчанию 192.168.100.1, постоянные IP(привязанные к интерфейсу) - 192.168.100.101, 192.168.100.102. Файлы настроек для них выглядят следующим образом:
wackamole.conf:
Spread = 4803
SpreadRetryInterval = 5s
Group = wack1
Control = /var/run/wack.it

Mature = 5s
Balance {
    AcquisitionsPerRound = all
    Interval = 4s
}
Arp-Cache = 90s
Prefer none
VirtualInterfaces {
    { eth0:195.5.3.180/24 }
    { eth0:195.5.3.181/24 }
    { eth0:195.5.3.182/24 }
    { eth0:195.5.3.183/24 }
}

Notify {
    eth0:192.168.100.1/32
    arp-cache
}

mod_backhand (httpd.conf):

<IfModule mod_backhand.c>
  UnixSocketDir /usr/local/apache/backhand
  MulticastStats 192.168.100.255:4445,1
  AcceptStats 192.168.100.0/24

  <Location "/backhand/">
    SetHandler backhand-handler
  </Location>
</IfModule>

<Files ~ "*">
    backhand bySession
    backhand byAge
    backhand removeSelf
    backhand byLoad
</Files>

Остальные пять машин умеют ip 192.168.100.50 - 192.168.100.55. На них установлен только apache+mod_backhand cо следующими настройками:

mod_backhand (httpd.conf):

<IfModule mod_backhand.c>
  UnixSocketDir /usr/local/apache/backhand
  MulticastStats 192.168.100.255:4445,1
  AcceptStats 192.168.100.0/24

  <Location "/backhand/">
    SetHandler backhand-handler
  </Location>
</IfModule>

Естественно все 5 машин должны предоставлять схожие web-ресурсы.

Все. Вам осталось, для проверки, только пощелкать кнопками выключения питания у некоторых компьютеров и посмотреть на результаты web-benchmark'а.