Одноразовые контейнеры в Docker Swarm

В этой статье мы рассмотрим варианты работы с одноразовыми (one-shot) контейнерами в Docker Swarm: разберем некоторые сценарии использования, сравним со старой версией Swarm (до 1.12) и поработаем с тестовыми короткоживущими контейнерами, используя Swarm Services.

Вот несколько сценариев использования короткоживущих контейнеров в кластере:

  • выполнение пакетных заданий — платежные ведомости, SEO, поисковые роботы;
  • распределение ресурсов между хостами;
  • цепочки CI и интеграционные/браузер-тесты;
  • миграции баз данных;
  • резервное копирование;
  • Serverless-задачи — map/reduce, функции и т. д.

Все вышеперечисленное было возможно и в предыдущей инкарнации Docker Swarm, где, используя Docker Remote API, можно было работать с несколькими демонами (хостами) как с одним.

23 июня 2016 года в docker/docker была создана тема issue 23880, в которой началось обсуждение ad-hoc/one-shot-задач в Swarm. Перейдя по указанной ссылке, вы можете получить дополнительную информацию по этому вопросу.

В настоящее время Swarm mode позволяет пользователям с помощью командной строки docker указать группу однородных долгоживущих контейнеров. Однако этот способ абстракции, пусть и достаточно мощный, может оказаться неудобным для контейнеров, которые в итоге должны быть остановлены или запускаются периодически.
Nathan Leclaire

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

Тестовая программа — SEO-анализ

В качестве примера для этой статьи я написал небольшую программку. Она загружает HTML-страницу по переданному ей URL и выдает количество ссылок на внутренние и внешние сайты. Эта функциональность позволяет получить простейшую SEO-оценку веб-страницы. Она автономна, не поддерживает каких-либо состояний и может запускаться по желанию.

Docker до версии 1.12

С помощью remote API можно было сделать что-то вроде этого:

$ export DOCKER_HOST=tcp://swarm_ip:2376
$ docker run --name crawler1 -e url=http://blog.alexellis.io -d crawl_site alexellis2/href-counter

При условии, что хотя бы на одном хосте в Swarm доступен alexellis2/href-counter, эта команда выполнится, и в docker ps появится соответствующая запись.

Различия между Remote API и Swarm Mode

В старой версии Swarm планировка (scheduling) производилась с помощью docker run или docker-compose, а для дальнейшей работы мы подключались к командной строке swarm-менеджера. Этот способ запуска контейнеров можно назвать императивным или ad-hoc. И он вполне подходит для запуска перечисленных выше одноразовых задач.

Swarm Mode вводит концепцию Сервисов (Services), которые определяются декларативно с помощью командной строки или API-запроса. Вместо того чтобы давать команду на запуск (run) контейнера, мы задаем желаемое конечное состояние:

$ export DOCKER_HOST=tcp://swarm_ip:2376
$ docker run -p 80:80 nginx

Становится:

$ docker service create nginx --publish 80:80 nginx

В обоих случаях мы получаем веб-сервер на базе nginx, но тут все же есть существенные различия, такие как, например, способ публикации 80-го порта. В старой версии Swarm было обязательно знать, на какой ноде был интересующий контейнер, но в Swarm Mode 80-й порт для доступа к серверу может быть открыт на любом хосте.

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

Для выполнения этой задачи есть два основных подхода:

  • CLI — с помощью интерфейса командной строки Docker (ну кто бы мог подумать?)
  • API — с помощью Docker/Swarm HTTP API

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

Docker CLI

При объявлении Swarm-сервиса можно указать множество различных опций. Их прописывают в командной строке или в файле docker-compose.yml.

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

Нам нужна настройка --restart-policy:

$ docker service create --restart-policy=none --name crawler1 -e url=http://blog.alexellis.io -d crawl_site alexellis2/href-counter

После установки политики перезапуска в 0 контейнер будет запущен где-то в кластере в виде задачи. Он будет выполнен, а затем завершен после того, как закончит выполнение задачи. Если контейнер по какой-либо уважительной причине не сможет запуститься, политика перезапуска, установленная в 0, в данном случае приведет к тому, что код приложения никогда не будет выполнен. Также не хватает возвращения кода завершения (если он ненулевой) и соответствующих записей журнала. Получается, что в целом такой подход не очень удобен. Является проблемой и то, что сервис теперь будет «захламлять» наш список сервисов в выводе команды docker service ls.

