В этой статье мы рассмотрим варианты работы с одноразовыми (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 можно здесь: