Docker swarm mode (режим роя)


На хабре вже писали про Docker swarm mode (режим рою), який є новою фичей версії 1.12. Дана опція внесла невелику плутанину в голови тих, хто знайомий з окремо стоїть реалізацією Docker Swarm мала поширення і раніше не відрізнялася зручністю установки і використання. Однак, після додавання Swarm в коробку з Docker все стало набагато простіше, очевидніше і функціональніша.

Докладніше про те, як влаштований новий кластер Docker контейнерів з точки зору користувача, а також про простому і зручному способі розгортання сервісів Docker на довільній інфраструктурі далі під катом.

Для початку, як я і обіцяв в попередній статті, з невеликою затримкою, але все ж випущено реліз Fabricio з підтримкою сервісів Docker. При цьому як і раніше зберігається можливість працювати з окремими контейнерами, плюс, залишилися незмінними інтерфейс користувача і розробника конфігурації, що значно спрощує перехід від конфігурацій, заснованих на окремих контейнерах до резервний та горизонтально масштабованим сервісів.

Активація Docker swarm mode
В режимі swarm всі ноди діляться на два типи: manager і worker. При цьому повноцінний кластер може обходитися без робочих нод взагалі, тобто менеджери за замовчуванням є також і робітниками.

Серед менеджерів завжди присутній один, який на даний момент є лідером кластера. Всі керуючі команди, які виконуються на інших менеджерів автоматично перенаправляються на нього.


Приклад списку нод працюючого кластера Docker
$ docker node ls
ID HOSTNAME STATUS AVAILABILITY MANAGER STATUS
6pbqkymsgtnahkqyyw7pccwpz * docker-1 Ready Active Leader
avjehhultkslrlcrevaqc4h5f docker-2 Ready Active Reachable
cg1maoa11ep7h14f2xciwylf3 docker-3 Ready Active Reachable


Для включення режиму swarm досить вибрати хост, який буде початковим лідером в майбутньому кластері, і виконати на ньому всього одну команду:

docker swarm init

Після того, як «рой» ініціалізований, він вже готовий для запуску не ньому будь-якої кількості сервісів. Щоправда, стан такого кластера буде неконсистентным (консистентное стан досягається при кількості менеджерів не менше 3). І звичайно ні про яке масштабуванні та відмовостійкості в цьому випадку мови бути не може. Для цього до кластеру потрібно підключити ще хоча б дві керуючі ноди. Дізнатися про те, як це зробити, виконавши на лідера наступні команди:

Додавання керуючої ноди
$ docker swarm join-token manager
To add a manager to this swarm, run the following command:

docker swarm join \
--token SWMTKN-1-1yptom678kg6hryfufjyv1ky7xc4tx73m8uu2vmzm1rb82fsas-c12oncaqr8heox5ed2jj50kjf \
172.28.128.3:2377


Додавання робочої ноди
$ docker swarm join-token worker
To add a worker to this swarm, run the following command:

docker swarm join \
--token SWMTKN-1-1yptom678kg6hryfufjyv1ky7xc4tx73m8uu2vmzm1rb82fsas-511vapm98iiz516oyf8j00alv \
172.28.128.3:2377


Додавати і видаляти ноди можна в будь-який момент життя кластера — це жодним серйозним чином не впливає на його працездатність.

Створення сервісу
Створення сервісу в Docker принципово не відрізняється від створення контейнера:

docker service create --name nginx --publish 8080:80 --replicas 2 nginx:stable-alpine

Відмінності, як правило, полягають у різному наборі опцій. Наприклад, у сервісу немає опції --volume, але є опція --mount — ці опції дозволяють підключати до контейнерів локальні ресурси нод, але роблять це по-різному.

Оновлення сервісу
Тут починається найбільша відмінність роботи контейнерів від роботи кластера контейнерів (сервісу). Зазвичай, щоб оновити одиночний контейнер, доводиться зупиняти поточний і запускати новий. Це призводить хоча й до незначного, але існуючого часу простою вашого сервісу (якщо ви не потурбувалися про те, щоб обробляти такі ситуації за допомогою інших інструментів).

