Использование контейнеров Docker для тестового ландшафта — это без сомнений быстро, просто и удобно. Но если развернуть с помощью контейнеров более нагруженную систему? Как в таком случае будет все работать? Действительно ли можно использовать Docker в продакшене?

Мы уже разбирали кейс при реализации которого было бы удобно воспользоваться контейнерами. Это система видеонаблюдения за несколькими домами одной управляющей компании. Здесь мы планировали, что при подключении дома для него создается свой контейнер, а при отключении — контейнер удаляется и не занимает серверные ресурсы.

shema_RTSP-to-WebRTC _WCS_Docker_network_WebRTC_browser_CDN_streaming_publish

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

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

Мы решили проверить и попробовать найти пути решения этих проблем.

Настройка сети Docker

Перед тем, как создавать контейнеры нужно настроить Docker сеть.

Для статьи мы использовали «белые» публичные IP адреса. В случае, если у вас контейнеры все же будут за NATом, то работа несколько усложнится из-за настройки правил проброса портов для контейнеров на вашем пограничном шлюзе.

При использовании драйвера ipvlan каждый контейнер — это полноценный участник сети, поэтому можно создавать правила проброса портов сквозь NAT, точно так же, как если бы WCS был развернут на реальном железе. Поэтому обратите внимание — никакого NATa для контейнеров за основным NATом не создается.

У своего провайдера мы запросили сеть из 8 белых Elastic IP

1 адрес - адрес сети: 147.75.76.160/29
2 адрес - адрес основного шлюза: 147.75.76.161
3 адрес - адрес хоста: 147.75.76.162
4 адрес диапазона для Docker 147.75.76.163/30
5 адрес первого контейнера 147.75.76.164
6 адрес второго контейнера 147.75.76.165
7 и 8 - резервные адреса.

Создаем Docker сеть с именем new-testnet на основе драйвера ipvlan

docker network create -d ipvlan -o parent=enp0s3 \
--subnet 147.75.76.160/29 \
--gateway 147.75.76.161 \
--ip-range 147.75.76.163/30 \
new-testnet

где:

  • ipvlan — тип сетевого драйвера;
  • parent=enp0s3 — физический сетевой интерфейс (enp0s3), через который будет идти трафик контейнеров;
  • —subnet — подсеть;
  • —gateway — шлюз по умолчанию для подсети;
  • —ip-range — диапазон адресов в подсети, которые Docker может присваивать контейнерам.

 

Настройка WCS

Нагрузку на контейнеры будем подавать при помощи нагрузочного теста WebRTC. Подготовим настройки для теста:

На хосте в каталоге

/opt/wcs/conf/

Создаем файлы flashphoner.properties и wcs-core.properties такого содержания:

Файл flashphoner.properties:

#server ip
ip =
ip_local =

#webrtc ports range
media_port_from =31001
media_port_to =40000

#codecs
codecs =opus,alaw,ulaw,g729,speex16,g722,mpeg4-generic,telephone-event,h264,vp8,flv,mpv
codecs_exclude_sip =mpeg4-generic,flv,mpv
codecs_exclude_streaming =flv,telephone-event
codecs_exclude_sip_rtmp =opus,g729,g722,mpeg4-generic,vp8,mpv

#websocket ports
ws.port =8080
wss.port =8443

wcs_activity_timer_timeout=86400000

wcs_agent_port_from=44001
wcs_agent_port_to=55000

global_bandwidth_check_enabled=true
zgc_log_parser_enable=true
zgc_log_time_format=yyyy-MM-dd'T'HH:mm:ss.SSSZ

Этот файл заменит оригинальный flashphoner.properties при запуске контейнера. Переменные ip и ip_local будут заполнены значениями, указанными при создании контейнера в переменных EXTERNAL_IP и LOCAL_IP соответственно.

Из специальных настроек здесь мы расширяем диапазон портов:

media_port_from=31001
media_port_to=40000
wcs_agent_port_from=44001
wcs_agent_port_to=55000

увеличиваем время на прохождение теста:

wcs_activity_timer_timeout=86400000

и включаем вывод на страницу статистики данных о скорости сетевого адаптера и работе ZGC:

zgc_log_parser_enable=true
zgc_log_time_format=yyyy-MM-dd'T'HH:mm:ss.SSSZ

В файле wcs-core.properties настраиваем использование ZGC и указываем размер хипа:

### SERVER OPTIONS ###
# Set this property to false to disable session debug
-DsessionDebugEnabled=false
# Disable SSLv3
-Djdk.tls.client.protocols="TLSv1,TLSv1.1,TLSv1.2"


### JVM OPTIONS ###
-Xmx16g
-Xms16g
#-Xcheck:jni

# Can be a better GC setting to avoid long pauses

# Uncomment to fix multicast crosstalk problem when streams share multicast port
-Djava.net.preferIPv4Stack=true

# Default monitoring port is 50999. Make sure the port is closed on firewall. Use ssh tunel for the monitoring.
-Dcom.sun.management.jmxremote=true
-Dcom.sun.management.jmxremote.local.only=false
-Dcom.sun.management.jmxremote.ssl=false
-Dcom.sun.management.jmxremote.authenticate=false
-Dcom.sun.management.jmxremote.port=50999
-Dcom.sun.management.jmxremote.host=localhost
-Djava.rmi.server.hostname=localhost

#-XX:ErrorFile=/usr/local/FlashphonerWebCallServer/logs/error%p.log
-Xlog:gc*:/usr/local/FlashphonerWebCallServer/logs/gc-core-:time

# ZGC
-XX:+UnlockExperimentalVMOptions -XX:+UseZGC

# Use System.gc() concurrently in CMS
-XX:+ExplicitGCInvokesConcurrent
# Disable System.gc() for RMI, for 10000 hours
-Dsun.rmi.dgc.client.gcInterval=36000000000
-Dsun.rmi.dgc.server.gcInterval=36000000000

Этот файл так же заменит собой файл wcs-core.properties, который находится в контейнере по умолчанию.

Обычно мы рекомендуем выставить размер хипа равным 50% объема доступной оперативной памяти. Но в этом варианте запуск двух контейнеров займет всю оперативную память сервера и может привести к нестабильной работе. Поэтому поступим иначе. Выделим контейнерам по 25% доступной оперативной памяти:

### JVM OPTIONS ###
-Xmx16g
-Xms16g

Теперь, когда настройки готовы, можно запускать контейнеры. (Примеры файлов wcs-core.properties и flashphoner.properties можно скачать в разделе «Полезные файлы»).

Запуск контейнеров

Теперь возвращаемся к решению первой задачи — Проблемы распределения ресурсов между контейнерами и смещения таймингов.

Действительно, при разделении ресурсов CPU между контейнерами через cgroups, софт внутри контейнера может выбирать квоту, установленную планировщиком, в результате чего возникает Jitter (нежелательные отклонения передаваемого сигнала), который негативно влияет на воспроизведение RTP потока.

Проблема с разделением CPU и появлением Jitter существует не только для Docker, но и для «классических» виртуалок на гипервизорах, и для железных машин поэтому назвать ее специфической именно для Docker нельзя. В случае WebRTC Jitter гасится адаптивным jitter-буфером, который работает на клиенте достаточно в широком диапазоне (до 1000 мс).

Проблема с таймингами в нашем случае не актуальна, т.к. RTP поток у нас не привязан к времени сервера. RTP пакеты не обязательно идут четко по времени энкодера, т.к. сеть не бывает идеальной. Это все нормально отрабатывается буферами в WebRTC. Опять же, не важно в Docker или нет.

Для того, чтобы минимизировать борьбу за ресурсы между контейнерами мы принудительно укажем какие процессорные ядра выделены контейнеру для работы. В этом случае контейнеры не конфликтуют, нет квот на выделение процессорного времени. Это можно сделать при создании контейнера с помощью ключа:

--cpuset-cpus=

В качестве значения можно указать список ядер, разделенный запятыми, или диапазон ядер через дефис. Первое ядро обознается как «0».

Запускаем первый контейнер:

docker run --cpuset-cpus=0-15 \
-v /opt/wcs/conf:/conf \
-e PASSWORD=123Qwe \
-e LICENSE=xxxx-xxxx-xxxx-xxxx-xxxx \
-e LOCAL_IP=147.75.76.164 \
-e EXTERNAL_IP=147.75.76.164 \
--net new-testnet \
--ip 147.75.76.164 \
--name wcs-docker-test-1 \
-d flashphoner/webcallserver:latest

ключи здесь:

  • —cpuset-cpus=0-15 — указываем, что для работы контейнер должен использовать ядра хоста с 0 по 15;
  • -v /opt/wcs/conf:/conf — монтируем директорию с файлами настройки к контейнеру;
  • PASSWORD — пароль на доступ внутрь контейнера по SSH. Если эта переменная не определена, попасть внутрь контейнера по SSH не удастся;
  • LICENSE — номер лицензии WCS. Если эта переменная не определена, лицензия может быть активирована через веб-интерфейс;
  • LOCAL_IP — IP адрес контейнера в сети докера, который будет записан в параметр ip_local в файле настроек flashphoner.properties;
  • EXTERNAL_IP — IP адрес внешнего сетевого интерфейса. Записывается в параметр ip в файле настроек flashphoner.properties;
  • в ключе —net указывается сеть, в которой будет работать запускаемый контейнер. Запускаем контейнер в сети testnet;
  • —ip 147.75.76.164 — адрес контейнера в сети докера;
  • —name wcs-docker-test-1 — имя контейнера;
  • -d flashphoner/webcallserver:latest — образ на основе которого будет развернут контейнер

 

Практически аналогичной командой запускаем второй контейнер:

docker run --cpuset-cpus=15-31 \
-v /opt/wcs/conf:/conf \
-e PASSWORD=123Qwe \
-e LICENSE=xxxx-xxxx-xxxx-xxxx-xxxx \
-e LOCAL_IP=147.75.76.165 \
-e EXTERNAL_IP=147.75.76.165 \
--net new-testnet \
--ip 147.75.76.165 \
--name wcs-docker-test-2 \
-d flashphoner/webcallserver:latest

Здесь мы указываем другой диапазон процессорных ядер и другой IP адрес для контейнера. Также можно указать другой пароль для ssh контейнера, но не принципиально.

Запуск тестирования и оценка результатов

В веб-интерфейсе первого контейнера запускаем консоль для проведения WebRTC теста с захватом потоков http://147.75.76.164:9091/client2/examples/demo/streaming/console/console.html:

stress_test_console_WCS_Docker_network_WebRTC_browser_CDN_streaming_publish

В веб-интерфейсе второго контейнера запускаем пример «Two-way Streaming»:

two-way-streaming-docker2_WCS_Docker_network_WebRTC_browser_CDN_streaming_publish

Затем запускаем нагрузочный тест:

start_stress_test_console_WCS_Docker_network_WebRTC_browser_CDN_streaming_publish

Метрики работы контейнеров оцениваем при помощи графиков от системы мониторинга Prometheus + Grafana. Для получения данных о загрузке CPU мы установили Prometheus Node Exporter на хост. Информация о загруженности контейнеров и состоянии стримов собирается со страниц статистики WCS серверов в контейнерах:

http://147.75.76.164:8081/?action=stat
http://147.75.76.165:8081/?action=stat

Панель для Grafana можно скачать в разделе «Полезные файлы».

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

test_result_16-core_WCS_Docker_network_WebRTC_browser_CDN_streaming_publish

Как видно, нагрузка на контейнер, который был тестирующим и забирал потоки была несколько выше (единичные пики до 20-25 единиц) чем на тестируемый контейнер, который потоки отдавал. При этом нет деградировавших стримов и контрольный поток (скриншот ниже) воспроизводился с приемлемым качеством — без артефактов и приостановок звука, поэтому можно говорить, что контейнеры с нагрузкой справились.

control-stream_play_16-core_WCS_Docker_network_WebRTC_browser_CDN_streaming_publish

Если посмотреть вывод утилиты htop на хосте во время тестирования, то видно, что 32 ядра, выделенные контейнерам, работают, а оставшиеся ядра не задействованы:

htop_host_16-core_WCS_Docker_network_WebRTC_browser_CDN_streaming_publish

Теперь перезапустим контейнеры без настроек распределения по ядрам.

Первый контейнер:

docker run \
-v /opt/wcs/conf:/conf \
-e PASSWORD=123Qwe \
-e LICENSE=xxxx-xxxx-xxxx-xxxx-xxxx \
-e LOCAL_IP=147.75.76.164 \
-e EXTERNAL_IP=147.75.76.164 \
--net new-testnet \
--ip 147.75.76.164 \
--name wcs-docker-test-1 \
-d flashphoner/webcallserver:latest

Второй контейнер:

docker run \
-v /opt/wcs/conf:/conf \
-e PASSWORD=123Qwe \
-e LICENSE=xxxx-xxxx-xxxx-xxxx-xxxx \
-e LOCAL_IP=147.75.76.165 \
-e EXTERNAL_IP=147.75.76.165 \
--net new-testnet \
--ip 147.75.76.165 \
--name wcs-docker-test-2 \
-d flashphoner/webcallserver:latest

Снова запустим нагрузочный тест с теми же условиями и смотрим графики результата:

htop_host_all-core_WCS_Docker_network_WebRTC_browser_CDN_streaming_publish

В этом случае, контейнеры получились «мощнее» — ведь мы не ограничивали ядра принудительно и каждый контейнер мог использовать все 40 ядер хоста. Поэтому в тесте получилось захватить больше потоков, чем в прошлом, но ближе к окончанию теста деградировал 1% стримов. Поэтому можно сделать вывод, что при распределении по ядрам контейнеры работают стабильнее.

Итак, по результатам проведенных тестов мы убедились, что контейнеры можно настроить так, чтобы они был полноценными участниками как локальной, так и глобальной сети. И смогли настроить распределение ресурсов хоста между контейнерами, что бы они не мешали работе друг друга. Напоминаем, что количественные результаты ваших тестов могут отличаться, это связано с конкретной задачей, техническими характеристиками системы и ее окружением.

Хорошего стриминга!

Полезные файлы

flashphoner.properties

wcs-core.properties

Docker_WebRTC_test.json

Ссылки

WCS в Docker