Настройка высокодоступного, масштабируемого отказоустойчивого сайта с помощью Nginx, GlusterFS и MySQL Galera в Linux

Современные сайты часто должны работать в режиме 24x7x365 без остановок на обслуживание. В этих условиях владельцы сайтов готовы инвестировать дополнительные средства в оборудование и программные решения для обеспечения высокой доступности.

Сайт без единой точки отказа становится де-факто требованием в мире, где большинство операций выполняются дистанционным образом. Простои обходятся дорого, особенно, если они происходят в неподходящее время — праздники, выходные, высокий сезон.

В этой статье мы рассмотрим как настроить высокодоступный сайт на трех узлах, которые могут быть как виртуальными так и аппаратными серверами. В ходе настройки мы будем использовать Nginx, СУБД MariaDB (MySQL), распределенную файловую систему GlusterFS, CDN CloudFlare, сертификаты Let’s Encrypt.

В рамках настройки мы рассмотрим следующие проблемы, которые обычно существуют в подобных системах:

  • настройка базы данных без единой точки отказа;
  • настройка Nginx для обслуживания приложения без единой точки отказа;
  • кластеризация статического контента между узлами с помощью GlusterFS;
  • репликация сессий между узлами с помощью SyncThing;
  • липкая балансировка трафика для избежания потери сессии;
  • балансировка трафика между узлами:
    • с помощью DNS;
    • с помощью CDN для быстрого переключения в случае отказа узла;

Для простоты будем считать, что приложение реализовано на PHP, однако, никаких ограничений по средству реализации приложения — будь то, Java (Jetty, Tomcat), Python (Django, Tornado, Turbogears), Ruby (RoR) или использовании другой экосистемы — нет, настройки в большинстве случаев успешно переносятся и в другие среды.

Топология

Мы будем настраивать трехузловую конфигурацию, как изображено на следующем рисунке:

Внутренняя сеть

Для успешной настройки все узлы должны быть соединены высоконадежной внутренней сетью. В том случае, если вы используете виртуальные машины, у которых есть только публичная сеть, то вы можете использовать VXLAN, туннели GRE или VPN для организации внутренней сети. В целом, мы предполагаем, что вы как-то свяжете свои серверы между собой, и эта связь будет надежной. От надежности и производительности внутренней сети будет зависеть то, насколько качественно будет работать отказоустойчивая конфигурация.

Если вы используете аппаратные серверы, рекомендуем связать их между собой через свич с поддержкой LACP, а лучше посредством MLAG, через два независимых коммутатора. Если же ваши коммутаторы не поддерживают MLAG, то вы можете использовать протокол OSPF для организации отказоустойчивой внутренней сети, используя Quagga, развернутый на каждом узле.

Если же у вас только один коммутатор, просто воткните три сервера в него и настройте общую частную сеть.

В облаке Cloud2 NetPoint каждая виртуальная машина получает две сети — публичную и приватную, при этом приватная сеть сразу настроена для правильной работы. Если вы клиент NetPoint, вам ничего не надо делать для настройки приватной сети.

Публичная сеть

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

Обратите внимание, что в моменты деградации, например, если один из узлов выйдет из строя, трафик перетечет на оставшиеся два узла. Каналы каждого из этих узлов должны выдержать добавление от 1/6 до 1/3 трафика без деградации.

На каждом узле мы установим одну и ту же операционную систему и все прикладное программное обеспечение — Nginx, GlusterFS, MariDB, PHP7 и настроим публичное взаимодействие с этими серверами по безопасному протоколу Let’s Encrypt.

DNS

Будем считать, что наши серверы имеют доменные имена:

  • server1.website.com, website.com
  • server2.website.com, website.com
  • server3.website.com, website.com

Необходимо убедиться, что серверы доступны по этим доменным именам из сети интернет (корректные записи A, AAAA). Доменные имена необходимы для получения сертификатов Let’s Encrypt, которые будут шифровать трафик.

Таким образом, каждый из серверов должен быть индивидуально доступен через serverX.website.com, а запись website.com должна указывать на каждый из них. При вызове nslookup website.com вы должны получить ip-адреса всех серверов.

Если вы не планируете использовать для внешней балансировки трафика CDN, необходимо установить корректный TTL для записей website.com A ip. Устанавливайте минимальный, какой позволяет провайдер. Это необходимо, чтобы при отказе сервера вы могли удалить запись для него и быстро исключить его из списка доступных.

