Pull to refresh

Docker Swarm+Consul+Gobetween в виде движка для гео распределенного кластера

Reading time 14 min
Views 17K

Преамбула


Некоторе время назад перед нами стала задача спроектировать и развернуть систему для потокового видео. Суть была в массовом запуске/остановке инстанций, на которых происходит обратная сборка потокового видео и стриминг на множество media cdn провайдеров (youtube, livestream, ustream итд ) а также на собственные rtmp и ts точки назначения. Каждая инстанция требовала настройки перед запуском т.к. содержала специфическую для каждого клиента информацию. Также было понятно, что система должна работать в большом количестве регионов (как минимум во всех точках, где присутствует Амазон, а как максимум — в любом месте где можно арендовать сервер). Также основное требование — запуск инстанции в течении 1-2 секунд максимум, чтобы это было прозрачно для пользователя.



Было это в начале 2015 года уже были разговоры о том что Docker в скором времени выпускает родную систему кластеризации. И 26 февраля 2015 года она таки вышла. Сразу стало понятно что это для нас серебряная пуля и Swarm идеально ложится на наш проект. Проект был запущен в мае 2015.


Через 11 месяцев мы запустили второй, более сложный проект в котором требовалось организовать статические точки входа (ip:port) для оборудования третьих производителей, расширив при этом всю логику работы и запуска инстанций динамически в нужном регионе.


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


Задача


В данной статье я хотел бы поделится идеей и принципиальной реализацией построения распределенной системы.
Я специально опущу аспект безопасности — тут уж каждый решает как / что лучше строить. Некоторые закрываются паролями/сертификатами и т.д., некоторые выносят менеджмент часть в сети не пересекающиеся напрямую с сетями общего доступа (живут внутри vpc и связаны между собой по-средством тоннелей).


Принципиальная схема докер кластера на Standalone Swarm.


Рассмотрим систему принципиально — как большое колличество регионов и с одним ядром(урпавляющим).


image

В данном случае рассматривается система с одним мастером и большим количеством докер хостов, которые анонсируют себя в консул кластер. Почему так? да потому что по сути 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
sever3.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 -bind="IP_ADDRESS"

Сервис должен запустится и захватить терминал. В 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е. Но мы рассмотрим простейшую схему.


Общая схема как все будет работать :


image

Установку 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 я оставил на усмотрение читателей, возможно, если будет интерес я постараюсь их осветить в последующих статьях.


Tags:
Hubs:
+11
Comments 12
Comments Comments 12

Articles