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

Преамбула
Деякий час тому перед нами постало завдання спроектувати і розгорнути систему для потокового відео. Суть була в масовому запуску/зупинки інстанцій, на яких відбувається зворотна збірка потокового відео і стрімінг на безліч 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 кластера.
Більше того — таких Мападе інстанцій при бажанні можна зробити скільки завгодно, але сенсу немає — нижче поясню чому.
Найбільший плюс такої системи — все прозоро, керовано, масштабируемо.
Отже, почнемо з конфігурації консул кластера.
Установка і настройка 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 в Амазоні — 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 мападе.
Але ми розглянемо найпростішу схему.
Загальна схема як все буде працювати :
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 я залишив на розсуд читачів, можливо, якщо буде інтерес я постараюся висвітлити їх у наступних статтях.
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.