Website development
Programming
Go
March 2014 13

Используем nginx, docker, skydns и skydock для обновления кода на лету (zero-downtime deployment)

From Sandbox

Инструменты, которые мы будем использовать


Docker


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

Собрав контейнер один раз, его можно многократно использовать.

Простой пример — это БД Redis. Если нам необходимо несколько серверов Redis на одном компьютере, при обычном подходе нам придется изменять конфигурационные файлы в /etc/redis и менять файлы в /etc/init.d. Можно написать bash скрипт, но это не делает процесс легче.

В случае Docker, мы можем использовать следующую команду:

docker run -d --name test-redis-server dockerfile/redis

Эта команда скачает контейнер Redis из главного репозитория (index.docker.io), запустит его в фоновом режиме и присвоит только что созданному контейнеру имя test-redis-server.

Этот контейнер можно запустить потом командой:

docker start test-redis-server

SkyDNS


Совсем недавно разработчики Docker ввели инструмент --link при запуске контейнера для того, чтобы можно было связать несколько контейнеров посредством переменных окружения ENV. Например для связывания контейнера веб приложения с контейнерами Redis, Postgresql, Elasticsearch и т.д.

Это удобно, но нам необходимо следить за правильностью ENV и менять код при изменении условий запуска контейнеров и параметра --link.

Это называется Service Discovery, для него существует множество решений (более подробно про существующие решения вы можете прочитать в статье Open-Source Service Discovery).

Одно из таких решений — это SkyDNS. Небольшой локальный DNS и DNS proxy сервер, написанный на языке Go (важно заметить, что Go крайне эффективен в плане потребления ресурсов, примерно до 6MB памяти при запуске. Я проводил лично для себя несколько синтетических тестов, максимальное потребление памяти при 50 одновременных простых запросах было около 20MB).

SkyDNS позволяет с помощью простого API (более подробно: SkyDNS) добавлять DNS записи, после чего делать обычные запросы, которые возвращают SRV, A или AAAA записи.

Если запись не будет найдена, то SkyDNS отправит запрос на публичные dns сервера google (8.8.8.8/8.8.4.4).

В SkyDNS используется собственная интересная схема для домена:

<uuid>.<host>.<region>.<version>.<service>.<environment>.skydns.local

Если environment = production, а service = redis, то можно сделать запрос к redis.production.skydns.local, который вернет одну или несколько записей (по умолчанию, он возвращает A запись).

Skydock


Skydock — это небольшая программа, которая объединяет Docker и SkyDNS.

Skydock использует схему домена SkyDNS следующим образом:

<name>.<container-name>.<environment>.skydns.local

сontainer-name — это название образа Docker без репозитория (например crosbymichael/redis => redis или test/cool-api => cool-api). name — это название контейнера, присвоенное ему с помощью параметра --name.

Skydock автоматически определяет запущенные контейнеры и добавляет их в SkyDNS.

Docker присваивает отдельный IP адрес для каждого контейнера, следовательно если мы запустим контейнер следующим образом:

docker run -d --name redis-test-app dockerfile/redis

Мы сможем сделать запрос (позже будет описано почему мы делаем запрос на 172.17.42.1) и получим в ответ A запись, то есть IP адрес контейнера, в котором запущен Redis.

dig @172.17.42.1 redis-test-app.redis.dev.skydns.local

dev.skydns.local.	27	IN	A	172.17.0.3

Порт Redis по умолчанию 6379, поэтому коде мы можем сделать нечто такое (пример на псевдоязыке):

redisConn = redis.Connect("redis-test-app.redis.dev.skydns.local:6379")

Установка


Мы не будем рассматривать установку Docker, она очень простая и детально описана тут: Start using Docker

Нам нужно запустить 2 контейнера — один с SkyDNS, другой с Skydock. В отличии от автора Skydock, я рекомендую запускать SkyDNS с дополнительным параметром -p 172.17.42.1:8080:8080 — это позволит вашим контейнерам использовать API SkyDNS напрямую для собственных нужд.

docker pull crosbymichael/skydns
docker run -d -p 172.17.42.1:53:53/udp -p 172.17.42.1:8080:8080 --name skydns crosbymichael/skydns -nameserver 8.8.8.8:53 -domain skydns.local

IP 172.17.42.1 — это мост docker0, используется Docker для конфигурирования собственной сети. Мы указали домен skydns.local, хотя можно сделать любой, на ваш выбор, для примера: docker, super.local. skydns.local по моему мнению удобнее и универсальнее, тем более используется по умолчанию в SkyDNS.

Далее запускаем Skydock:

docker pull crosbymichael/skydock
docker run -d -v /var/run/docker.sock:/docker.sock --name skydock -link skydns:skydns crosbymichael/skydock -ttl 30 -environment dev -s /docker.sock -domain skydns.local


Здесь нужно указать TTL (если он меньше — более динамические окружение, можно быстрее вносить больше изменений в архитектуру. Если больше — менее динамическое, более кешированное окружение) и environment — это может быть совершенно любая строка (dev, development, production, stage, qa).

Если все прошло успешно, то все необходимые компоненты запущены.

Единственное, что нужно сделать — это при запуске контейнера с вашим приложением, необходимо указать первичный DNS сервер:

docker run -d --dns 172.17.42.1 test/cool-api

Наш контейнер с cool-api будет доступен в локальном DNS по адресу: cool-api.dev.skydns.local — это выдаст список IP адресов всех контейнеров с названием cool-api. Это свойство мы и будем использовать для конфигурирования nginx.

Если при запуске контейнера указать --name api1, то он будет доступен по api1.cool-api.dev.skydns.local — именно только 1 контейнер с именем api1.

Внутри контейнера теперь можно указывать домен напрямую, так как он будет использовать локальный DNS: redis.dev.skydns.local — вернет А записи всех контейнеров с запущенным Redis. Естественно будет выбран только 1 адрес, к которому подключится клиент Redis.

nginx


Использую эту магию локального DNS, мы можем сделать например:

  • Балансировку нагрузки — веб сервера или подключений к базе данных
  • Простой роутинг запросов по критерию. Например создав образ test/cool-api-v1 — мы можем направлять запросы к API v1 на одни контейнеры (cool-api-v1.dev.skydns.local), а cool-api.dev.skydns.local использовать как последнюю версию. При этом с автоматической балансировкой
  • Обновление кода на лету — что нам и нужно

Добавим в nginx вот такой конфигурационный файл:

server {
        listen 80;
        server_name super-cool-domain.com;

        # говорим nginx использовать SkyDNS
        resolver 172.17.42.1 valid=5s;
        resolver_timeout 5s;

        # Это нам необходимо сделать для того, чтобы nginx использовал локальный DNS. По-другому nginx не понимает
        set $dns cool-api.production.skydns.local;
        
        location /api {
            proxy_pass http://$dns:8080;
        }

        # Чисто для примера
        location / {
            try_files $uri /index.html;
        }
    }

Мы просто указываем, чтобы nginx использовал локальный DNS сервер, далее указываем куда обращаться — переменная $dns и делаем старый добрый proxy_pass на тот порт, который использует ваше приложение.

Теперь, при запуске нового контейнера, Skydock добавит его в SkyDNS, nginx будет проксировать запросы к этому контейнеру. При остановке контейнера, Skydock удалит его адрес из SkyDNS.

Запустив таким образом 2-3 контейнера, мы можем балансировать нагрузку между ними.

Запустим несколько дополнительных контейнеров, nginx автоматически начнет проксировать запросы к ним. После этого, мы можем остановить работу старых — тем самым сделав обновление на лету.

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

# Скачиваем новую версию контейнера с обновленным кодом веб приложения
docker pull test/cool-api

# Сохраняем ID текущих контейнеров
OLDPORTS=( `docker ps | grep cool-api | awk '{print $1}'` )

# Запускаем новый контейнер
docker run -d test/cool-api

# Останавливаем старые
for i in ${OLDPORTS[@]}
do
	echo "removing old container $i"
	docker kill $i
done

Заключение


Автор Skydock хочет развить идею и сделать поддержку нескольких серверов в датацентре. На данный момент это не реализовано, хотя сам SkyDNS уже используется в production окружении на множестве серверов.

В статье так же не затронута тема репозиториев docker. Есть решение с открытым кодом, которое позволяет создавать приватное хранилище образов.

Решение почти не влияет на производительность, Docker и SkyDNS очень хорошо спроектированы и используют эффективный и быстрый язык Go.
На первый взгляд кажется, что это некоторое усложнение процесса, но в итоге получается очень гибкое решение, которое не нужно дополнительно конфигурировать после первоначальной настройки.
+35
22k 291
Comments 14