Необходимо заметить, что не все провайдеры правильно обрабатывают малые TTL для DNS, поэтому мы бы не рекомендовали использовать этот способ, если есть возможность использовать внешнюю балансировку с помощью CDN.

Если вы планируете отправку почты с напрямую серверов или через внешний SMTP, то для настройки обратитесь к руководствам по настройке дополнительных записей, необходимых для правильной доставки почты — SPF, PTR.

Установка базового программного обеспечения LEMP

Для выполнения дальнейших шагов нам необходимы настроенные LEMP серверы с установленными сертификатами Let’s Encrypt. В зависимости от выбранной ОС, вы можете воспользоваться для настройки одним из руководств, приведенных ниже. При настройке каждого сервера используйте его доменное имя — server1.service-website.com, server2.service-website.com, server3.service-website.com.

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

Установка LEMP в CentOS 7 с использованием внешнего репозитория Remi’s Repository для установки PHP 7.2.

Стандартная установка LEMP в Debian 9 Stretch с использованием PHP 7.0

Стандартная установка LEMP в Ubuntu 18.04 Bionic с использованием PHP 7.2

На этом шаге вы должны получить три сервера с установленным стеком LEMP. Каждый сервер должен быть доступен по доменному имени server<X>.website.com по протоколу HTTPS. На каждом сервере должен быть установлен сервер СУБД MariaDB.

Кластерная файловая система GlusterFS для статических файлов

Для обеспечения мгновенной доступности статического контента и упрощения доступа к нему можно использовать различные подходы. Если ваш хостинг-провайдер предлагает высокодоступное хранилище, доступное по протоколу Swift или S3, разумно использовать его, храня данные вне серверов. Если ваш сайт поддерживает такое решение, рекомендуем использовать его. В этом случае вы можете просто пропустить данный раздел.

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

Синхронизация с помощью rsync, inosync. Методы широко применяются, но имеют основной недостаток, связанный с возможным рассогласованием или потерей файлов, которые на момент отказа еще не были синхронизированы. Кроме того, в зависимости от объемов хранимых данных и типов хранимых файлов, данный подход часто может быть неосуществим практически.

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

Высокая доступность с помощью распределенной файловой системы. Этот подход немного сложнее в настройке и управлении, однако гарантирует одинаковую доступность файлов и каталогов.

Мы рассмотрим способ синхронизации для сессий PHP и обеспечение высокой доступности для статических файлов с помощью высокодоступной распределенной системы GlusterFS с трехкратной репликацией данных.

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

Трехкратная репликация не отменяет резервное копирование. Вы должны выполнять резервное копирование вне зависимости от надежности хранения текущих данных. Читайте статью о настройке резервного копирования файлов с помощью Duplicity.

При настройке GlusterFS ожидается, что все серверы соединены по внутренней сети 10.0.0.0/24 и имеют адреса 10.0.0.1, 10.0.02, 10.0.03. Если у вас другие адреса, измените настройки, приведенные в инструкции соответствующим образом.

Для дальнейшей настройки GlusterFS перейдите в наше руководство по настройке для трехузлового кластера. В руководстве приведены инструкции для CentOS 7, Debian 9, Ubuntu 16.04 или 18.04.

После успешного завершения настройки GlusterFS создайте каталог /var/www/webiste.com/assets и добавьте в /etc/fstab запись вида:

/mnt/gluster /var/www/website.com/assets none bind,_netdev,default 0 0

В общем случае, мы не рекомендуем размещать сам код сайта в распределенной файловой системе. Специфические системные вызовы, которые используют движки, могут снизить производительность для таких сред, которые на лету автоматически определяют изменения и применяют их — например, PHP-FPM. С другой стороны, если после чтения кода обращение к файловой системе не происходит, можно легко использовать хранение кода в GlusterFS.

Всеми силами избегайте использования файловых блокировок на распределенных файловых системах — это существенно снизит производительность работы с файлами. Если вам нужны распределенные блокировки, рассмотрите вариант использования Apache Zookeeper вместо файловых блокировок.

Репликация сессий

Если вы используете MySQL для хранения сессий, то этот раздел можно пропустить, поскольку мы будем использовать кластерный вариант MySQL, который обеспечит доступность сессий на всех узлах. Аналогично обстоят дела, если вы храните сессии в Redis.

Настройка высокодоступного кластера Redis на 3х узлах рассматривается в отдельной статье из-за объемности.

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

Использование удаленной синхронизации

Далее рассмотрим как реализовать асинхронную репликацию сессий с помощью инструментов на основе удаленной синхронизации, которая подходит для многих, но не всех сайтов.

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

Почему асинхронная репликация работает? Кажется, что клиент может попасть на обслуживание на любой сервер, где отсутствует информация о сессии или является устаревшей. Решение заключается в липкой балансировке, когда Nginx продолжает обслуживание клиента на одном сервере до тех пор, пока этот сервер доступен, а распределение осуществляется с помощью хэш-функции, которая строится на основании IP-адреса клиента. Если же вы не планируете использовать липкую балансировку, асинхронная репликация с помощью Rsync не является приемлемым решением.

Удаленная репликация с помощью SyncThing

Преимущества SyncThing состоят в том, что это программное обеспечение использует пиринговый протокол, что особенно удобно в случае группы, состоящей из более чем 2х узлов. Для настройки SyncThing воспользуйтесь нашим руководством, которое показывает как настроить трехузловой кластер для синхронизации сессий между узлами.

В качестве альтернативы SyncThing вы можете рассмотреть и Resilio, однако, это коммерческий инструмент, для поддержки некоторых функций требуется приобрести PRO-лицензию.


Теперь, когда мы разобрались с файлами и каталогами, пришло время решить вопрос с высокодоступным сервером СУБД.

Настройка мастер-мастер кластера Galera

В приложениях без единой точки отказа доступность СУБД — особо важный аспект. В рамках MySQL уже несколько лет существует продвинутая система для организации мульти-master репликации, которая реализуется с помощью Galera. В отличие от способов обеспечения отказоустойчивости MySQL, которые применялись ранее и являлись асинхронными или почти синхронными, кластер Galera позволяет реализовать самую настоящую синхронную репликацию MySQL, которая может смело применяться в продуктовой среде.

Настройка кластера MySQL Galera для операционных систем Ubuntu 16.04, Ubuntu 18.04, Debian 9, CentOS 7 приведена в отдельной статье нашего блога. После выполнения инструкций, приведенных в ней, вы получите полноценный отказоустойчивый кластер MySQL без единой точки отказа.

Настройка подключения клиентов к кластеру Galera

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

Речь идет о том, что в Galera не поддерживается репликация явных блокировок, включая LOCK TABLES, FLUSH TABLES {явный список таблиц} WITH READ LOCK, (GET_LOCK (), RELEASE_LOCK (),…). В том случае, если вы используете данные операции в своем приложении, вы не сможете реализовать масштабирование, довольствуясь только отказоустойчивостью. Ваши приложения должны работать с одним сервером, чтобы эти операции обрабатывались корректно.

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

Keepalived для реализации VRRP. В этом подходе для группы серверов создается виртуальный IP-адрес, который переносится между серверами при отказе. Для определения отказа прикладной части MySQL настраивается скрипт, проверяющий его состояние.

Внимание. Не используйте HAProxy, Nginx, если вам необходимо всегда обслуживать все операции MySQL на одном и том же сервере. Несмотря на то, что в некоторых случаях все будет работать, ряд ситуаций приведет к тому, что соединения будут обслуживаться разными серверами.

Пример проблемы с возникновением обслуживания на разных узлах

Рассмотрим пример балансировки соединений с помощью Nginx, который приводит к возникновению ошибки.

cat /etc/nginx/modules-available/mysql.conf 

stream {
  upstream db {
    server 10.0.0.1:3306;
    server 10.0.0.2:3306 backup;
    server 10.0.0.3:3306 backup;
  }

  server {
    listen 127.0.0.1:33306;
    proxy_pass db;
  }
}

Данный фрагмент конфигурации Nginx делает следующее:

Nginx будет слушать соединения по протоколу TCP на порту 33306 текущего сервера и перенаправлять их на сервер 10.0.0.1:3306, если он не доступен, то на сервер 10.0.0.2:3306, в случае его отказа на 10.0.0.3:3306. Эта конфигурация Nginx активируется на всех серверах, где будут выполняться приложения.

Приложения будут соединяться с MySQL по адресу 127.0.0.1:33306, который будет проксировать запросу на нужный сервер MySQL.

Сначала все серверы MySQL работают, появляются клиенты, проксируемые соединения устанавливаются с сервером 10.0.0.1:

Сервер MySQL 1 выходит из строя, клиенты получают сообщение о потере соединения и инициализируют повторное соединение, в результате чего они переключаются на сервер 10.0.0.2:

Пока что все отлично, новые клиенты, которые будут появляться будут отправляться на сервер 10.0.0.2:

Теперь сервер 10.0.0.1 опять появляется в сети, и появляется клиент 5:

Поскольку Nginx обнаружил, что сервер 10.0.0.1 снова доступен, он подключил клиента нему. В результате возникла ситуация обслуживания на различных серверах.

Этот пример показывает, что при наличии требования обслуживания на одном сервере СУБД, которое продиктовано ограничениями кластера Galera, возникающими при использовании ряда операций блокировок, использование проксирования соединений не является возможным способом решения проблемы. Оно позволяет бороться с ситуацией выхода серверов из строя, но автоматический ввод серверов обратно приведет к некорректной обработке запросов.

При необходимости обработки всех запросов на одном сервере СУБД, вы должны использовать Keepalived для реализации виртуального IP-адреса. Это отлично работает, поскольку при переносе адреса на другой сервер все соединения автоматически разрываются и должны быть установлены заново, соответственно, все клиенты автоматически будут переключены как в случае аварии, так и после решения проблемы.

В том случае, если нет требования обработки на одном сервере, проксирование соединений с помощью Nginx будет прекрасно работать. Рассмотрим эту настройку подробнее.

Проксирование соединений к MySQL помощью Nginx

Как мы уже выяснили ранее, подобная схема может успешно использоваться, если нет требования обработки на одном сервере.

Внимание. Такая настройка должна использоваться только в том случае, если вы не можете передать IP-адреса всех серверов MySQL напрямую в движок сайта. Если движок уже поддерживает соединение с сервером из списка, лучше использовать встроенную возможность.

Настройка Nginx. Добавьте в файл /etc/nginx/nginx.conf следующий фрагмент:

stream {
  upstream db {
    server 10.0.0.1:3306;
    server 10.0.0.2:3306;
    server 10.0.0.3:3306;
  }

  server {
    listen 127.0.0.1:33306;
    proxy_pass db;
  }
}

Проверьте, что конфигурация Nginx корректна и перезапустите его:

sudo nginx -t
sudo systemctl restart nginx

Настройка пользователя MySQL. Вам необходимо создать пользователя MySQL, который может соединяться с базой данных с любого из ваших серверов через проксированное соединение:

mysql -uroot -s
MariaDB [(none)]> CREATE USER 'user'@'10.0.0.%' IDENTIFIED BY 'secret';
MariaDB [(none)]> FLUSH PRIVILEGES;
MariaDB [(none)]> exit

Проверьте, что созданный пользователь может соединяться с СУБД через прокси:

mysql -u user -psecret -h 127.0.0.1 -P 33306 -e "show variables where Variable_name like 'wsrep_node_address';"
+--------------------+---------------+
| Variable_name      | Value         |
+--------------------+---------------+
| wsrep_node_address | 10.0.0.1      |
+--------------------+---------------+

На этом настройка завершена. Теперь движок вашего сайта может соединяться с любым из доступных серверов MySQL через единый адрес 127.0.0.1:33306.

Настройка балансировки нагрузки между приложениями

В этом разделе мы настроим механизм отказоустойчивой обработки с помощью распределения обработки между локальным движком сайта и удаленными, которые размещаются на двух других серверах. Для простоты будем считать, что используется PHP-FPM, хотя принципиальной разницы нет. Топология обработки запросов приведена на следующем изображении:

Запрос от пользователя, который поступил на сервер 1, обрабатывается Nginx следующим способом:

  • если возможно исполнение на локальном PHP-FPM, запрос исполняется на нем;
  • если локальный PHP-FPM недоступен, запрос передается на один из доступных серверов напрямую в PHP-FPM (мимо Nginx, который на них тоже выполняется).

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

Предполагается, что вы уже выполнили настройку серверов LEMP по руководству, приведенному ранее. К этому момент у вас должны быть развернуты три сервера с Nginx и PHP-FPM.

Настройка PHP-FPM

CentOS. В файле /etc/opt/remi/php72/php-fpm.d/www.conf измените listen = /var/run/php72-fpm.sock на listen = 10.0.0.X:20000, где X1, 2, 3 в зависимости от сервера. После внесения изменений перезапустите PHP-FPM командой sudo systemctl restart php72-php-fpm.

Если вы установили из репозитория Remi другую версию PHP, сделайте замену аналогичным способом с учетом именования.

Debian 9. В файле /etc/php/7.0/fpm/pool.d/www.conf измените listen = /run/php/php7.0-fpm.sock на listen = 10.0.0.X:20000, где X1, 2, 3 в зависимости от сервера. После внесения изменений перезапустите PHP-FPM командой sudo service php7.0-fpm restart.

Ubuntu 18. В файле /etc/php/7.2/fpm/pool.d/www.conf измените listen = /run/php/php7.2-fpm.sock на listen = 10.0.0.X:20000, где X1, 2, 3 в зависимости от сервера. После внесения изменений перезапустите PHP-FPM командой sudo service php7.2-fpm restart.

Настройка Nginx

Замените в конфигурации Nginx фрагмент:

upstream php {
        server unix:...;
}

на:

upstream php {
        server 10.0.0.X:20000;
        server 10.0.0.Y:20000 backup;
        server 10.0.0.Z:20000 backup;
}

где X — октет, соответствующий локальному адресу, например, для сервера 10.0.0.11, а Y, Z — октеты адресов двух оставшихся серверов в случайном порядке, например, для сервера 10.0.0.1:

upstream php {
        server 10.0.0.1:20000;
        server 10.0.0.2:20000 backup;
        server 10.0.0.3:20000 backup;
}

Проверьте настройки Nginx и перезапустите его:

sudo nginx -t && systemctl restart nginx

Централизованное хранение журналов приложений

Вы можете захотеть хранить журналы приложений в общем каталоге GlusterFS, чтобы иметь возможность их совместной обработки. В этом случае вам необходимо изменить параметры Nginx, добавив в них имя хоста или его адрес:

access_log /var/log/nginx/access-10.0.0.1.log;
error_log /var/log/nginx/error-10.0.0.1.log;

Не забудьте организовать ротацию журналов — в общем каталоге это сделать несколько сложнее. Другой способ организации централизованного хранения записей журналов — отправлять их в общее хранилище, например, в Elasticsearch с помощью Filebeat и анализировать их в Kibana.

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

Внешняя балансировка с помощью DNS

К настоящему моменту у вас должно быть настроено 3 сервера LEMP, при этом ваш сайт должен быть доступен по трем доменным именам — server1.website.com, server2.website.com, server3.website.com по протоколу HTTPS: https://serverX.website.com/. Как мы говорил ранее, у вас так же есть настроенный домен website.com, для которого созданы три записи типа A/AAAA:

  • website.com A server1-ip;
  • website.com A server2-ip;
  • website.com A server3-ip.

К сожалению, вы пока что не можете открыть сайт по безопасному протоколу HTTPS по адресу https://website.com/, поскольку сертификат для данного домена еще не установлен.

Сертификат SSL для website.com

Если вы используете купленный сертификат, то все, что вам необходимо сделать — копировать виртуальный хост Nginx, созданный для serverX.website.com, заменить в нем serverX.website.com на website.com, а сертификат Let’s Encrypt на приобретенный вами.

Если вы планируете для сайта https://website.com использовать сертификат Let’s Encrypt, то, опять же, скопируйте виртуальный хост Nginx, получите для него SSL-сертификат точно так же, как было описано в инструкции по настройке LEMP-сервера.

Отказоустойчивая балансировка трафика между серверами с помощью CloudFlare

На этом опциональном шаге вы можете дополнить систему с помощью отказоустойчивой балансировки трафика пользователей между доступными серверами с помощью CDN. Вышедшие из строя серверы автоматически будут исключаться из балансировки. Итоговая схема высокой доступности станет выглядеть как изображено на картинке:


Если вы не имеете представления о CDN и CloudFlare, мы рекомендуем ознакомиться с нашей статьей на эту тему.

Выполнение этого шага не только обеспечивает автоматическое отключение вышедших из строя серверов, но и позволяет обеспечить защиту кластера от DDoS.

Резервное копирование данных

Важно настроить резервное копирование сразу же, как только вы настроили серверы, не откладывая это действие на потом. Воспользуйтесь нашим пошаговым руководством по настройке инкрементного резервного копирования файлов и баз данных для MySQL и PostgreSQL с помощью Duplicity.

Поскольку серверы используют GlusterFS и кластер Galera, вам достаточно выполнять резервное копирование для этих данных только на одном из серверов. Что же касается системных каталогов, для них рекомендуем настроить резервное копирование на каждом из серверов.