swarm

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

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

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

Голый Докер

Как бы выглядела подобная схема с обычным докером?

Нам бы нужно было:

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

manual-lb

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

  • сколько реплик приложения запускать?
  • как их распределить по хостам?
    • топорно - по штуке на хост?
    • а если на части хостов уже есть другие приложения?
  • как публиковать его порты?
    • если мы будем запускать больше одной реплики на хосте, то кому-то придется подвинуться с дефолтного порта
  • как контейнерам одного проекта общаться друг с другом? (мы же так привыкли к http://backend и mongo://db при локальной разработке с помощью docker-compose)
  • как балансировщику узнавать обо всем этом безобразии?
    • где брать адреса докер-хостов?
    • как быть с недефолтными портами?

Тупой ответ на все вопросы: РУКАМИ. И это грустно.

Service Discovery

Для автоматизации части, связанной с балансировщиком, придумали Service Discovery - подход в котором приложения при запуске регистрируютя (а при завершении - выписываются) в некоем реестре, адрес которого известен заранее, а балансировщик консультируется с этим реестром и заполняет свою конфигурацию согласно записям в нем.

service-discovery

Это несомненный шаг вперед по сравнению с ручным поддержанием конфигурации в актуальном состоянии, но опять же, имеет ряд недостатков. А именно, все участники процесса должны:

  • знать о местоположении реестра
  • уметь говорить на языке конкретного реестра (Consul, etcd, ZooKeeper)
  • заранее определиться с форматом записей в реестре, чтобы балансировщик мог понять, что туда записали приложения

Ну и в конечном счете, этот самый реестр должен существовать и функционировать. А еще он никак не решает вопросы собственно деплоя приложений.

Docker Swarm - To the Rescue!

Для решения всех этих проблем и был придуман Swarm - система оркестрации докер-контейнеров. Он призван перенести ништяки, к которым мы привыкли при локальной работе с докером, на уровень кластера из докер-хостов, а так же взять на себя задачи планировщика, балансировщика и Service Discovery. Что именно приносит Swarm-режим?

Overlay Network: любимые сети приложений - теперь между хостами

Возможность создать для проекта изолированную виртуальную сеть, в которой контейнеры могут общаться друг с другом просто по именам, например http://backend или mongo://db - наверное вторая по значимости киллер-фича докера, после изоляции зависимостей. Возможность компонентов проекта обращаться друг к другу по одинаковым адресам вне зависимости от того, в каком окружении они запущены - тоже своего рода изоляция от среды исполнения.

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

overlay

Без оверлейной сети для мультихостового проекта пришлось бы публиковать порты приложений, а внутри контейнеров указывать реальные физические адреса вроде http://backend.acme.com:13695. С оверлейной же сетью разницы между http://backend на ноутбуке разработчика и на боевом кластере просто не будет.

Концепция сервисов - местный Service Discovery

В Сворме на смену docker run приходит docker service create. Вторая команда по сути является надмножеством первой: все так же нужно/можно указывать желаемый образ, имя сервиса, доступные сети, опубликованные порты присоединенные вольюмы. Ключевая разница в том, что у сервиса есть параметр --replicas. Сворм может представить набор одинаковых контейнеров как единую сущность, к которой можно обращаться по имени сервиса.

В классическом режиме мы создавали контейнер бекенда через docker run --name backend mybackend:latest и обращались к нему по адресу http://backend. В Сворме все точно так же: docker service create --name backend --replicas 3 mybackend:latest. Только на этот раз http://backend - это не адрес отдельного контейнера, а всей группы из 3 контейнеров сразу. Запросы на это имя будут автоматически балансироваться между всеми репликами.

service

Добавим к этому тот факт, что благодаря оверлейной сети реплики одного сервиса могут быть раскиданы по разным физическим серверам, и получится, что мы скрыли от потребителей сервиса как количество реплик, так и их физическое положение. Адрес http://backend как был “местом, куда надо сходить за инфой от бекенда” на ноуте разработчика - так и им и остался в географически-распределенном докер-кластере на продакшене.

Кроме балансировки между репликами, концепция серисов имеет ряд опций для описания процесса обновления данного сервиса. Заданием всего нескольких опций и вызовом docker service update myservice ... мы можем вызвать процедуру поочередного обновления всех контейнеров сервиса на новую версию образа или для изменения любых опций, доступных при его создании. Орекстратор Сворма будет поочередно производить следующую процедуру:

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

Таким образом во время обновления сервиса он всегда будет доступен (если конечно сервис не состоит из одной реплики), максимум неприятностей - это получение смешанных ответов от новой и старой версий сервиса.

Планировщик

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

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

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

В качестве резерваций мы можем сказать: “вот этому контейнеру нужно 500 МБ памяти и 2 ядра”. Если для каждого сервиса мы будем указывать реалистичные резервации, то велик шанс получить равномерно загруженный кластер.

Мы можем ограничить запуск контейнеров некоторыми условиями, например только на хостах с определенной ролью или тегом: constraints: [node.role == manager] запустит контейнеры только на управляющих серверах.

Также мы можем указать стратегию распределения контейнеров по серверам. Например мы можем указать имя датацентра для каждого хоста и задать стратегию placement-pref 'spread=node.labels.datacenter, чтобы сервис был распределен по всем датацентрам, обеспечивая большую отказоустойчивость.

Кроме изначального распределения контейнеров по серверам планировщик следит за сохранением статуса кво. Контейнер, упавший по каким-то внутренним причинам, будет перезапущен, возможно на другом хосте. При падении целого физического сервера все контейнеры, запущенные на нем в момент падения, будут “расселены” (на самом деле пересозданы) по здоровым серверам.

Ingress Network: все дороги ведут в Сворм

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

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

Представим себе простую ситуацию: у нас есть кластер из 5 нод с какими-то IP-адресами и сервис acme.com из 3 реплик, запущенных планировщиком на случайных нодах. Адреса каких нод нам внести в DNS, чтобы наши клиенты смогли к нам попасть? Одной случайной? Всех размещающих сервис в данный момент? Всего кластера?

no-ingress

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

manual-dns

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

dns-update

Помимо необходимости внешних инструментов для обновления DNS, сами обновления не всегда смогут успевать за изменениями в кластере, DNS-кеш никто не отменял.

По этой причине разработчики сворма создали Ingress Network - специальную сеть, призванную быть проводником гостей из внешнего мира в оверлейные сети приложений. Как это работает:

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

ingress

Таким образом достаточно один раз внести все ноды кластера в DNS для любого сервиса, и все будет в шоколаде (почти). На самом деле в DNS должны быть указаны все здоровые ноды кластера, а их количество может меняться как вверх при масштабировании, так и вниз при падении нод. Но адаптация системы к этим изменениям - это уже совсем другая история.

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

Реверс-прокси - это веб-сервер, который не хостит никакой контент, а просто проксирует запросы на сервисы, которые за ним находятся. Польза от этого следующая:

  • единая точка входа в инфраструктуру
  • скрытие от конечных пользователей деталей реализации сервиса
  • управление сертификатами и шифрованием в едином месте

Роль и особенности реверс-прокси в контексте Сворма - тема отдельной статьи, но в целом ситуация такая, что сервисы не должны самостоятельно публиковать свои порты, а присоединяться к сети реверс-прокси, который является единоличным владельцем HTTP/HTTPS портов во всем кластере.

Но чего-то не хватает…

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

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

  • масштабировать сервисы на основе внутренних метрик (ответ на запрос за 1.5 секунды - это слишком долго, увеличим число реплик с 3 до 10)
  • масштабировать инфраструктуру на основе аппаратных метрик (у нас в среднем по больнице съедено 90% памяти, если не добавим в кластер пару серверов - все рухнет)
  • уметь делать обе эти вещи и в обратную сторону, чтобы впустую не прожигать ресурсы

Но это все хотелки на будущее, без них пока можно пережить, написать свой обвяз, ну, или продаться Амазону и использовать Docker4AWS. Также есть набор обидных багов и недоработок - неотъемлемая черта молодой технологии. Но это временно, а настоящей ахиллесовой пятой докер-кластера является хранение состояния приложений.

Если мы живем в мире, где контейнер приложения может в любой момент пересоздан планировщиком на другом хосте, то хостовые маунты /home/user/myproject:/app, так привычные разработчикам, работать в Сворме не будут. Проблема эта настолько нова, что сообщество еще не определилось с оптимальным решением и их обзор заслушивает отдельной статьи.