В дополнение замечу, что с точки зрения масштабирования лучше работать с одноразовыми контейнерами не по имени, а по UUID.

Ситуацию можно улучшить, обернув эти задачи в bash-скрипты. Но поскольку у Docker есть первоклассный Golang API, возможно, использование этого программного интерфейса будем самым лучшим вариантом.

Docker API

Я написал небольшую Golang-программку под названием JaaS (Job as a Service — Задача как сервис), которая делает то же самое, что и CLI, но также обладает дополнительной функциональностью:

  • создает сервис с политикой перезапуска restarts = 0;
  • получает динамический ID и подтверждение успешного создания сервиса;
  • опрашивает сервис до тех пор, пока не получит статус ‘exited’;
  • получает stdout/stderr-логи контейнера с помощью экспериментальной функции под названием service logs.

Бинарный файл JaaS может быть запущен на Swarm-менеджере с помощью cron. Поскольку мы создаем обычный сервис, у нас есть возможность указать важные опции:

  • тома для монтирования;
  • имя сети (когда нам нужно иметь доступ к долгоживущим сервисам типа баз данных для выполнения задач миграции и резервного копирования);
  • доступ к секретной информации или Swarm secrets.

Исследуем Docker API

Для работы программы необходимо импортировать следующие пакеты:

import(
     "github.com/docker/docker/api/types"
     "github.com/docker/docker/api/types/filters"
     "github.com/docker/docker/api/types/swarm"
     "github.com/docker/docker/client"
     "golang.org/x/net/context"
)

Это самый простой способ написания Docker-клиента, использующего Docker API. Сервисы в Swarm может создавать только менеджер.

var c *client.Client
    var err error
    c, err = client.NewEnvClient()

С этого момента мы взаимодействуем с API, представляющим локальную ноду Docker в виде, аналогичном docker run или docker build. JaaS использует методы ServiceCreate/ServiceList и TaskList, которые есть у client.Client.

Я не буду вдаваться в подробности, а опишу лишь основные моменты работы программы:

spec := makeSpec(image)
    createOptions := types.ServiceCreateOptions{}
    createResponse, _ := c.ServiceCreate(context.Background(), spec, createOptions)
    fmt.Printf("Service created: %s\n", createResponse.ID)
    pollTask(c, createResponse.ID, timeout, showlogs)

Мы создаем спецификацию, которая в терминологии Swarm называется Service declaration.

Следующие строки создают запрос ServiceSpec, который устанавливает политику перезапуска:

max := uint64(1)
    spec := swarm.ServiceSpec{
        TaskTemplate: swarm.TaskSpec{
            RestartPolicy: &swarm.RestartPolicy{
                MaxAttempts: &max,
                Condition:   swarm.RestartPolicyConditionNone,
            },

Затем мы убеждаемся в том, что сервис создался должным образом, и получаем его ID. Далее опрашиваем задачу до тех пор, пока она не завершится, и загружаем логи.

Выполнение JaaS выглядит следующим образом:

$ jaas -image alexellis2/href-counter:latest --env url=http://blog.alexellis.io/ --showlogs=true
Service created: fervent_bartik (ba0cermll96aqbwu2sma0q7w7)
ID:  ba0cermll96aqbwu2sma0q7w7  Update at:  2017-03-11 18:04:00.404841013 +0000 UTC
...........
Printing service logs
Exit code: 0
State: complete
?2017-03-11T18:04:05.383172605Z com.docker.swarm.node.id=6ehcqb287l63v3oan4ybai7i9,com.docker.swarm.service.id=ba0cermll96aqbwu2sma0q7w7,com.docker.swarm.task.id=xb6lgthlnuozs3qvpqnwi01oo
{"internal":9,"external":5}

В этом проекте я не придаю большого значения названию программы, но рекомендую быть осторожным, чтобы случайно не использовать в названии чужие торговые марки, включая ‘swarm’ и ‘docker’. Здесь я в первую очередь хотел бы показать, что задачами можно управлять через Docker API.

В текущей версии программы ей можно передавать:

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

После того как Swarm сгенерировал поток событий, уже больше не нужно опрашивать API через конечную точку TaskList.

Также стоит подумать об уборке и о судьбе сервисов Swarm после выполнения задачи. Как вариант — назначать метку и на основе ее значения удалять ненужные сервисы.

Скопировать и оценить исходный код JaaS можно здесь: