По заверениям разработчиков на сегодняшний день Docker Swarm легко и просто позволяет масштабировать ваши приложения на множество хостов с десятков до тысяч контейнеров. Но выполняется ли это обещание в реальности? Давайте проверим запуск 1000 контейнеров на трех виртуальных серверах в Облаке КРОК и узнаем об этом наверняка.

Запускаем виртуальных машин

Весь эксперимент мы будем проводить на трех виртуальных серверах Ubuntu 16.04 размером m1.large (2 vCPU, 8 Gb RAM). Все серверы подключены к единой виртуальной сети и имеют следующие имена:

  • master.swarm-demo.avmaksimov.ru
  • node-1.swarm-demo.avmaksimov.ru
  • node-2.swarm-demo.avmaksimov.ru

Установка Docker

Разрешаем доступ к виртуальной сети по SSH (22/TCP) и 2376/TCP для управления хостами с Docker Machine.

Установим Docker на каждый из серверов следующими командами:

docker-machine create -d generic \ 
--generic-ssh-user ec2-user \
--generic-ssh-key ~/.ssh/id_rsa \
--generic-ip-address 185.12.28.100 \
master.swarm-demo.avmaksimov.ru
docker-machine create -d generic \
--generic-ssh-user ec2-user \
--generic-ssh-key ~/.ssh/id_rsa \
--generic-ip-address 185.102.122.120 \
node-1.swarm-demo.avmaksimov.ru
docker-machine create -d generic \
--generic-ssh-user ec2-user \
--generic-ssh-key ~/.ssh/id_rsa \
--generic-ip-address 185.12.28.39 \
node-2.swarm-demo.avmaksimov.ru

Проверим установку:

docker-machine ls
NAME ACTIVE DRIVER STATE URL SWARM DOCKER ERRORS
master.swarm-demo.avmaksimov.ru - generic Running tcp://185.12.28.100:2376 v17.03.1-ce
node-1.swarm-demo.avmaksimov.ru - generic Running tcp://185.102.122.120:2376 v17.03.1-ce
node-2.swarm-demo.avmaksimov.ru - generic Running tcp://185.12.28.39:2376 v17.03.1-ce

Инициализируем Swarm кластер. На master узле выполним команду:

eval $(docker-machine env master.swarm-demo.avmaksimov.ru) && \
docker swarm init

Команда для выполнения на node-1 и node-2 будет в выводе предыдущей команды. Не забудьте переключить переменные окружения для клиента docker:

eval $(docker-machine env node-1.swarm-demo.avmaksimov.ru) && \
docker swarm join \
--token SWMTKN-1-5alztmf6vhq9sbxrlgd6fysdg1xasdfkoxahl7by9q6vt621u-aso816y7t5qk1n5l9qboxfi0g \
192.168.1.11:2377
This node joined a swarm as a worker.
eval $(docker-machine env node-2.swarm-demo.avmaksimov.ru) && \
docker swarm join \
--token SWMTKN-1-5alztmf6vhq9sbxrlgd6fysdg1xasdfkoxahl7by9q6vt621u-aso816y7t5qk1n5l9qboxfi0g \
192.168.1.11:2377
This node joined a swarm as a worker.

Проверим состояние Swarm кластера:

docker node ls
ID HOSTNAME STATUS AVAILABILITY MANAGER STATUS
co835i5lt8ojqi8av39euuvdp node-1.swarm-demo.avmaksimov.ru Ready Active
jokrnk2o566g6flcim6hlmaow node-2.swarm-demo.avmaksimov.ru Ready Active
o6b0kelc4n6u57ep6xt7bacyj * master.swarm-demo.avmaksimov.ru Ready Active Leader

Создадим для запуска тестовых контейнеров отдельную сеть:

eval $(docker-machine env master.swarm-demo.avmaksimov.ru) && \
docker network create --driver overlay --subnet 10.0.0.0/20 \
--attachable swarm-scale-test-net
bx2idzv2629ed8mlfnvi4v6pj

Эта сеть будет использоваться всеми контейнерами для коммуникации друг с другом. Сетевая маска /20 даст нам возможность запускать в этой сети до 4 тысяч контейнеров. Ключ --attachable дает нам возможность запускать в этой сети не управляемые Docker Swarm (обычные) контейнеры, запускаемые при помощи команды docker run.

Настройка хостов

Из-за того, что на каждом хосте будет запущено очень большое количество контейнеров (а это в свою очередь означает запуск очень большого количества системных процессов Docker), нам нужно немного настроить некоторые sysctl параметры, чтобы избежать нестабильности работы нашего сервиса.

ARP кеш

Самое первое сообщение, которое вы увидите в выводе dmesg, если оставить sysctl настройки по умолчанию - neighbour: arp_cache: neighbor table overflow!. Эта проблема вызвана слишком маленьким ARP кешом по умолчанию, который будет не успевать очищаться.

Каждый запускаемый вами контейнер подключается к трем сетям:

  • swarm-scale-test-net - общая сеть, которую мы настроили
  • ingress - сеть, откуда поступает трафик с опубликованного порта на сервис
  • docker_gwbridge - публичная интернет сеть

Т.к. в этих сетях для каждого контейнера будут назначены несколько IP-адресов, то суммарно получается порядка 5000 IP адресов для работы запускаемого нами сервиса.

ARP кешем управляют следующие настройки:

sysctl -w net.ipv4.neigh.default.gc_thresh1=8096
sysctl -w net.ipv4.neigh.default.gc_thresh2=12288
sysctl -w net.ipv4.neigh.default.gc_thresh3=16384
  • gc_thresh1 - минимальное количество данных, которое необходимо хранить в ARP кеше
  • gc_thresh2 - мягкое максимальное количество данных кеша
  • gc_thresh3 - жесткое максимальное количество данных

Вообще говоря, работа с ARP кешем документирована слабо, поэтому обычно эти значения подбирают, умножая предыдущие значения на 2, до тех пор, пока система не начнет чувствовать себя нормально.

Другие важные настройки

Также вам определенно понадобятся и другие sysctl настройки, которые касаются главным образом того, сколько соединений может быть открыто в данный момент времени, какие порты используются для соединений, как быстро перерабатываются соединения и т.д.

# Определить большее количество портов (диапазон) для подключения
net.ipv4.ip_local_port_range=1024 65000

# Быстрее переиспользовать закрытые сокеты
net.ipv4.tcp_tw_reuse=1
net.ipv4.tcp_fin_timeout=15

# Максимальное количество "накопившихся" сокетов. По умолчанию 128.
net.core.somaxconn=4096
net.core.netdev_max_backlog=4096

# 16MB на сокет - скорее всего не будет
# использоваться так много
net.core.rmem_max=16777216
net.core.wmem_max=16777216

# Общий сетевой тюнинг
net.ipv4.tcp_max_syn_backlog=20480
net.ipv4.tcp_max_tw_buckets=400000
net.ipv4.tcp_no_metrics_save=1
net.ipv4.tcp_rmem=4096 87380 16777216
net.ipv4.tcp_syn_retries=2
net.ipv4.tcp_synack_retries=2
net.ipv4.tcp_wmem=4096 65536 16777216

# Отслеживание соединений для предотвращения отбрасывания пакетов
# (обычно проблема на балансировщиках)
net.netfilter.nf_conntrack_max=262144
net.ipv4.netfilter.ip_conntrack_generic_timeout=120
net.netfilter.nf_conntrack_tcp_timeout_established=86400

# Настройки ARP кеша для высоко нагруженного docker swarm-а
net.ipv4.neigh.default.gc_thresh1=8096
net.ipv4.neigh.default.gc_thresh2=12288
net.ipv4.neigh.default.gc_thresh3=16384

Запуск реплицированного сервиса

После того, как был произведен тюнинг хостов, давайте попробуем запустить наш сервис на 1000 контейнеров в Docker Swarm кластере. В качестве сервиса будет использоваться очень маленький образ titpetric/sonyflake, предназначенный для генерации ID-шников и отдачи их по HTTP. Исходный код сервиса доступен на GitHub

docker service create \
--replicas 1000 \
--network swarm-scale-test-net \
--update-parallelism 5 \
--name sonyflake \
-p 80:80 \
titpetric/sonyflake

Пара минут и все контейнеры запустились!

docker service ls

ID NAME MODE REPLICAS IMAGE
uxtfzjy7q203 sonyflake replicated 1000/1000 titpetric/sonyflake:latest

Небольшой тест

В качестве теста будет использоваться Wrk — современный инструмент тестирования производительности HTTP, способный генерировать значительную нагрузку при работе даже с одним многоядерным процессором. Он сочетает в себе многопоточный дизайн с масштабируемыми системами уведомления о событиях, такими как epoll и kqueue.

docker run \
--net=swarm-scale-test-net \
--rm williamyeh/wrk \
-t 6 \
-c 30 \
http://sonyflake

Running 10s test @ http://sonyflake
6 threads and 30 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 1.69ms 1.87ms 45.42ms 91.81%
Req/Sec 2.55k 0.88k 8.27k 80.97%
152568 requests in 10.08s, 29.79MB read
Requests/sec: 15137.45
Transfer/sec: 2.96MB

Уменьшим количество запущенных контейнеров до 30.

docker service scale sonyflake=30

Проверить статус операции можно командой

docker ps -a --format ".Status"  | sort | uniq -c

470 Dead
181 Removal In Progress
8 Exited (2) About a minute ago
8 Up 56 minutes
3 Up 58 minutes
3 Up 59 minutes
8 Up About an hour

Как только количество контейнеров в статусе «Removal in Progress» станет равно нулю и docker service ls будет показывать 30/30, проведем тест еще раз

docker run \
--net=swarm-scale-test-net \
--rm williamyeh/wrk \
-t 6 \
-c 30 \
http://sonyflake

Running 10s test @ http://sonyflake
6 threads and 30 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 1.36ms 1.06ms 18.20ms 80.58%
Req/Sec 2.64k 1.26k 7.63k 83.67%
157952 requests in 10.09s, 30.84MB read
Requests/sec: 15651.65
Transfer/sec: 3.06MB

Результаты теста показывают примерно 30% увеличение времени ответа каждого отдельного контейнера при увеличении количества запускаемых контейнеров в 30 раз (с 30 до 1000), обуславливаемое контекстным переключением CPU каждого сервера кластера, затрачиваемое на обслуживание работы каждого отдельного docker процесса.

Выводы

Мы успешно протестировали возможности Docker Swarm кластера и запустили в нем 1000 контейнеров. Создать и запустить для этого теста виртуальную инфраструктуру в Облаке КРОК заняло у меня считанные минуты, а производительности даже таких небольших виртуальных серверов с лихвой хватило на обслуживание работы такого количества контейнеров. Само собой, чтобы запустить еще большее количество микросервисных контейнеров на одном виртуальном сервере, нужно еще чуть-чуть увеличить параметры sysctl.

Успехов в работе!