Пользователь
0,0
рейтинг
13 марта 2014 в 16:34

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

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


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.
На первый взгляд кажется, что это некоторое усложнение процесса, но в итоге получается очень гибкое решение, которое не нужно дополнительно конфигурировать после первоначальной настройки.
@8protons
карма
12,0
рейтинг 0,0
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Спецпроект

Самое читаемое Разработка

Комментарии (14)

  • +2
    Спасибо за статью, весьма интересная тема. Добавят к этому Skydock поддержку нескольких серверов — будет отличная штука. Именно таких вот утилит и не хватает докеру для нормального использования в продакшне. Сам я пока использую docker только для QA, но перспективы у него очень большие.
  • 0
    Красиво, спасибо.
  • +2
            # Это нам необходимо сделать для того, чтобы nginx использовал локальный DNS. По-другому nginx не понимает
            set $dns cool-api.production.skydns.local;
    
    Это мягко говоря неверная формулировка. Разница между именем сервера заданным константой и переменной описана в документации: в одном случае используется системный резолвер и имя резолвится на этапе чтения конфигурации, а во втором случае у нас такой возможности нет, ибо системный блокируется, что недопустимо в nginx в процессе обработки запроса, поэтому применяется своя встроенная асинхронная реализация резолвера.

    Чтобы nginx использовал ваш локальный DNS с именем заданным константой, достаточно было настроить системный резолвер на его использование.
  • 0
    А все не мог понять, почему и как Dock используется со www.skydns.ru/ ?!
    • 0
      Вы еще skydns.kz предложите.
      github.com/skynetservices/skydns
      • 0
        Да я и не предлагал, просто привык это название видеть у DNS-сервиса, который, до появления dns.yandex.ru, был чуть не единственным российским DNS-сервисов с фильтрацией сайтов.

        Чем он отличался и отлается от Я.DNS-а, так это наличием категорий сайтов, т.е. можно блокировать не все. Но при этом, конечно, он разное отдает для разных клиентов, отличая тех по IP, что, сами понимаете, создает некую лишнюю обвязку для работы на пользу клиентов с динамическими IP.

        К тому говорю, что DNS-ный SkyDNS не забыть, молодцы ребята, а что под таким именем существует еще и service discovery демон…
  • +4
    Это восхитительно. Я ко хорошо что я не курю.
  • +2
    Может чего-то недопонял, но похоже этот SkyDNS можно заменить на dnsmasq (к слову, у меня занимает 880кб памяти, раз уж упомянули).
    • +1
      Нельзя, как управлять кластером таких серверов? В данном случае SkyDNS связан с другими серверами по протоколу RAFT. Это как смесь Zoorkeper + DNS сервер
      • 0
        Я так и не смог найти и понять — как связывать через SkyDNS/SkyDock между собой разные хосты (в разных сетях)?

        Даже используя виртуальную общую сеть weave.

        Не подскажете, куда копать?
  • 0
    У меня примерно похожая схема, но я использую lxc контейнеры.
    В родительской оси запущен nginx и bind, а в гостевых разые сервисы типа mysql php redis tarantool 1c и прочие.
    Внутри гостевых контейнеров примонтированы диски, ссылающиеся на родительский диск.
    Таким образом я могу видеть файлы как из родительской системы так и из гостевой.
    • 0
      Насколько мне известно, Docker как раз и работает поверх данных контейнеров.
      • +1
        с 0.9.0 можно запускать docker и без lxc, что, в целом, хорошо (так как liblxc они не использовали, а врапперы вокруг бинарей lxc-* работали, иногда, из рук вон плохо).
      • 0
        Да, просто я не стал использовать лишний софт.

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