При використанні сервісу з кількістю реплік не менше 2 простою сервісу в більшості випадків не відбувається. Це досягається за рахунок того, що Docker оновлює контейнери сервісу по черзі. Тобто в один і той же момент часу завжди є хоча б один працюючий контейнер, який може обслужити запит користувача.

Для оновлення (у тому числі додавання і видалення) властивостей сервісу, які можуть мати кілька значень (наприклад, --publish або --label), Docker пропонує використовувати спеціальні опції, що закінчуються суфіксами -add та -rm:

# додавання в сервіс нової мітки
docker service update --label-add foo=bar nginx

Видалення деяких опцій, проте, менш тривіально і часто залежить від самої опції:

# мітки видаляються за іменем
docker service update --label-rm foo nginx

# порти видаляються за значенням порту призначення (target port)
docker service update --publish-rm 80 nginx

Подробиці про кожної опції можна дізнатися в описі команди docker update service.

Масштабування і балансування

Для розподілу запитів між наявними нодами Docker використовується схема звана ingress load balacing. Суть цього механізму полягає в тому, що на яку б з нод не прийшов запит користувача, він спочатку пройде через внутрішній механізм балансування, а потім буде перенаправлено на ту ноду, яка в цей момент може обслужити такий запит. Тобто, будь-яка нода здатна обробити запит до будь-якого з сервісів кластера.

Масштабування сервісу Docker досягається за рахунок вказівки необхідної кількості реплік. В той момент, коли вам необхідно збільшити (або зменшити) кількість мод, що обслуговують запити від клієнта, ви просто оновлюєте властивості сервісу із зазначенням потрібного значення параметру --replicas:

docker service update --replicas 3 nginx

В цьому випадку треба не забути попередньо переконатися, що кількість доступних нод не менше, ніж кількість реплік, які ви хочете використовувати. Хоча нічого страшного не станеться навіть якщо нсд менше, ніж реплік — просто деякі ноди запустять у себе більше одного контейнера одного і того ж сервісу (в іншому випадку Docker буде намагатися запускати репліки одного сервісу на різних ноди).

Відмовостійкість

Відмовостійкість сервісу гарантується самим Docker. Це досягається в тому числі за рахунок того, що в кластері можуть одночасно працювати кілька керуючих нод, які можуть у будь-який момент замінити що вийшов з ладу лідера. Якщо говорити більш детально, то використовується так званий алгоритм підтримки розподіленого консенсусу — Raft. Зацікавленим рекомендую подивитися цю чудову візуальну демонстрацію: Raft в роботі.

Raft: вибір нового лідера

Автоматичний деплой
Деплой нової версії програми на бойові сервера завжди супроводжується ризиком того, що щось піде не так. Саме тому вважається поганою прикметою викочувати нову версію програми перед вихідними та святами. Причому, перед тривалими святами, начебто новорічних канікул, будь-які зміни на бойовий інфраструктурі припиняються за тиждень а то й за два до їх початку.

Незважаючи на те, що сервіси Docker пропонують цілком надійний спосіб запуску оновлення програми, досить часто швидкий відкат до попередньої версії утруднений по тій або іншій причині, що може служити причиною недоступності вашого сервісу користувачам протягом багатьох годин.

Найнадійніший спосіб уникнути проблем при оновленні програми — автоматизація та тестування. Саме для цього розробляються системи автоматичного деплоя. Важливою частиною таких систем, як правило, є можливість швидкого оновлення і відкат до попередньої версії на будь-якій обраній інфраструктурі.

Fabricio
Більшість інструментів для автоматизації деплоя пропонують описувати конфігурацію за допомогою популярних мов розмітки начебто XML або YAML. Деякі йдуть далі і розробляють свій власний мову опису таких конфігурацій (наприклад, HCL або Puppet language). Я ж не бачу необхідності йти ні по одному з цих шляхів з наступних причин:

  • XML/YAML ніколи не зрівняються по можливостям розширення і використання з повноцінними мовами програмування, а прагнення спростити конфігурування через використання спрощеної розмітки часто навпаки, лише ускладнює Плюс, мало хто з програмістів захоче програмувати на XML/YAML, адже конфігурування — це і є приватний випадок програмування.
  • Розробка свого власного мови програмування — надзвичайно складний і виснажливий процес, найчастіше нестоящий витрачених на неї зусиль.
Тому Fabricio для описи конфігурацій використовує звичайний Python і частина надійних і перевірених часом бібліотек (серед них небезизвестный Fabric).

Звичайно, багато хто може заперечити з цього приводу, що, мовляв, не всі розробники і DevOps знають Python. Ну, по-перше, Python (так само як і Bash) входить в джентльменський набір скриптових мов, які повинен знати кожен поважаючий себе DevOps (ну або майже кожен). А по-друге, як це не парадоксально, знати Python практично необов'язково. На підтвердження своїх слів наводжу приклад конфігурації сервісу заснованого на Django для Fabricio:

fabfile.py
from fabricio import tasks
from fabricio.apps.python.django import DjangoService

django = tasks.DockerTasks(
service=DjangoService(
name="django",
image="project/django",
options={
"publish": "8080:80",
"env": "DJANGO_SETTINGS_MODULE=my_settings",
"replicas": 3,
},
),
hosts=["user@manager1", "user@manager2", "user@manager3"],
)


Погодьтеся, що цей приклад не складніше, ніж аналогічне опис на YAML. Людина, яка володіє хоча б однією мовою програмування, розбереться в даному конфіги без особливих проблем.

Але досить лірики.

Процес деплоя
Схематично процес деплоя сервісу за допомогою Fabricio виглядає так, як показано на малюнку нижче (після виконання команди fab django для описаного вище конфига):



Розглянемо кожен пункт порядку. Для початку, одразу хочу зауважити, що представлена схема актуальна при включеному режимі паралельного виконання (з зазначеної опцією --parallel). Відмінність послідовного режиму тільки в тому, що всі дії в ньому виконуються строго послідовно.

Відразу після запуску команди деплоя послідовно починають виконуватися наступні кроки:

  • pull, одночасно на всіх ноди запускається процес скачування нового способу Docker. Зауважу, що в конфігурації достатньо вказати тільки адреси керуючих нсд (менеджерів), при цьому навіть необов'язково перелічувати всіх наявних менеджерів — незазначені ноди буде автоматично оновлено самим Docker. Хоча ніщо не заважає вказати в конфігурації в тому числі і воркеров (в деяких випадках це буває необхідно, наприклад, при використання SSH тунелю).
  • migrate, наступний крок — застосування міграцій. Важливо, щоб цей крок одночасно виконувався лише на одній з поточних нод, тому Fabricio у цьому випадку використовує спеціальний механізм, який гарантує, що процес міграції буде запущений тільки на одній ноде і виконається тільки один раз.
  • update, так як для оновлення всіх контейнерів сервісу команду update досить виконати тільки один раз, то Fabricio на цьому етапі також стежить за тим, щоб вона не була виконана двічі.
Кожну команду (pull, migrate, update) у разі необхідності можна виконати окремо. У процес деплоя також можна включити додаткові кроки (prepare, push, backup) як описано в цій більш ранньої оглядовій статті про Fabricio.

Всі команди Fabricio (крім backup і restore) є идемпотентными, то є безпечними при повторному виконанні з тими ж самими параметрами.

Тест на идемпотентностьfab --parallel nginx
$ fab --parallel nginx
[vagrant@172.28.128.3] Executing task 'nginx.pull'
[vagrant@172.28.128.4] Executing task 'nginx.pull'
[vagrant@172.28.128.5] Executing task 'nginx.pull'
[vagrant@172.28.128.5] run: docker pull nginx:stable-alpine
[vagrant@172.28.128.4] run: docker pull nginx:stable-alpine
[vagrant@172.28.128.3] run: docker pull nginx:stable-alpine
[vagrant@172.28.128.3] out: stable-alpine: Pulling from library/nginx
[vagrant@172.28.128.3] out: Digest: sha256:ce50816e7216a66ff1e0d99e7d74891c4019952c9e38c690b3c5407f7af57555
[vagrant@172.28.128.3] out: Status: Image is up to date for nginx:stable-alpine
[vagrant@172.28.128.3] out: 
[vagrant@172.28.128.4] out: stable-alpine: Pulling from library/nginx
[vagrant@172.28.128.4] out: Digest: sha256:ce50816e7216a66ff1e0d99e7d74891c4019952c9e38c690b3c5407f7af57555
[vagrant@172.28.128.4] out: Status: Image is up to date for nginx:stable-alpine
[vagrant@172.28.128.4] out: 
[vagrant@172.28.128.5] out: stable-alpine: Pulling from library/nginx
[vagrant@172.28.128.5] out: Digest: sha256:ce50816e7216a66ff1e0d99e7d74891c4019952c9e38c690b3c5407f7af57555
[vagrant@172.28.128.5] out: Status: Image is up to date for nginx:stable-alpine
[vagrant@172.28.128.5] out: 
[vagrant@172.28.128.3] Executing task 'nginx.migrate'
[vagrant@172.28.128.4] Executing task 'nginx.migrate'
[vagrant@172.28.128.5] Executing task 'nginx.migrate'
[vagrant@172.28.128.5] run: docker info 2>&1 | grep 'Is Manager:'
[vagrant@172.28.128.4] run: docker info 2>&1 | grep 'Is Manager:'
[vagrant@172.28.128.3] run: docker info 2>&1 | grep 'Is Manager:'
[vagrant@172.28.128.3] Executing task 'nginx.update'
[vagrant@172.28.128.4] Executing task 'nginx.update'
[vagrant@172.28.128.5] Executing task 'nginx.update'
[vagrant@172.28.128.5] run: docker inspect --type image nginx:stable-alpine
[vagrant@172.28.128.4] run: docker inspect --type image nginx:stable-alpine
[vagrant@172.28.128.3] run: docker inspect --type image nginx:stable-alpine
[vagrant@172.28.128.3] run: docker inspect --type container nginx_current
[vagrant@172.28.128.3] run: docker info 2>&1 | grep 'Is Manager:'
[vagrant@172.28.128.4] run: docker inspect --type container nginx_current
[vagrant@172.28.128.3] run: docker service inspect nginx
[vagrant@172.28.128.4] run: docker info 2>&1 | grep 'Is Manager:'
[vagrant@172.28.128.3] No changes detected, update skipped.
[vagrant@172.28.128.4] No changes detected, update skipped.
[vagrant@172.28.128.5] run: docker inspect --type container nginx_current
[vagrant@172.28.128.5] run: docker info 2>&1 | grep 'Is Manager:'
[vagrant@172.28.128.5] No changes detected, update skipped.

Done.
Disconnecting from vagrant@127.0.0.1:2222... done.
Disconnecting from vagrant@127.0.0.1:2200... done.
Disconnecting from vagrant@127.0.0.1:2201... done.


fab nginx
$ fab nginx
[vagrant@172.28.128.3] Executing task 'nginx.pull'
[vagrant@172.28.128.3] run: docker pull nginx:stable-alpine
[vagrant@172.28.128.3] out: stable-alpine: Pulling from library/nginx
[vagrant@172.28.128.3] out: Digest: sha256:ce50816e7216a66ff1e0d99e7d74891c4019952c9e38c690b3c5407f7af57555
[vagrant@172.28.128.3] out: Status: Image is up to date for nginx:stable-alpine
[vagrant@172.28.128.3] out: 
[vagrant@172.28.128.4] Executing task 'nginx.pull'
[vagrant@172.28.128.4] run: docker pull nginx:stable-alpine
[vagrant@172.28.128.4] out: stable-alpine: Pulling from library/nginx
[vagrant@172.28.128.4] out: Digest: sha256:ce50816e7216a66ff1e0d99e7d74891c4019952c9e38c690b3c5407f7af57555
[vagrant@172.28.128.4] out: Status: Image is up to date for nginx:stable-alpine
[vagrant@172.28.128.4] out: 
[vagrant@172.28.128.5] Executing task 'nginx.pull'
[vagrant@172.28.128.5] run: docker pull nginx:stable-alpine
[vagrant@172.28.128.5] out: stable-alpine: Pulling from library/nginx
[vagrant@172.28.128.5] out: Digest: sha256:ce50816e7216a66ff1e0d99e7d74891c4019952c9e38c690b3c5407f7af57555
[vagrant@172.28.128.5] out: Status: Image is up to date for nginx:stable-alpine
[vagrant@172.28.128.5] out: 
[vagrant@172.28.128.3] Executing task 'nginx.migrate'
[vagrant@172.28.128.3] run: docker info 2>&1 | grep 'Is Manager:'
[vagrant@172.28.128.4] Executing task 'nginx.migrate'
[vagrant@172.28.128.4] run: docker info 2>&1 | grep 'Is Manager:'
[vagrant@172.28.128.5] Executing task 'nginx.migrate'
[vagrant@172.28.128.5] run: docker info 2>&1 | grep 'Is Manager:'
[vagrant@172.28.128.3] Executing task 'nginx.update'
[vagrant@172.28.128.3] run: docker inspect --type image nginx:stable-alpine
[vagrant@172.28.128.3] run: docker inspect --type container nginx_current
[vagrant@172.28.128.3] run: docker service inspect nginx
[vagrant@172.28.128.3] No changes detected, update skipped.
[vagrant@172.28.128.4] Executing task 'nginx.update'
[vagrant@172.28.128.4] run: docker inspect --type image nginx:stable-alpine
[vagrant@172.28.128.4] run: docker inspect --type container nginx_current
[vagrant@172.28.128.4] No changes detected, update skipped.
[vagrant@172.28.128.5] Executing task 'nginx.update'
[vagrant@172.28.128.5] run: docker inspect --type image nginx:stable-alpine
[vagrant@172.28.128.5] run: docker inspect --type container nginx_current
[vagrant@172.28.128.5] No changes detected, update skipped.

Done.
Disconnecting from vagrant@172.28.128.3... done.
Disconnecting from vagrant@172.28.128.5... done.
Disconnecting from vagrant@172.28.128.4... done.



Відкат до попередньої версії
Відкат до попередньої версії (команда fab django.rollback для раніше описаної конфігурації) багато в чому аналогічний процесу деплоя:


І відкат міграцій, і відкат самого сервісу до попереднього стану виконуються строго один раз на одній з менеджерських нсд.

Висновок
За контейнерезацией майбутнє серверної розробки. Ті, хто цього ще не усвідомив, скоро будуть поставлені перед доконаним фактом. Контейнери — зручне, просте і потужна зброя в руках розробників і DevOps.

З виходом Docker 1.12 у прихильників Kubernetes практично не залишилося аргументів на користь використання останнього. Сервіси Docker не тільки забезпечують ті ж можливості, що і сервіси Kubernetes, але при цьому володіють навіть поруч переваг, завдяки простоті установки на будь ОС (Linux, macOS, Windows) і відсутності необхідності установки і запуску додаткових компонентів (контейнерів).

Fabricio — інструмент, який допомагає в розробці, тестуванні та викладення нових версій додатків на бойові та тестові сервера за допомогою Docker — тепер підтримує розгортання масштабованих і відмовостійких сервісів. З різними варіантами використання Fabricio можна познайомитися на сторінці прикладами і рецептами (всі приклади докладно описані і автоматизовані за допомогою Vagrant).

Докладно про Fabricio я сподіваюся розповісти на заході DevOpsDays в Москві. Приходьте, буде про що поспілкуватися і дізнатися багато нового.
Джерело: Хабрахабр

0 коментарів

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