Всем привет, мы используем Docker Swarm в продакшене, и столкнулись с проблемой балансировки контейнеров и нагрузки по нодам в кластере. Я хотел бы рассказать с какими сложностями мы встретились, и поделиться нашим решением.
1) Описание проблемы
Чтобы понять проблему, рассмотрим ее на примере проекта нашей компании. Исторически сложилось, что мы использовали монолитную архитектуру с оркестрацией на docker swarm. Помимо монолита у нас имеется ряд вспомогательных сервисов и консьюмеров. Источником основной нагрузки на сервера выступает php-fpm, который выполняет код монолита. В продакшене мы имели следующую схему.
На схеме показаны два сервера. Первый сервер DB1 — это MySQL база данных, которая не управляется Docker Swarm, поскольку установлена непосредственно на основную систему для большей производительности при работе с диском. Второй — Web 1 сервер, это непосредственно наш монолит с его консьюмерами и сервисами, которые запущены внутри. По данной схеме видно, что не все возможности оркестрации используются , поскольку у нас единственный сервер. Отказоустойчивость также очень мала — в случае падения сервера весь наш продукт становится не работоспособным.
На начальном этапе это решение закрывало задачи которые стояли пред нами. Swarm снял с нас надобность следить и обновлять вручную контейнеры — меньше ручных операций и больше автоматизации.
Данная схема достаточно хорошо работала, но с ростом количества пользователей нагрузка на сервер Web 1 значительно росла и становилось понятно, что его мощностей уже не достаточно. Мы понимали, что покупать более мощный сервер менее перспективно в плане отказоустойчивости и дороже по цене, чем масштабироваться горизонтально, увеличивая количество серверов. К тому же у нас в продакшене уже был готовый инструмент на сервере Web1, который успешно выполнял свою задачу. Поэтому мы добавили под управление Docker Swarm еще один сервер. Получилась следующая схема.
Мы получили кластер из двух серверов, в котором Web 1 является master нодой, а web2 — обычный worker. В этой схеме мы были уверены в master ноде, поскольку это все тот же сервер, который у нас был . Мы понимали, что он надежный и имеет высокую доступность. А вот сервер Web 2 был темной лошадкой, поскольку его выбрали cloud сервером, исходя из ценовой политики, который ранее не испытывали в продакшене. При этом сервера не находятся в одном помещении, поэтому могут быть проблемы с сетевым взаимодействием.
Отсюда мы получили следующие важные для нас критерии: кластер должен автоматически перестраиваться в случае отказа воркера (Web 2) и забирать всю нагрузку и сервисы на себя, но после появления воркера (Web 2) автоматически раскидывать всю нагрузку обратно равномерно по серверам. По сути, это стандартная задача, которую должен решать Docker Swarm.
Мы провели эксперимент, отключили сервер Web 2 сами и посмотрели, что будет делать Swarm. Он сделал, что и ожидалось — поднял все сервисы на master ноде (Web 1). Проверив то, что наш кластер верно себя ведет при отказе второго сервера, мы обратно включили Web 2.
На этом этапе мы обнаружили первую проблему — нагрузка осталась по прежнему на сервере Web 1 и Docker Swarm лишь запустил сервисы, которые запускались глобально для всего кластера. Столкнувшись с первым ограничением, мы поняли, что сервера не так часто становятся недоступными. Поэтому в случае отказа Web 2 сервера, мы сами проведем балансировку, воспользовавшись командой:
docker service update --force
Она позволяет распределить контейнеры указанного сервиса равномерно по серверам, что мы и хотели получить.
Спустя некоторое время, выполняя deploy кода на боевой кластер, мы начали замечать, что иногда после обновления контейнеров нагрузка снова делилась неравномерно по серверам. Причиной этого факта было то, что основной сервис в нашем кластере php-fpm, который является источником нагрузки, запускал больше php-fpm реплик (контейнеров) на одном из серверов, чем на другом. Эта проблема была достаточно критичной, поскольку мы хотели равномерной утилизации серверов и не перегружать один из них, а также проводить deploy без вхождений на сервер и ручной балансировки этих реплик.
Первое очевидное решение, которое пришло на ум — выставить deploy сервиса php-fpm глобально, чтобы Swarm сам их запускал на каждой доступной ноде. Но данное решение было не очень подходящим в перспективе, поскольку не факт, что кластер будет содержать ноды только для обработки запросов пользователей — хотелось оставить гибкость в настройке кластера и иметь возможность не запускать php-fpm реплику на какой-то группе серверов.
Обратившись к документации Docker, мы нашли следующий вариант: для разрешения проблемы распределения контейнеров по серверам, Docker Swarm имеет механизм placement, который позволяет указать конкретному сервису на каких серверах с каким label запускать контейнеры. Он дает возможность запустить контейнеры на ряде серверов в кластере, но все так же остается проблема с балансировкой. Для ее решения в Docker документации предлагается установить лимиты на ресурсы и зарезервировать в Docker Swarm необходимые нам мощности. Такой подход в связке с placement казался самым подходящим, чтобы закрыть нашу задачу.
Мы выполнили настройку кластера, выставили резервацию ресурсов под основной сервис php-fpm и выполнили проверку как поведет себя Docker Swarm при отключении ноды Web 2. Оказалось, что решив проблему с распределением сервиса php-fpm по серверам, мы указали резервацию ресурсов, которая не позволяла запускать php-fpm контейнеров больше, чем сейчас есть на данном сервере. Соответственно с отключением сервера Web 2 все остальные контейнеры запускались на сервере Web1, но сервис php-fpm оставался в подвешенном состоянии, поскольку из-за ограничения резервации ресурсов процессора он не имел подходящих нод для запуска всех реплик. С включением сервера Web 2 происходил запуск всех реплик php-fpm, которые не могли найти подходящий сервер, все остальные сервисы продолжали работу на сервере Web 1. В разрезе того, что основную нагрузку дает php-fpm, мы получили равномерное распределение загрузки серверов, при этом решили проблему с балансировкой нагрузки после отказа одной ноды и возвращения ее в строй. Но спустя некоторое время обнаружилась новая проблема.
Однажды нам понадобилось отключить Web 2 сервер для технических работ. В этот момент разработчики заливали код через ci на наш кластер и обнаружилось, что пока сервер Web 2 выключен, обновление кода не происходит. Это было очень плохо, поскольку сами разработчики не должны заботиться о состоянии кластера и иметь возможность в любой момент залить код на продакшен окружение. Источником проблемы как раз была резервация ресурсов под контейнер в Docker Swarm. Из-за недостатка свободных ресурсов, Swarm выдавал информацию об отсутствии подходящих нод для запуска и наше обновление кода благополучно зависало до появления второй ноды (Web 2) в кластере.
2) Наше решение проблемы
Выполнив поиск возможных решений этой проблемы, мы поняли что уперлись в тупик. Мы хотели, чтобы во всех случаях, пока работает хотя бы один сервер, наш продукт продолжал свою работу, а по возвращению сервера в кластер нагрузка делилась по ним равномерно. При этом, в любом состоянии кластера, будь то один сервер или десять, мы могли обновлять код. На этом этапе мы решили попробовать автоматизировать наши действия, которые мы выполняли руками для распределения нагрузки, когда еще не было резервации ресурсов, а именно запускать команду docker service update --force в нужный момент, чтобы все происходило автоматически.
Именно эта идея и стала основой для нашего мини-проекта Swarm Manager. Swarm Manager — это обычный bash-скрипт, который основывается на докер команды и ssh, осуществляет ту самую балансировку в нужный момент. Для его работы как демона мы запускаем его в cron контейнере. Визуально это выглядит следующим образом.
В целом видно, что в контейнер мы передаем cron конфиг с вызовом нашего скрипта swarm_provisioner.sh, который уже выполняет действия по балансировке. Чтобы swarm_provisioner.sh смог корректно работать на любой из нод кластера, необходимо разрешить ssh подключение к root пользователю с любого сервера кластера к любому серверу в кластере. Это даст возможность скрипту зайти на удаленный сервер и проверить запущенные на нем контейнеры. Для тех, кому не подходит пользователь root, можно поменять пользователя в swarm_provisioner.sh, заменив root в переменной SSH_COMMAND на подходящего пользователя с доступом к команде docker ps. Рассмотрим пример cron file:
SHELL=/bin/bash
*/1 * * * * /swarm_provisioner.sh "web-group" "edphp-fpm" "-p 22"
Как видим, это обычный cron файл с вызовом каждую минуту скрипта swarm_provisioner.sh с заданными параметрами.
Рассмотрим параметры, которые передаются в скрипт.
Первый параметр — имя label. Устанавливаем его с произвольным удобным значением на все сервера, которые будут содержать реплики сервиса, нуждающегося в балансировке. На текущий момент существует ограничение по количеству таких серверов — их должно быть меньше либо столько же, сколько и запускаемых реплик сервиса.
Второй параметр — имя сервиса, балансируемого по нодам, с приставкой названия кластера. В примере кластер называется ed, а сервис - php-fpm.
Третий параметр — это порт ssh, по которому скрипт будет стучаться на сервера в кластере с указанным label и проверять количество запущенных контейнеров сервиса. Если скрипт увидит перекос по запущенным контейнерам на серверах, он выполнит команду docker service update --force.
В итоге данный сервис запускается на любой мастер ноде, как показано ниже, и выполняет распределение нужного нам docker swarm сервиса равномерно по серверам. В случае, если контейнеры распределены равномерно, он просто выполняет проверку без запуска каких-либо других действий.
swarm-manager:
image: swarm-manager:latest
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- /swarm-keys:/root/.ssh
deploy:
replicas: 1
update_config:
parallelism: 1
delay: 1s
order: start-first
restart_policy:
condition: on-failure
placement:
constraints:
- node.role==manager
3) Выводы
Мы получили инструмент, который решил наши проблемы. На данном этапе это только первая версия. Скорее всего, в будущем мы выполним замену ssh на docker api, которое позволит более просто запускать этот сервис из коробки, и поработаем над ограничениями, которые сейчас существуют.
vanyas
Печально…
Maxistr Автор
Возможно, но он выполняет свои задачи. И достаточно прост в настройке.