Docker: Оточення для тестування



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

Постановка завдання

Наше додаток збирає, аналізує і зберігає всі можливі типи лог-файлів. Основне завдання оточення — це провести первинне тестування сервісу під навантаженням.
Отже, що ми маємо:
  • Наш сервіс, написаний на Go і має клієнт-серверну архітектуру.
  • Сервіс вміє паралельно записувати дані у сховища різного типу. Цей момент дуже важливий при побудові оточення для тестування.
  • Розробникам потрібна можливість швидко та безболісно відтворювати знайдені несправності на тестовому оточенні.
  • Ми повинні протестувати мережеве взаємодія компонентів в розподіленому середовищі на декількох мережевих вузлах. Для цього потрібно проаналізувати хід трафіку між клієнтами і серверами.
  • Нам необхідно проконтролювати споживання ресурсів і упевнитися в стабільній роботі демона при високому навантаженні.
  • Ну і, звичайно, нам хочеться подивитися на всі можливі метрики в реальному часі та за результатами тестування.
У підсумку ми вирішили побудувати оточення для тестування на базі Docker і супутніх технологій. Це дозволило нам реалізувати всі наші запити і ефективно використовувати наявні апаратні ресурси без необхідності купувати окремий сервер для кожного окремого компонента. При цьому, апаратними ресурсами можуть бути окремий сервер, набір серверів або навіть ноутбук розробника.

Архітектура оточення для тестування

Для початку розглянемо основні компоненти архітектури:
  • Довільну кількість серверних примірників нашої програми.
  • Довільну кількість агентів.
  • Окремі оточення з сховищами даних, такими як ElasticSearch, MySQL або PostgreSQL.
  • Генератор навантаження (ми реалізували простий стрес-генератор, але можна використовувати будь-який інший, наприклад, Яндекс.Танк або Apache Benchmark).
Оточення для тестування повинно легко підніматися й обслуговуватися.

Розподілену мережеву середу ми побудували за допомогою Docker контейнерів, ізолюючих в собі і зовнішні сервіси, і docker-machine, яка дозволяє організувати ізольований простір для тестування. В результаті архітектура тестового оточення виглядає так:



Для візуалізації оточення ми використовуємо Weave Scope, так як це дуже зручний і наочний сервіс для моніторингу Docker контейнерів.



З даним підходом зручно тестувати взаємодія SOA компонентів, наприклад, невеликі клієнт-серверні додатки, подібні нашим.

Реалізація базового оточення

Далі детально розглянемо кожен крок створення тестового середовища на базі Docker контейнерів, з використанням docker-compose і docker-machine.
Почнемо з docker-machine, яка дозволить нам безболісно виділити тестове віртуальне оточення. При цьому нам буде дуже зручно працювати з цим оточенням безпосередньо з хост-системи.
Отже, створюємо тестову машину:

$ docker-machine create -d virtualbox testenv
Creating VM VirtualBox...
Creating SSH key...
Starting VM VirtualBox...
Starting VM...
To see how to connect Docker to this machine, run: docker-machine env testenv

Ця команда створює VM VirtualBox з встановленими всередині неї CoreOS і Docker, готовим до роботи (Якщо ви використовуєте Windows або MacOS, то рекомендується встановити Docker Toolbox, в якому вже все закладено. А якщо ви використовуєте Linux, то необхідно поставити docker, docker-machine, docker-compose і VirtualBox самостійно). Ми рекомендуємо ознайомитися з усіма можливостями docker-machine, це досить потужний інструмент для управління окружениями.

Як видно з висновку цієї команди, docker-machine створює все необхідне для роботи з віртуальною машиною. Після створення, віртуальна машина запущена і готова до роботи. Давайте перевіримо:

$ docker-machine ls
NAME ACTIVE DRIVER STATE URL SWARM
testenv virtualbox Running tcp://192.168.99.101:2376

Чудово, віртуальна машина запущена. Треба активувати доступ до неї в нашій поточній сесії. Повернемося на попередній крок і уважно подивимося на останній рядок:

To see how to connect Docker to this machine, run: docker-machine env testenv

Це autosetup для нашої сесії. Виконавши цю команду ми побачимо наступне:

$ docker-machine env testenv
export DOCKER_TLS_VERIFY="1"
export DOCKER_HOST="tcp://192.168.99.101:2376"
export DOCKER_CERT_PATH="/Users/logpacker/.docker/machine/machines/testenv"
export DOCKER_MACHINE_NAME="testenv"
# Run this command to configure your shell:
# eval "$(docker-machine env testenv)"

Це просто набір змінних оточення, який повідомить вашого локального docker-клієнту де шукати сервер. В останньому рядку розташована підказка. Виконаємо цю команду і подивимося на вивід команди
ls
:

$ eval "$(docker-machine env testenv)"
$ docker-machine ls
NAME ACTIVE DRIVER STATE URL SWARM
testenv * virtualbox Running tcp://192.168.99.101:2376

У стовпці
АКТИВНИЙ
наша активна машина позначена зірочкою. Зверніть увагу, машина активна в рамках тільки поточної сесії. Ми можемо відкрити ще одне вікно терміналу і активувати там іншу машину. Це може бути зручно для тестування, наприклад, оркестрации за допомогою Swarm. Але це тема для окремої статті :).
Далі перевіримо наш docker-сервер:

$ docker info
docker version
Client:
Версія: 1.8.0
API версія: 1.20
Go version: go1.4.2
Git commit: 0d03096
Built: Tue Aug 11 17:17:40 UTC 2015
OS/Arch: darwin/amd64

Server:
Версія: 1.9.1
API версія: 1.21
Go version: go1.4.3
Git commit: a34a1d5
Built: Fri Nov 20 17:56:04 UTC 2015
OS/Arch: linux/amd64

Акцентуємо увагу на OS/Arch, там завжди буде linux/amd64, так як docker-сервер працює в VM, потрібно не забувати про це. Трохи відвернемося і заглянемо всередину VM:

$ docker-machine ssh testenv
## .
## ## ## ==
## ## ## ## ## ===
/"""""""""""""""""\___/ ===
~~~ {~~ ~~~~ ~~~ ~~~~ ~~~ ~ / ===- ~~~
\ ______ o __/
\ \ __/
\____\_______/
_ _ ____ _ _
| |__ ___ ___ | |_|___ \ __| | ___ ___| | _____ _ __
| '_ \ / _ \ / _ \| __| __) / _` |/ _ \ / __| |/ / _ \ '__|
| |_) | (_) | (_) | |_ / __/ (_| | (_) | (__| < __/ |
|_.__/ \___/ \___/ \__|_____\__,_|\___/ \___|_|\_\___|_|
Boot2Docker версія 1.9.1, build master : cef800b - Fri Nov 20 19:33:59 UTC 2015
Docker версія 1.9.1, build a34a1d5
docker@testenv:~$

Так, це boot2docker, але цікаво не це. Подивимося на змонтовані розділи:

docker@testenv:~$ mount
tmpfs on / type tmpfs (rw,relatime,size=918088k)
proc on /proc type proc (rw,relatime)
sysfs on /sys type sysfs (rw,relatime)
devpts on /dev/pts type devpts (rw,relatime,mode=600,ptmxmode=000)
tmpfs on /dev/shm type tmpfs (rw,relatime)
fusectl on /sys/fs/fuse/connections type fusectl (rw,relatime)
/dev/sda1 on /mnt/sda1 type ext4 (rw,relatime,data=ordered)
[... cgroup skipped ...]
none on /Users type vboxsf (rw,nodev,relatime)
/dev/sda1 on /mnt/sda1/var/lib/docker/aufs type ext4 (rw,relatime,data=ordered)
docker@testenv:~$ ls /Users/
Shared/ logpacker/
docker@testenv:~$

В даному випадку ми використовуємо MacOS і, відповідно, всередину машини змонтована директорія /Users (аналог /home в linux). Це дозволяє нам прозоро працювати з файлами на host-системі в рамках docker, тобто спокійно підключати і відключати volumes, не піклуючись про прошарку у вигляді VM. Це дійсно дуже зручно. По ідеї, нам можна забути про цю VM, вона потрібна тільки для того, щоб docker працював в «рідній» середовищі. При цьому використання docker-клієнта буде абсолютно прозорим.
Отже, базове оточення побудовано, далі будемо запускати Docker контейнери.

Налагодження та запуск контейнерів

Наше додаток вміє працювати за принципом кластера, тобто забезпечує відмовостійкість всієї системи в разі зміни кількості сайтів. Завдяки внутрішньому міжсервісного API додавання або видалення нового вузла в кластер проходить безболісно і не потребує перевантаження інших вузлів, і цю відмітну особливість нашого додатка нам теж потрібно врахувати при побудові оточення.
В принципі, все добре укладається в ідеологію Docker: «один процес — один контейнер». Тому ми не будемо відходити від канонів і вчинимо так само. На старті запустимо наступну конфігурацію:
  • Три контейнери з серверною частиною програми.
  • Три контейнери з клієнтською частиною програми.
  • Генератор навантаження для кожного агента. Наприклад, Ngnix, який буде генерувати логи, а ми його будемо стимулювати Яндекс.Танком або Apache Benchmark.
  • І в ще одному контейнері ми відійдемо від ідеології. Наш сервіс вміє працювати в так званому «dual mode», тобто клієнт і сервер знаходяться на одному і тому ж хості, більш того, це всього один примірник додатка, що працює відразу і як клієнт, і як сервер. Його ми запустимо в контейнері під контролем supervisord, і в цьому ж контейнері буде запущений наш власний невеликий генератор навантаження в якості основного процесу.
Отже, у нас є зібраний бінарники нашої програми — це один файл, так, спасибі Golang :), з яким ми зберемо універсальний контейнер для запуску сервісу, в рамках тестового оточення. Різниця буде в переданих ключах (запускаємо сервер або агент), ними ми і будемо керувати при запуску контейнера. Невеликі нюанси є в останньому пункті, при запуску сервісу в «dual mode», але про це трохи пізніше.
Отже, готуємо
docker-compose.yml
. Це файл з директивами для docker-compose, який дозволить нам підняти тестове оточення однією командою:
docker-compose.yml
# external services
elastic:
image: elasticsearch
ngx_1:
image: nginx
volumes:
- /var/log/nginx
ngx_2:
image: nginx
volumes:
- /var/log/nginx
ngx_3:
image: nginx
volumes:
- /var/log/nginx

# lp servers
lp_server_1:
image: logpacker_service
command: bash -c "cd /opt/logpacker && ./logpacker -s -v -devmode -p=0.0.0.0:9995"
links:
- elastic
expose:
- "9995"
- "9998"
- "9999"
lp_server_2:
image: logpacker_service
command: bash -c "cd /opt/logpacker && ./logpacker -s -v -devmode -p=0.0.0.0:9995"
links:
- elastic
- lp_server_1
expose:
- "9995"
- "9998"
- "9999"
lp_server_3:
image: logpacker_service
command: bash -c "cd /opt/logpacker && ./logpacker -s -v -devmode -p=0.0.0.0:9995"
links:
- elastic
- lp_server_1
- lp_server_2
expose:
- "9995"
- "9998"
- "9999"

# lp agents
lp_agent_1:
image: logpacker_service
command: bash -c "cd /opt/logpacker && ./logpacker -a -v -devmode -p=0.0.0.0:9995"
volumes_from:
- ngx_1
links:
- lp_server_1
lp_agent_2:
image: logpacker_service
command: bash -c "cd /opt/logpacker && ./logpacker -a -v -devmode -p=0.0.0.0:9995"
volumes_from:
- ngx_2
links:
- lp_server_1
lp_agent_3:
image: logpacker_service
command: bash -c "cd /opt/logpacker && ./logpacker -a -v -devmode -p=0.0.0.0:9995"
volumes_from:
- ngx_3
links:
- lp_server_1


У цьому файлі все стандартно. Першим запускаємо elasticsearch, як основне сховище, потім три примірники з nginx, які будуть виступати постачальниками навантаження. Далі запускаємо наші сервера-додатка. Зверніть увагу, всі наступні контейнери линкуются з попередніми. В рамках нашої docker-мережі, це дозволить звертатися до контейнерів по імені. Трохи нижче, коли ми будемо розбирати запуск нашого сервісу в «dual mode», ми ще повернемося до цього моменту і розглянемо його трохи докладніше. Також з першим контейнером, в якому знаходиться примірник сервер-додатки, залинкованы агенти. Це означає, що всі три агента будуть пересилати логи саме цього сервера.

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

І ще один момент: зверніть увагу на логіку монтування volumes. На контейнерах з nginx ми вказуємо іменований volume, який буде доступний в docker-мережі, а на контейнерах з агентами ми просто підключаємо його, вказавши ім'я сервісу. Таким чином, у нас вийде shared volume між споживачами і постачальниками навантаження.

Отже, запускаємо наше оточення:

$ docker-compose up -d

Перевіряємо, що все запустилося нормально:

$ docker-compose ps
Command Name State Ports
--------------------------------------------------------------------------------------------
assets_lp_agent_1_1 bash -c cd /opt/logpacker Up ... 
assets_lp_agent_2_1 bash -c cd /opt/logpacker Up ... 
assets_lp_agent_3_1 bash -c cd /opt/logpacker Up ... 
assets_lp_server_1_1 bash -c cd /opt/logpacker ... Up 9995/tcp, 9998/tcp, 9999/tcp
assets_lp_server_2_1 bash -c cd /opt/logpacker ... Up 9995/tcp, 9998/tcp, 9999/tcp
assets_lp_server_3_1 bash -c cd /opt/logpacker ... Up 9995/tcp, 9998/tcp, 9999/tcp
assets_ngx_1_1 nginx -g daemon off; Up 443/tcp, 80/tcp
assets_ngx_2_1 nginx -g daemon off; Up 443/tcp, 80/tcp
assets_ngx_3_1 nginx -g daemon off; Up 443/tcp, 80/tcp
elastic /docker-entrypoint.sh elas ... Up 9200/tcp, 9300/tcp

Відмінно, оточення піднялося, працює і всі порти проброшены. В теорії ми можемо стартувати тестування, але нам потрібно доробити деякі моменти.

Присвоєння імен контейнерів

Повернемося до контейнера, в якому ми хотіли запустити наш додаток в «dual mode». Основним процесом в цьому контейнері буде виступати генератор навантаження (найпростіший shell-сценарій). Він генерує текстові рядки і складає їх у текстові «лог»-файли, які, в свою чергу, будуть навантаженням для нашого застосування. Спочатку потрібно зібрати контейнер з нашим додатком, що запускається під
supervisord
. Візьмемо останню версію
supervisord
, так як нам потрібна можливість передачі змінних оточення в конфігураційний файл. Нам підійде
supervisord
версії 3.2.0, проте в Ubuntu 14.04 LTS, яку ми взяли за базовий образ, версія
supervisord
досить стара (3.0b2). Встановимо свіжу версію
supervisord
через
pip
. Підсумковий Dockerfile вийшов таким:

Dockerfile
FROM ubuntu:14.04

# Setup locale environment variables
RUN locale-gen en_US.UTF-8
ENV LANG en_US.UTF-8
ENV LANGUAGE en_US:en
ENV LC_ALL en_US.UTF-8

# Ignore interactive
ENV DEBIAN_FRONTEND=noninteractive

RUN apt-get update && \
apt-get install -y wget unzip curl python-pip

# Install supervisor via pip for latest version
RUN pip install supervisor
RUN mkdir -p /opt/logpacker
ADD final/logpacker /opt/logpacker/logpacker
ADD supervisord-logpacker-server.ini /etc/supervisor/conf.d/logpacker.conf
ADD supervisor.conf /etc/supervisor/supervisor.conf
# Load generator
ADD random.sh /opt/random.sh 
# Start script
ADD lp_service_start.sh /opt/lp_service_start.sh



Генератор навантаження вкрай простий:
#!/bin/bash

# generate random lines
OUTPUT_FILE="test.log"

while true
do
_RND_LENGTH=`awk -v min=1 -v max=100 'BEGIN{srand(); print (int) (min+rand()*(max-min+1))}"
_RND=$(( ( RANDOM % 100 ) + 1 ))
_A="[$RANDOM-$_RND] $(dd if=/dev/urandom bs=$_RND_LENGTH count=1 2>/dev/null | base64 | tr = d)";
echo $_A;
echo $_A >> /tmp/logpacker/lptest.$_RND.$OUTPUT_FILE;
done

Стартовий скрипт теж не складний:

#!/bin/bash

# run daemon
supervisord -c /etc/supervisor/supervisor.conf

# launch randomizer
/opt/random.sh

Вся хитрість буде полягати в конфігураційному файлі
supervisord
та запуску Docker-контейнера.
Розглянемо конфігураційний файл:

[program:logpacker_daemon]
command=/opt/logpacker/logpacker %(ENV_LOGPACKER_OPTS)s
directory=/opt/logpacker/
autostart=true
autorestart=true
startretries=10
stderr_logfile=/var/log/logpacker.stderr.log
stdout_logfile=/var/log/logpacker.stdout.log

Зверніть увагу на
%(ENV_LOGPACKER_OPTS)s
. Supervisord може виконувати підстановки в конфігураційний файл із змінних оточення. Змінна записується як
%(ENV_VAR_NAME)s
та її значення підставляється в конфігураційний файл при старті демона.
Запускаємо контейнер, виконавши наступну команду:

$ docker run -it -d --name=dualmode --link=elastic -e 'LOGPACKER_OPTS=-s -a -v -devmode' logpacker_dualmode /opt/random.sh

За допомогою ключа
e
є можливість встановити необхідну змінну оточення, при цьому вона буде встановлена глобально всередині контейнера. І саме її ми підставляємо в конфігураційний файл
supervisord
. Таким чином, ми можемо керувати ключами запуску для нашого демона і запускати його в потрібному нам режимі.
Ми отримали універсальний образ, хоча трохи не відповідає ідеології. Загляньмо всередину:
Environment
$ docker exec -it dualmode bash
$ env
HOSTNAME=6b2a2ae3ed83
ELASTIC_NAME=/suspicious_dubinsky/elastic
TERM=xterm
ELASTIC_ENV_CA_CERTIFICATES_JAVA_VERSION=20140324
LOGPACKER_OPTS=-s -a -v -devmode
ELASTIC_ENV_JAVA_HOME=/usr/lib/jvm/java-8-openjdk-amd64/jre
ELASTIC_ENV_JAVA_VERSION=8u66
ELASTIC_ENV_ELASTICSEARCH_REPO_BASE=http://packages.elasticsearch.org/elasticsearch/1.7/debian
ELASTIC_PORT_9200_TCP=tcp://172.17.0.2:9200
ELASTIC_ENV_ELASTICSEARCH_VERSION=1.7.4
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
ELASTIC_PORT_9300_TCP_ADDR=172.17.0.2
ELASTIC_ENV_ELASTICSEARCH_MAJOR=1.7
ELASTIC_PORT_9300_TCP=tcp://172.17.0.2:9300
PWD=/
ELASTIC_PORT_9200_TCP_ADDR=172.17.0.2
ELASTIC_PORT_9200_TCP_PROTO=tcp
ELASTIC_PORT_9300_TCP_PORT=9300
SHLVL=1
HOME=/root
ELASTIC_ENV_JAVA_DEBIAN_VERSION=8u66-b17-1~bpo8+1
ELASTIC_PORT_9300_TCP_PROTO=tcp
ELASTIC_PORT=tcp://172.17.0.2:9200
LESSOPEN=| /usr/bin/lesspipe %s
ELASTIC_ENV_LANG=C. UTF-8
LESSCLOSE=/usr/bin/lesspipe %s %s
ELASTIC_PORT_9200_TCP_PORT=9200
_=/usr/bin/env



Крім нашої змінної, яку ми явно вказали при старті контейнера, ми бачимо ще й всі змінні, які відносяться до залинкованному контейнера, а саме: IP-адресу, всі відкриті порти і всі змінні, які були встановлені при складанні образу elasticsearch за допомогою директиви ENV. Всі змінні мають префікс з ім'ям експортує контейнера і назва, що вказує на їх суть. Наприклад,
ELASTIC_PORT_9300_TCP_ADDR
означає, що зберігається у змінній значення, що вказує на контейнер з ім'ям elastic і його ip-адреса, на якому відкритий порт 9300. Якщо піднімати окремий discovery-сервіс для поставлених завдань не резонно, то це відмінний спосіб отримати IP-адресу і дані залинкованных контейнерів. При цьому залишається можливість використовувати їх в своїх додатках, які запущені в Docker контейнерах.

Управління контейнерами і система моніторингу

Отже, ми побудували оточення для тестування відповідає всім нашим споконвічним запитам. Залишилася пара нюансів. По-перше, встановимо Weave Scope (скріншоти якого були на початку статті). За допомогою Weave Scope можна візуалізувати середовище, в якому ми працюємо. Крім відображення зв'язків та інформації про контейнери, ми можемо виконати
attach
до будь-якого контейнера або запустити повноцінний термінал з
s
прямо в браузері. Це незамінні функції при налагодження і тестування. Отже, з хост-машини виконуємо наступні дії, в рамках нашої активної сесії:


$ wget -O scope https://github.com/weaveworks/scope/releases/download/latest_release/scope
$ chmod +x scope
$ scope launch

Після виконання цих команд, перейшовши за адресою VM_IP:4040 ми потрапляємо в інтерфейс управління контейнерами, представлений на картинці нижче:



Відмінно, майже все готово. Для повного щастя нам не вистачає системи моніторингу. Скористаємося cAdvisor від Google:

$ docker run --volume=/:/rootfs:ro --volume=/var/run:/var/run:rw --volume=/sys:/sys:ro --volume=/var/lib/docker/:/var/lib/docker:ro --publish=8080:8080 --detach=true --name=cadvisor google/cadvisor:latest

Тепер за адресою VM_IP:8080 у нас є система моніторингу ресурсів у реальному часі. Ми можемо відслідковувати та аналізувати основні метрики нашого оточення, такі як:

  • використання системних ресурсів;
  • завантаження мережі;
  • список процесів;
  • інша корисна інформація.
На скріншоті нижче представлений cAdvisor інтерфейс:



Висновок

Використовуючи Docker контейнери, ми побудували повноцінне тестове оточення з функціями автоматичного розгортання і мережевої взаємодії всіх вузлів, і що особливо важливо, з гнучкою настройкою кожного елемента і системи в цілому. Реалізовані всі основні вимоги, а саме:
  • Повноцінна емуляція мережі для тестування мережного взаємодії.
  • Додавання і видалення вузлів з додатком здійснюється за рахунок змін у docker-compose.yml і застосовується однією командою.
  • Всі вузли можуть повноцінно отримувати інформацію про мережевому оточенні.
  • Додавання і видалення сховищ даних виконується однією командою.
  • Управління і моніторинг системи доступні через браузер. Це реалізовано за допомогою інструментів, окремо запущених у контейнерах поруч з нашим додатком, що дозволяє ізолювати їх від host-системи.

Посилання на всі інструменти, згадані в статті:



Джерело: Хабрахабр

0 коментарів

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