Преамбула
Некоторе время назад перед нами стала задача спроектировать и развернуть систему для потокового видео. Суть была в массовом запуске/остановке инстанций, на которых происходит обратная сборка потокового видео и стриминг на множество media cdn провайдеров (youtube, livestream, ustream итд ) а также на собственные rtmp и ts точки назначения. Каждая инстанция требовала настройки перед запуском т.к. содержала специфическую для каждого клиента информацию. Также было понятно, что система должна работать в большом количестве регионов (как минимум во всех точках, где присутствует Амазон, а как максимум — в любом месте где можно
Было это в начале 2015 года уже были разговоры о том что Docker в скором времени выпускает родную систему кластеризации. И 26 февраля 2015 года она таки вышла. Сразу стало понятно что это для нас серебряная пуля и Swarm идеально ложится на наш проект. Проект был запущен в мае 2015.
Через 11 месяцев мы запустили второй, более сложный проект в котором требовалось организовать статические точки входа (ip:port) для оборудования третьих производителей, расширив при этом всю логику работы и запуска инстанций динамически в нужном регионе.
В процессе проектирования и последующей разработки мы использовали Docker swarm довольно нестандартно. И далее появилась идея как динамически управлять и балансировать своей инфраструктурой. Также как часть этого пути появился проект Gobetween который должен был помочь нам в будущем гибко управлять нашей инфраструктурой, а также быть легко реплицируемым.
Задача
В данной статье я хотел бы поделится идеей и принципиальной реализацией построения распределенной системы.
Я специально опущу аспект безопасности — тут уж каждый решает как / что лучше строить. Некоторые закрываются паролями/сертификатами и т.д., некоторые выносят менеджмент часть в сети не пересекающиеся напрямую с сетями общего доступа (живут внутри vpc и связаны между собой по-средством тоннелей).
Принципиальная схема докер кластера на Standalone Swarm.
Рассмотрим систему принципиально — как большое колличество регионов и с одним ядром(урпавляющим).
В данном случае рассматривается система с одним мастером и большим количеством докер хостов, которые анонсируют себя в консул кластер. Почему так? да потому что по сути Manage не представляет никакой ценности сам по себе — в нем никаких данных, которые не хотелось бы потерять. В нем даже настроек толком нет — он является планировщиком задач для списка докер нод, который он берет из Consul кластера. Более того — таких Мanagе инстанций при желании можно сделать сколько угодно, но смысла нет — ниже объясню почему.
Самый большой плюс этакой системы — все прозрачно, управляемо, масштабируемо. Итак, начнем с конфигурирования консул кластера.
Установка и настройка Consul Кластера
На выходе мы хотим получить такую структуру кластера:
3 ноды в RAFT с авто выбором мастера.
sever1.consul.example.com 10.0.0.11 bootstrap consul server, consul agent
sever2.consul.example.com 10.0.0.12 consul server, consul agent
sever2.consul.example.com 10.0.0.13 consul server, consul agent
Сначала устрановим Consul на server(N).consul.example.com:
Качаем консул с https://consul.io
Никаких контейнеров, консул под UPSTART.
$wget https://releases.hashicorp.com/consul/0.6.4/consul_0.6.4_linux_amd64.zip
$unzip *.zip
$mv consul /usr/sbin/consul
Проверяем что Consul работает:
$consul --version
Consul v0.6.4
Consul Protocol: 3 (Understands back to: 1)
После установки на всех трех серверах — подготовим кластер к первичной загрузке:
Генерируем кластерный токен :
$consul keygen
ozgffIYeX6owI0215KWR5Q==
Создаем пользователя из под которого будет запускаться Consul:
$adduser consul
Создаем на всех 3 серверах необходимые директории:
$mkdir -p /etc/consul.d/{bootstrap,server,client}
Создаем директории для хранения данных Консула:
$mkdir /var/consul
$chown consul:consul /var/consul
На ноде которую мы будем использовать для первичного запуска кластера (sever1.consul.example.com) нужно создать bootstrap конфиг:
$vim /etc/consul.d/bootstrap/config.json
{
"bootstrap": true,
"server": true,
"datacenter": "production",
"data_dir": "/var/consul",
"encrypt": "",
"log_level": "INFO",
"encrypt":ozgffIYeX6owI0215KWR5Q==,
"enable_syslog": true
}
На ВСЕХ трех серверах нужно создать серверный конфиг /etc/consul.d/server/config.json:
Server1:
{
"bootstrap": false,
"server": true,
"datacenter": "production",
"data_dir": "/var/consul",
"encrypt": "ozgffIYeX6owI0215KWR5Q==",
"log_level": "INFO",
"enable_syslog": true,
"start_join": ["10.0.0.12", "10.0.0.13"]
}
Server2
{
"bootstrap": false,
"server": true,
"datacenter": "production",
"data_dir": "/var/consul",
"encrypt": "ozgffIYeX6owI0215KWR5Q==",
"log_level": "INFO",
"enable_syslog": true,
"start_join": ["10.0.0.11", "10.0.0.13"]
}
Server3:
{
"bootstrap": false,
"server": true,
"datacenter": "production",
"data_dir": "/var/consul",
"encrypt": "ozgffIYeX6owI0215KWR5Q==",
"log_level": "INFO",
"enable_syslog": true,
"start_join": ["10.0.0.11", "10.0.0.12"]
}
Теперь создадим UPSTART скрипт для Consul на всех серверах: /etc/init/consul.conf:
description "Consul server process"
start on (local-filesystems and net-device-up IFACE=eth0)
stop on runlevel [!12345]
respawn
setuid consul
setgid consul
exec consul agent -config-dir /etc/consul.d/server
Первичный запуск кластера
Проинициализируем наш кластер (Первичный запуск): на sever1.consul.example.com:
#consul agent -config-dir /etc/consul.d/bootstrap
Сервис должен запустится и захватить терминал. В bootstrap режиме сервер сам назначает себя мастером и инициализирует все нужные структуры данных.
На остальных двух серверах (sever2.consul.example.com, sever3.consul.example.com) просто запускается консул в режиме сервера.
#start consul
Эти сервера подсоединяются к bootstrap серверу. В данный момент мы имеем кластер из трех серверов, два из которых — работают в обычном режиме сервера, и один — в режиме инициализации, что значит что он сам принимает решения по распространению данных не спрашивая остальных.
Теперь мы можем остановить bootstrap сервер и перезапустить консул в режиме стандартного сервера:
На bootstrap сервере нажмите:
CTRL-C
Консул остановится и затем перезапустите в режиме стандартного сервера :
sever1.consul.example.com:
#consul start
Проверьте что все прошло хорошо с помощью такой команды:
#consul members -rpc-addr=10.0.0.11:8400
Вывод должен быть :
Node Address Status Type Build Protocol DC
server1.consul.example.com 10.0.0.11:8301 alive server 0.6.4 2 production
server2.consul.example.com 10.0.0.12:8301 alive server 0.6.4 2 production
server3.consul.example.com 10.0.0.13:8301 alive server 0.6.4 2 production
Итак, у нас есть готовый и работоспособный консул кластер.
Балансировщик нагрузки, как фронтенд консул кластера
В данном случае мы будем использовать балансировщик нагрузки и все запросы к консул кластеру будут идти через него. В случае Амазона использование консул агентов из разных VPC, не говоря уже о регионах имеет много неразрешенных проблем (анонс своего внутреннего IP вместо указанного при старте внешнего, что рушит второй этап входа ноды/сервера в кластер ), а поднимать консул кластер в каждом регионе и настраивать синхронизацию — с мой точки зрения на данном этапе не рационально.
Устанавливаем Consul Agent
По аналогии — скачаем и установим на сервер, где будет находится наш балансировщик, Consul.
потом создадим конфиг агента :
$vim /etc/consul.d/server/config.json
{
"server": false,
"datacenter": "production",
"data_dir": "/var/consul",
"ui_dir": "/home/consul/dist",
"encrypt": "ozgffIYeX6owI0215KWR5Q==",
"log_level": "INFO",
"enable_syslog": true,
"start_join": ["10.0.0.11", "10.0.0.12", "10.0.0.13"]
}
создадим upstart для агента:
description "Consul server process"
start on (local-filesystems and net-device-up IFACE=eth0)
stop on runlevel [!12345]
respawn
setuid consul
setgid consul
exec consul agent -config-dir /etc/consul.d/agent
стартуем агент :
$start consul
проверим что у нас вышло:
$consul members -rpc-addr=10.0.0.11:8400
после запуска вывод должен быть что-то вроде:
Node Address Status Type Build Protocol DC
server1.consul.example.com 10.0.0.11:8301 alive server 0.6.4 2 production
server2.consul.example.com 10.0.0.12:8301 alive server 0.6.4 2 production
server3.consul.example.com 10.0.0.13:8301 alive server 0.6.4 2 production
lb.consul.example.com 10.0.0.1:8301 alive client 0.6.4 2 production
Настраиваем Gobetween в виде балансировщика.
в предыдущих статьях я описывал часть функционала нашего ЛБ.
в данном случае проще всего использовать EXEC discovery вместе с Consul agent установленном локально — это позволяет смотреть на консул кластер изнутри (ведь в будущем вполне может получится что мы добавим/уберем часть нод и тогда лб не придется перенастраивать).
качаем последний релиз(версия может потом сменится, так что следите за релизами ):
$wget https://github.com/yyyar/gobetween/releases/download/0.3.0/gobetween_0.3.0_linux_amd64.tar.gz
$tar -xvf gobetween_0.3.0_linux_amd64.tar.gz
$cp gobetween_0.3.0_linux_amd64/gobetween /usr/sbin
Итак, давайте создадим скрипт определения списка доступных consul бекенд серверов, который будет возвращать список серверов прошедших проверку и у которые зарегистрированы к консуле и отмечены сервисом CONSUL
создадим директорию для конфигов и дискавери скриптов
$mkdir /etc/gobetween/
создадим дискавери скрипт :
$vim /etc/gobetween/consul_node_discovery.sh
такого содержания:
#!/bin/bash
curl -Ss http://0.0.0.0:8500/v1/catalog/service/consul |jq '.[] | .Address' |sed 's/"//g'| sed 's/$/:8500/'
если запустить от руки вывод скрипта должен быть что-то типа :
10.0.0.11:8500
10.0.0.12:8500
10.0.0.13:8500
при данном типе получения списка рабочих серверов хелсчеки мы использовать не будем — оставим это самому консул кластеру. Теперь настроим сам Gobetween:
$vim /etc/gobetween/gobetween.toml
[logging]
level = "info"
output = "stdout"
[api]
enabled = true
bind = ":8888"
[api.basic_auth]
login = "admin"
password = "admin"
[defaults]
max_connections = 0
client_idle_timeout = "0"
backend_idle_timeout = "0"
backend_connection_timeout = "0"
[servers.consul]
bind = "10.0.0.1:8500"
protocol = "tcp"
balance = "iphash"
max_connections = 0
client_idle_timeout = "10m"
backend_idle_timeout = "10m"
backend_connection_timeout = "5s"
[servers.consul.access]
default = "deny"
rules = [
"allow 99.99.99.1/24", # region1 docker nodes ip`s pool
"allow 199.199.199.1/24", # region 2 docker nodes pool
"allow 200.200.200.200/32", #mage node
"allow 99.99.98.1/32 " #region-1 load balancer
]
[servers.consul.discovery]
failpolicy = "keeplast"
interval = "10s"
timeout = "5s"
kind = "exec"
exec_command = ["/etc/gobetween/consul_node_discovery.sh"]
теперь создадим upstart скрипт /etc/init/gоbetween.conf :
$vim /etc/init/gоbetween.conf
# gobetween service
description "gobetween"
env DAEMON=/usr/sbin/gobetween
env NAME=gobetween
env CONFIG_PATH=/etc/gobetween/gobetween.toml
export GOMAXPROCS=`nproc`
start on runlevel [2345]
stop on runlevel [!2345]
kill signal INT
respawn
respawn limit 10 5
umask 022
expect stop
respawn
script
exec $DAEMON -c $CONFIG_PATH 2>&1
end script
post-stop script
pid=`pidof gobetween`
kill -9 $pid
end script
и теперь запустим балансировщик:
$start gobetween
Теперь у нас есть кластер, можно зайти на http://lb_ip:8888/servers/consul и проверить что список серверов консула определился успешно.
установка Докер нод (серверов с докером)
Наши ноды будут жить в 2 подсетях:
99.99.99.1/24 - region 1
199.199.199.1/24 -region 2
Также внешний ELASTIC IP в Aмазоне — 50.50.50.50
Docker
Итак, повторять тут шаги установки докера на сервер не вижу смысла. Их можно прочесть тут. Я же остановлюсь только на специфических вопросах. Также следует отметить — данное руководство работает для 12 и 14 веток Ubuntu, для Ubuntu 16 требуются те же настройки докер демона, но делаются они чуть по другому.
Начнем с конфигурации докер демона и его запуска.
отредактируем строку инициализации докер демона:
vim /etc/default/docker
требуется добавить строку ниже, остальные строки — закомментировать:
DOCKER_OPTS="-H tcp://0.0.0.0:2375 -H unix:///var/run/docker.sock --label region=region1 "
далее перезагрузим Docker:
$service docker restart
после этого требуется убедиться что докер демон запустился с нужными настройками:
$ps ax |grep docker
должны увидеть что-то типа этого :
10174 ? Ssl 264:59 /usr/bin/docker daemon -H tcp://0.0.0.0:2375 -H unix:///var/run/docker.sock --label region=region
Swarm join
установим Docker Swarm на наш сервер с уже установленным докером. Я не использую Docker Swarm в контейнерах, мне нравится когда все прозрачно и когда я могу инициализировать именно так как хочу и контролировать его по средством простого upstart.
Итак, самый простой способ заполучить бинарный файл со свормом — это скачать его образ себе на локальную машину, потом распакровать образ и извлечь любым возможным способом. Я же предпочитаю собирать его для себя сам. Решать как лучше я оставлю читателю.
Представим что у нас уже есть бинарник с Docker Swarm, мы скопировали его на наш настраиваемый сервер с докером. теперь нам нужно просто настроить Swarm по средством написания upstart-скрипта:
$vim /etc/init/swarm.conf
```upstart
description "Consul server process"
start on (local-filesystems and net-device-up IFACE=eth0)
stop on runlevel [!12345]
respawn
setuid root
setgid root
exec bash -c 'swarm join --ttl 20s --heartbeat 4s --advertise=SERVER_IP:2375 consul://50.50.50.50:8500/swarmcluster/ '
SERVER_IP вы можете легко извлечь сами, у меня он вставляется подставляясь из Elastic IP во время создания сервера в Амазон с помощью Ansible. Это именно тот айпи по которому будет соединятся SWARM Manage к этому докер хосту.
теперь стартуем наш swarm join:
start swarm
проверить можно запросом типа такого
$curl -Ss http://50.50.50.50:8500/v1/kv/swarmcluster/docker/swarm/nodes/?keys&seperator=/&dc=production
и должны получить ответ :
$ ["swarmcluster/docker/swarm/nodes/SERVER_IP:2375"]
Теперь так же размасштабируйте кластер в любое колличество регионов. Меняя в настройках докер демона метки (lables) для каждого региона. Также можно добавлять несколько меток на каждый сервер (удобно делить сервера по регионам а также, скажем, производительности процессора, размеру памяти, типа диска).
установка Swarm Manage
Итак, теперь перейдем непосредственно к установке нашего сворм менеджера. По сути, его установка мало чем отличается от установки Swarm Join.
опять же повторяем шаги по копированию бинарника на сервер и далее создаем UPSTART скрипт:
$vim /etc/init/swarm.conf
description "Consul server process"
start on (local-filesystems and net-device-up IFACE=eth0)
stop on runlevel [!12345]
respawn
setuid root
setgid root
exec bash -c 'swarm manage -H tcp://$0.0.0.0:2377 -strategy "binpack" consul://50.50.50.50:8500/swarmcluster/'
SWARM_MANAGE_IP это айпи нашего Manage. В нашем случае — 200.200.200.200. Остановимся на -strategy эта опция определяет распределение контейнеров по нодам соответствующим все параметрам выборок. При стратегии binpack — вначале заполняется контейнерами первая нода, только потом — вторая. Если у вас сотни стартов/остановок контейнеров в час — это позволяет избежать фрагментации и позволяет удалять ненужные ноды из кластера.
Существуют 3 вида стратегий распределения контейнеров:
spread — распределение на наименее загруженную ноду
binpack — максимально плотная упаковка контейнеров
random — тут нечего и говорить — все и так понятно :) используется только для дебага.
теперь наконец запустим наш swarm manage:
$service swarm start
и проверим что у нас вышло :
$docker -H 0.0.0.0:2377 info
и получим что-то вроде этого :
Containers: 1
Images: 3
Server Version: swarm/1.2.4
Role: primary
Strategy: binpack
Filters: health, port, dependency, affinity, constraint
Nodes: 3
host1: 99.99.99.1:2375
└ Status: Healthy
└ Containers: 0
└ Reserved CPUs: 0 / 8
└ Reserved Memory: 0 B / 16.46 GiB
└ Labels: executiondriver=, kernelversion=3.13.0-86-generic, operatingsystem=Ubuntu 14.04.4 LTS, region=region-1, storagedriver=devicemapper
└ Error: (none)
└ UpdatedAt: 2016-08-21T14:40:03Z
host3: 99.99.99.2:2375
└ Status: Healthy
└ Containers: 0
└ Reserved CPUs: 0 / 2
└ Reserved Memory: 0 B / 3.86 GiB
└ Labels: executiondriver=native-0.2, kernelversion=3.13.0-74-generic, operatingsystem=Ubuntu 14.04.3 LTS, region=region-1, storagedriver=devicemapper
└ Error: (none)
└ UpdatedAt: 2016-08-21T14:40:42Z
host3: 199.199.199.1:2375
└ Status: Healthy
└ Containers: 1
└ Reserved CPUs: 0 / 2
└ Reserved Memory: 512 MiB / 3.86 GiB
└ Labels: executiondriver=native-0.2, kernelversion=3.13.0-74-generic, operatingsystem=Ubuntu 14.04.3 LTS, region=region-2, storagedriver=devicemapper
└ Error: (none)
└ UpdatedAt: 2016-08-21T14:40:55Z
Kernel Version: 3.13.0-44-generic
Operating System: linux
CPUs: 12
Total Memory: 24.18 GiB
Name: lb.ourcoolcluster.com
по сути можно уже запускать контейнера :
$docker tcp://0.0.0.0:2377 run -d -P -e constraint:region==region-1 hello-world
Более подробно о фильтрах и политиках можно почитать тут.
Итак, у нас есть кластер, в котором можно запускать контейнера в зависимости от региона, и других данных. Что же дальше? — попробуем организовать по точку входа для каждого региона.
Создание enterypoint для каждого региона
Теперь попробуем построить сервис дискавери на исключительно внутренних механизмах Sandalone Swarm. Мы будем пользоваться лейблами при запуске контейнеров. Можно делать это руками, или написать свой движок который будет запускать нужные контейнера и одновременно по rest api работать с Балансировщиком. В данной схеме LB настраивается один раз при старте нового сервиса в регионе, где его до этого не было. после этого можно спокойно запускать реплики сервиса по мере роста нагрузки, а также останавливать контейнера при ее падении — дискавери списка нод предоставляющих сервис сделает сам балансировщик.
Также если это необходимо — можно легко держать реплику консула в регионе и так же реплику Swarm managе. Но мы рассмотрим простейшую схему.
Общая схема как все будет работать :
Установку Gobetween мы опустим, приведем толко конфиг с которым он будет запущен:
[logging]
level = "info"
output = "stdout"
[api]
enabled = true
bind = ":8888"
[api.basic_auth]
login = "admin"
password = "admin"
[defaults]
max_connections = 0
client_idle_timeout = "0"
backend_idle_timeout = "0"
backend_connection_timeout = "0"
Этого вполне достаточно для старта балансировщика. все последующие процедуры мы будем проводить через rest api — обычно этим занимаются специальные сервисы. Также для упрощения тестирования — на каждом докер сервере создайте файл /tmp/test и пропишите туда уникальную для каждого сервера информацию. Например "host1" и "host2"
Для примера запустим контейнер:
$docker run -l service=region-1.nginx -d -p 22001:80 -e constraint:region==region-1 -v /tmp/test:/usr/share/nginx/html:ro nginx
$docker run -l service=region-1.nginx -d -p 22001:80 -e constraint:region==region-1 -v /tmp/test:/usr/share/nginx/html:ro nginx
Если у нас в регионе region-1 будет 2 и более нод то контейнера запустятся (Docker Swarm проверяет доступность портов для маппинга). В случае одной докер ноды в регионе можно запустить 2 конейнера таким образом:
$docker run -l service=region-1.nginx -d -p 22002:80 -e constraint:region==region-1 -v /tmp/test:/usr/share/nginx/html:ro nginx
$docker run -l service=region-1.nginx -d -p 22001:80 -e constraint:region==region-1 -v /tmp/test:/usr/share/nginx/html:ro nginx
Контейнеры запустились в нужном регионе и работают. Теперь настало время настроить наш балансировщик:
$curl --user admin:admin -XPOST "http://50.50.50.50:8888/servers/r1nginx" --data '
{
"bind":"LB_IP:LB_PORT",
"protocol": "tcp",
"balance": "weight",
"max_connections": "0",
"client_idle_timeout": "10m",
"backend_idle_timeout": "10m",
"backend_connection_timeout": "1m"
"healthcheck": {
"kind": "ping",
"interval": "2s",
"timeout": "1s"
},
"discovery": {
"kind": "docker",
"docker_endpoint" : "http://50.50.50.50:2377",
"docker_container_private_port" : "80",
"docker_container_label":"service=region-1.nginx"
}
}
'
Где:
LB_IP — IP адрес смотрящий в сторону проверяющего на сервере с запущенным балансировщиком.
LB_PORT — tcp порт на LB_IP смотрящий в сторону проверяющего на сервере с запущенным балансировщиком.
Вот теперь можно проверять что у нас вышло:
$curl -sS http://LB_IP:LB_PORT
host1
$curl -sS http://LB_IP:LB_PORT
host2
Итак, мы рассмотрели один из самых простых, но и при том и довольно таки функциональных способов построения гео распределенного кластера на Docker Swarm standalone. Установка довольно проста и прозрачна, как и поиск и устранение неисправностей. Продумывая данную статью я отдавал себе отчет что не смогу осветить многие аспекты построения и эксплуатации системы данного типа, я преследовал скорее цель заставить читателя взглянуть на проблему под другим углом — необходимой достаточности и аскетичности, ведь сложно сделать легко, а вот легко и стройно — сделать сложно.
Проблемы безопасности и HA я оставил на усмотрение читателей, возможно, если будет интерес я постараюсь их осветить в последующих статьях.
- вторая обзорная статъя
- первая обзорная статъя
- DRAFT Rest API
- Проект на Github
- Вики на Github
- тесты производительности Gobetween
- Установка Gobetween под Windows
Автор: nickdoikov