Когда стоит задача по построению среды с требованиями к отказоустойчивости, масштабируемости и возможности распределения нагрузки, то конечно смотришь на системы кластеризации. Если речь идет еще и о контейнерной кластеризации, то выбор в сторону систем оркестрации контейнеров.
Оркестровка — это управление и координация взаимодействия контейнеров.
Docker Swarm является стандартным инструментом кластеризации для Docker. Хосты Docker объединяются в один последовательный кластер, называемый Swarm. Docker Swarm обеспечивает доступность и высокую производительность работы, равномерно распределяя ее по нодам Docker внутри кластера. Это очень похоже на кластеры Kubernetes, а может и наоборот.
Вообще если сравнивать Docker Swarm с Kubernetes, то Kubernetes более масштабен и гибок, но Swarm это инструмент который доступен из коробки и для его разворачивание не нужны такие трудозатраты и вообще он прост, что является его плюсом: рабочий кластер Swarm можно поднять за минуту. Прекрасно подойдет для небольших и средних компаний или проектов, где не нужна гибкость Kubernetes.
В некоторых материал встречаются такие цифры: Docker Swarm можно рассматиривать для запуска не более 50 тысяч контейнеров и 1000 нод. При этих достаточно внушительных цифрах у Swarm-а нет автомасштабирования (autoscaling).
Docker Swarm это по сути состояние Docker Engine работающий в кластере. Плюс кластерные хосты объединяются единым контейнерным пространством.
В режиме Swarm в Docker появляются понятия stack и service:
docker node
docker stack
docker service
Стеки, сервисы — это еще одни уровни абстракции над контейнерами. У них будут имена, порты, тома и т.д.
Попробуем поднять свой кластер Docker Swarm. Для этого нам необходимо развернуть на какой-либо платформе три хоста (выбор цифры 3 наверно является уже академическим, можно для себя выбрать иное число, главное понять сущность происходящего).
На хостах (нодах) должны быть развернуты Docker Engine.
И так, наш стенд. Я использую Vagrant на Ubuntu 20.04, ноды тоже будут на Ubuntu Focal, на них установлен Docker.
Описание стенда:
Ноды:
master - 192.168.10.11
worker1 - 192.168.10.12
worker2 - 192.168.10.13
Установка Docker Engine на ноды
Для развертывания Docker Engine на Ubuntu используем скрипт:
#!/bin/env bash
sudo apt-get update -y
sudo apt-get install -y \
apt-transport-https \
ca-certificates \
curl \
gnupg \
lsb-release
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg
echo \
"deb [arch=amd64 signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu \
$(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt-get update -y
sudo apt-get install -y docker-ce docker-compose docker-ce-cli containerd.io
Или, что более предпочтительнее для меня, Ansible:
---
- name: 'Install Docker Engine to Ubuntu'
hosts: all
become: true
tasks:
- name: Install prerequisites for Docker repository
apt:
name: ['apt-transport-https', 'ca-certificates', 'curl', 'gnupg2', 'software-properties-common']
update_cache: yes
- name: Add Docker GPG key
apt_key: url=https://download.docker.com/linux/ubuntu/gpg
- name: Add Docker APT repository
apt_repository:
repo: deb [arch=amd64] https://download.docker.com/{{ ansible_system | lower }}/{{ ansible_distribution | lower }} {{ ansible_distribution_release }} stable
- name: Install Docker CE
apt:
name: ['docker-ce', 'docker-ce-cli', 'containerd.io']
update_cache: yes
- name: Install prerequisites for docker-compose
apt:
name: ['python3-pip', 'python3-setuptools', 'virtualenv']
- name: Install docker-compose
pip:
name: docker-compose
Инициализация Docker Swarm
Создаем кластер Swarm:
sudo docker swarm init
Если есть несколько интерфейсов на сервере, определяем на каком интерфейсе работает swarm (ip или интерфейс):
sudo docker swarm init --advertise-addr 192.168.10.11
или
sudo docker swarm init --advertise-addr eth{X}
vagrant@master:~$ sudo docker swarm init --advertise-addr 192.168.10.11
Swarm initialized: current node (c8c1bngf6s7upknjbwykv3q2v) is now a manager.
To add a worker to this swarm, run the following command:
docker swarm join --token SWMTKN-1-0c77d9x5mph6k6zizn93290degg1z2c25rotgbabxrwx4sh0by-f03hbqskhbszllpt4vbb95qvv 192.168.10.11:2377
To add a manager to this swarm, run 'docker swarm join-token manager' and follow the instructions.
При создании кластера Swarm сгенериет токен для всех участников данного кластера и при подключении к кластеру необходимо указывать данный токен.
Добавляем наши ноды в кластер:
sudo docker swarm join --token SWMTKN-1-0c77d9x5mph6k6zizn93290degg1z2c25rotgbabxrwx4sh0by-f03hbqskhbszllpt4vbb95qvv 192.168.10.11:2377
vagrant@worker1:~$ sudo docker swarm join --token SWMTKN-1-0c77d9x5mph6k6zizn93290degg1z2c25rotgbabxrwx4sh0by-f03hbqskhbszllpt4vbb95qvv 192.168.10.11:2377
This node joined a swarm as a worker.
vagrant@worker2:~$ sudo docker swarm join --token SWMTKN-1-0c77d9x5mph6k6zizn93290degg1z2c25rotgbabxrwx4sh0by-f03hbqskhbszllpt4vbb95qvv 192.168.10.11:2377
This node joined a swarm as a worker.
Проверим состояние нашего кластера:
vagrant@master:~$ sudo docker node ls
ID HOSTNAME STATUS AVAILABILITY MANAGER STATUS ENGINE VERSION
c8c1bngf6s7upknjbwykv3q2v * master Ready Active Leader 20.10.5
w0r72yyrxv10fq9zsvdwea7u7 worker1 Ready Active 20.10.5
s7klsqwemj419vfbxqg3tvl3l worker2 Ready Active 20.10.5
vagrant@master:~$ sudo docker stack ls
NAME SERVICES ORCHESTRATOR
vagrant@master:~$ sudo docker service ls
ID NAME MODE REPLICAS IMAGE PORTS
vagrant@master:~$ sudo docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
Как видим наш кластер активен, состоит из master, worker1 и worker2. Стеков, сервисов и контейнеров не запущено.
Web интерфейс для Swarm
Не знаю, что лучше, работать через CLI или веб-интерфейс, тут выбор за каждым, но для представления развернем Portainer.
Portainer состоит из двух элементов: сервера Portainer и агента Portainer. Оба элемента работают как легкие контейнеры в кластере Swarm. Агенты обычно разворачиваются на всех нодах кластера Swarm и «информируют» сервер о состоянии нод.
По умолчанию веб-интерфейс Portainer открыт через порт 9000, а агенты через порт 8000.
Кстати, Portainer разворачивают и на Kubernetes кластерах.
Установим:
curl -L https://downloads.portainer.io/portainer-agent-stack.yml -o portainer-agent-stack.yml
docker stack deploy -c portainer-agent-stack.yml portainer
YAML файл для разворачивания стека очень похож на файл для docker-compose:
cat portainer-agent-stack.yml
version: '3.2'
services:
agent:
image: portainer/agent
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- /var/lib/docker/volumes:/var/lib/docker/volumes
networks:
- agent_network
deploy:
mode: global
placement:
constraints: [node.platform.os == linux]
portainer:
image: portainer/portainer-ce
command: -H tcp://tasks.agent:9001 --tlsskipverify
ports:
- "9000:9000"
- "8000:8000"
volumes:
- portainer_data:/data
networks:
- agent_network
deploy:
mode: replicated
replicas: 1
placement:
constraints: [node.role == manager]
networks:
agent_network:
driver: overlay
attachable: true
volumes:
portainer_data:
Вкратце, из файла видно, что сначала собирается агент с deploy mode: global, т.е. на всех нодах, с условием, что на нодах Linux система, после собирается сервер Portainer:
deploy:
mode: replicated
replicas: 1
placement:
constraints: [node.role == manager]
- реплики = 1
- условие на ноде = manager, т.е. master
Еще раз проверим, что изменилось в нашем кластере:
vagrant@master:~$ sudo docker stack ls
NAME SERVICES ORCHESTRATOR
portainer 2 Swarm
vagrant@master:~$ sudo docker service ls
ID NAME MODE REPLICAS IMAGE PORTS
258z8x88618i portainer_agent global 3/3 portainer/agent:latest
im61j820aio8 portainer_portainer replicated 1/1 portainer/portainer-ce:latest *:8000->8000/tcp, *:9000->9000/tcp
Зайдем на веб интерфейс — http://192.168.10.11:9000/
https://documentation.portainer.io/v2.0/deploy/ceinstallswarm/
Разворачиваем свой контейнер в кластере
Для примера возьмём простой контейнер со своей сборкой Nginx и развернем.
sudo docker service create --replicas 2 --publish 8080:80 --name nginx-srv airmeno/alpine-nginx
Проверим наш сервис:
vagrant@master:~$ sudo docker service ls
ID NAME MODE REPLICAS IMAGE PORTS
zf4m0vk687av nginx-srv replicated 2/2 airmeno/alpine-nginx:latest *:8080->80/tcp
258z8x88618i portainer_agent global 3/3 portainer/agent:latest
im61j820aio8 portainer_portainer replicated 1/1 portainer/portainer-ce:latest *:8000->8000/tcp, *:9000->9000/tcp
Тут же можно масштабировать сервис или через cli:
sudo docker service scale zf4m0vk687av=3
vagrant@master:~$ sudo docker service ps zf4m0vk687av
ID NAME IMAGE NODE DESIRED STATE CURRENT STATE ERROR PORTS
wf7ypamsd6ij nginx-srv.1 airmeno/alpine-nginx:latest worker2 Running Running 13 minutes ago
mkv4igumbavk nginx-srv.2 airmeno/alpine-nginx:latest worker1 Running Running 13 minutes ago
by7wvtw14yq4 nginx-srv.3 airmeno/alpine-nginx:latest master Running Running 3 minutes ago
Обратное масштабирование сервиса (уменьшаем количество развернутых контейнеров):
sudo docker service scale zf4m0vk687av=1
vagrant@master:~$ sudo docker service ps zf4m0vk687av
ID NAME IMAGE NODE DESIRED STATE CURRENT STATE ERROR PORTS
wf7ypamsd6ij nginx-srv.1 airmeno/alpine-nginx:latest worker2 Running Running 34 minutes ago
Проверим работу контейнера:
Притом не имеет значение по какому из адресов будет обращение к кластеру и работает ли на ноде по этому адресу контейнер.
Разворачиваем стек
Стек — это объединенные по функционалу сервисы. Например у нас есть классическая связка Nginx + PHP-FPM, развернутая на отдельных контейнерах. Создаем сервис для Nginx c определенными параметрами доступности и отказоустойчивости и создаем так же сервис для php-fpm с распределением нагрузки. Для совместного функционирования создается стек. Утрируя, стек это аналог компосера для сервисов.
Команды для стека:
sudo docker stack ls
sudo docker stack ps portainer
sudo docker stack services portainer
sudo docker stack rm portainer
Создаем файл для деплоя стека:
cat my-nginx-stack.yml
version: '3.8'
services:
nginx:
image: airmeno/otus-nginx
ports:
- 80:80
volumes:
- /opt/code:/www:rw
networks:
- nginx-php
deploy:
mode: replicated
replicas: 1
placement:
constraints: [node.platform.os == linux]
myphp:
image: airmeno/otus-php
container_name: myphp
volumes:
- /opt/code:/www:rw
networks:
- nginx-php
deploy:
mode: replicated
replicas: 2
placement:
constraints: [node.platform.os == linux]
networks:
nginx-php:
sudo docker stack deploy -c my-nginx-stack.yml mynginx
Проверим:
vagrant@master:~$ sudo docker service ls
ID NAME MODE REPLICAS IMAGE PORTS
80mf4xnlq5sl mynginx_myphp replicated 2/2 airmeno/otus-php:latest
p65lm40kmz9p mynginx_nginx replicated 1/1 airmeno/otus-nginx:latest *:80->80/tcp
zf4m0vk687av nginx-srv replicated 1/1 airmeno/alpine-nginx:latest *:8080->80/tcp
258z8x88618i portainer_agent global 3/3 portainer/agent:latest
im61j820aio8 portainer_portainer replicated 1/1 portainer/portainer-ce:latest *:8000->8000/tcp, *:9000->9000/tc
vagrant@master:~$ sudo docker stack ls
NAME SERVICES ORCHESTRATOR
mynginx 2 Swarm
portainer 2 Swarm
Создаем файл по месту назначения из опции volumes /opt/code:/www
sudo bash -c 'echo "<?php phpinfo(); ?>" > /opt/code/index.php'
Проверим наш стек: