Thank you for reading this post, don't forget to subscribe!
мы можем проверять, отзывается ли веб-сервер внутри контейнера на входящие запросы, вменяемое ли количество памяти он использует, встречаются ли в логах фразы вроде «epic fail!!!»… Много чего можно проверить, ведь проверка делается через запуск какого-нибудь стороннего скрипта или приложения, а её результат зависит от кода, с которым это приложение завершилось.
В обычном режиме контейнер, последние несколько проверок которого оказались «так себе», получит атрибут «unhealthy», в Docker events появится соответствующая запись (health_status
), и на этом история закончится. Но если речь идёт о Swarm режиме, то проблемный контейнер без лишних разговоров усыпят и автоматически запустят новый. Вот так жестоко.
Как включить проверку состояний
Есть по крайней мере четыре места, где её можно включить и настроить:
- в самом Dockerfile,
- в команде
docker run
, - в YAML для
docker-compose
иdocker stack
- и в
docker service create
команде.
Как минимум в настройку проверки нужно передать команду (скрипт, экзешник), которая эту проверку будет делать. Команда должна завершаться с кодом 0
, если контейнер здоров, и 1
, если всё уже плохо. В довесок можно указать, как часто запускать проверку (--interval
), как долго ей можно длиться (--timeout
), и сколько раз (—retries) она должна вернуть 1
, прежде чем на контейнере поставят жирный «unhealthy» крест.
Проверка состояния контейнера в Dockerfile
Представьте, что у нас есть контейнер, в котором живёт веб-сервер, способность отвечать на запросы которого мы хотим проконтролировать. Например, раз в 5 секунд мы будем отправлять ему запрос, давать максимум 10 секунд на то, чтобы тот ответил, и если 3 раза подряд он слажает — будем подозревать неладное. Так как в Dockerfile есть инструкция HEALTHCHECK, и её формат — прост до безобразия ( HEALTHCHECK [OPTIONS] CMD command)
, то при помощи какого-нибудь curl
нашу проверку можно сделать так:
FROM …
…
HEALTHCHECK --interval=5s --timeout=10s --retries=3 CMD curl -sS 127.0.0.1 || exit 1
…
Команда будет запускаться изнутри контейнера, так что обращаться к серверу можно и по 127.0.0.1.
Проверка состояния в docker-compose YAML
Она практически никак не отличается от оной в Dockerfile:
[codesyntax lang="php"]
1 2 3 4 5 6 7 |
... healthcheck: test: curl -sS http://127.0.0.1 || echo 1 interval: 5s timeout: 10s retries: 3 ... |
[/codesyntax]
Проверка в docker run и service create
У этих двух синтаксис проверок одинаковый и тоже предсказуемо понятный:
docker run --health-cmd='curl -sS http://127.0.0.1 || echo 1' \
--health-timeout=10s \
--health-retries=3 \
--health-interval=5s \
.…
Кстати, если в Dockerfile образе, который мы запускаем, уже была проверка, то на этой стадии её можно переопределить или даже выключить с помощью --no-healthcheck=true
.
Пример проверки состояний
Жертва
Имеется маленький node.js сервер, главной целью в жизни которого является отвечать ‘OK’ на запросы к порту 8080, и отключаться/включаться после запросов к порту 8081:
[codesyntax lang="php" blockstate="collapsed"]
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
"use strict"; const http = require('http'); function createServer () { return http.createServer(function (req, res) { res.writeHead(200, {'Content-Type': 'text/plain'}); res.end('OK\n'); }).listen(8080); } let server = createServer(); http.createServer(function (req, res) { res.writeHead(200, {'Content-Type': 'text/plain'}); if (server) { server.close(); server = null; res.end('Shutting down…\n'); } else { server = createServer(); res.end('Starting up…\n'); } }).listen(8081); |
[/codesyntax]
Примерно так:
$ node server.js
# switch to another terminal
curl 127.0.0.1:8080
# OK
curl 127.0.0.1:8081
# Shutting down…
curl 127.0.0.1:8080
# curl: (7) Failed to connect to 127.0.0.1 port 8080: Connection refused
curl 127.0.0.1:8081
# Starting up…
curl 127.0.0.1:8080
# OK
Теперь положим этот server.js в Dockerfile, добавим туда HEALTHCHECK, соберём это всё в образ по имени server
и запустим:
FROM node
COPY server.js /
EXPOSE 8080 8081
HEALTHCHECK --interval=5s --timeout=10s --retries=3 CMD curl -sS 127.0.0.1:8080 || exit 1
CMD [ "node", "/server.js" ]
$ docker build . -t server:latest
# Lots, lots of output
$ docker run -d --rm -p 8080:8080 -p 8081:8081 server
# ec36579aa452bf683cb17ee44cbab663d148f327be369821ec1df81b7a0e104b
$ curl 127.0.0.1:8080
# OK
Первых трём символов айдишки контейнера — ec3
— нам будет достаточно, чтобы к нему обратиться, так что самое время перейти к проверкам.
Мониторим состояние контейнера
Основная команда для запроса состояния контейнера в Docker — docker inspect
. Она много чего может рассказать, но нам всего-то нужно свойство State.Health
:
[codesyntax lang="php"]
1 2 3 4 5 6 7 8 9 10 11 12 13 |
$ docker inspect ec3 | jq '.[].State.Health' #{ # "Status": "healthy", # "FailingStreak": 0, # "Log": [ # { # "Start": "2017-06-27T04:07:03.975506353Z", # "End": "2017-06-27T04:07:04.070844091Z", # "ExitCode": 0, # "Output": "OK\n" # }, #… } |
[/codesyntax]
Вполне предсказуемо, контейнер — в ‘healthy’ состоянии, а в Log
даже можно посмотреть, чем отзывался сервер на запросы. Но если мы теперь отправим запрос на 8081 и подождём 3*5 секунд (3 проверки подряд), то кое-что изменится в интересную сторону.
[codesyntax lang="php"]
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
$ curl 127.0.0.1:8081 # Shutting down… # Через 15 секунд $ docker inspect ec3 | jq '.[].State.Health' #{ # "Status": "unhealthy", # "FailingStreak": 4, # "Log": [ # … # { # "Start": "2017-06-27T04:16:27.668441692Z", # "End": "2017-06-27T04:16:27.740937964Z", # "ExitCode": 1, # "Output": "curl: (7) Failed to connect to 127.0.0.1 port 8080: Connection refused\n" # } # ] #} |
[/codesyntax]
Я промазал мимо 15 секунд и пропустил аж четыре проверки, поэтому значение в FailingStreak
стало равно четырём. Но в остальном Нострадамус не врал — контейнер перешёл в разряд ‘unhealthy’.
Правда, достаточно лишь одной успешной проверки, чтобы контейнер официально воскрес:
$ curl 127.0.0.1:8081
# Starting up…
$ docker inspect ec3 | jq '.[].State.Health.Status'
# "healthy"
Узнаём статус контейнера из Docker events
Кроме как спрашивать контейнер о его здоровье напрямую, можно спросить у его соседей — docker events
:
$ docker events --filter event=health_status
# 2017-06-27T00:23:03.691677875-04:00 container health_status: healthy ec36579aa452bf683cb17ee44cbab663d148f327be369821ec1df81b7a0e104b (image=server, name=eager_swartz)
# 2017-06-27T00:23:23.998693118-04:00 container health_status: unhealthy ec36579aa452bf683cb17ee44cbab663d148f327be369821ec1df81b7a0e104b (image=server, name=eager_swartz)
Обычно событий у Docker достаточно много, так что пришлось приглушить ненужные параметром --filter
. Сама docker events
самостоятельно не завершится и будет спамить в консоль до скончания веков.
Состояние контейнера в Swarm сервисах
Чтобы начать играться с сервисами, мне пришлось временно перевести локальный Docker в Swarm режим через docker swarm init
. Зато теперь наш server
образ можно запускать вот так:
$ docker service create -p 8080:8080 -p8081:8081 \
--name server \
--health-cmd='curl -sS 127.0.0.1:8080' \
--health-retries=3 \
--health-interval=5s \
server
#unable to pin image server to digest: errors:
#denied: requested access to the resource is denied
#unauthorized: authentication required
#ohkvwbsk06vkjyx69434ndqij
Оказывается, Swarm не очень любит локально созданные образы, поэтому-то и выплюнул в консоль несколько ошибок со своим недовольством. Но сервис всё-таки создал и вернул его айдишку:
1 2 3 |
docker service ls #ID NAME MODE REPLICAS IMAGE #ohkvwbsk06vk server replicated 1/1 server |
curl 127.0.0.1:8080
теперь снова будет возвращать OK (я проверял), а запрос на порт 8081 временно заткнёт сервер. Но в отличие от первого примера, примерно через пол минуты после того, как сервер был явно отключён, он снова начнёт отзываться на 8080. Как же так?
Прикол в том, что как только мы отключили сервер и его контейнер получил статус unhealthy, это тут же заметил Swarm менеджер и понял, что заявленная конфигурация сервиса больше не выполняется. А так нельзя, поэтому он быстренько прибил проблемный сервисный контейнер и заменил его на новый, рабочий. Следы этого можно увидеть, посмотрев историю задач для всего сервиса:
1 2 3 4 |
$ docker service ps server #ID NAME IMAGE NODE DESIRED STATE CURRENT STATE ERROR PORTS #mt67hkhp7ycr server.1 server moby Running Running 50 seconds ago #pj77brhfhsjm \_ server.1 server moby Shutdown Failed about a minute ago "task: non-zero exit (137): do…" |
Маленькая предыстория: для каждого контейнера в сервисе Swarm создаёт задачу. То есть сначала идёт задача, а она уже приводит к контейнеру. Наш первый контейнер «упал», поэтому менеджер создал новую задачу, и она привела к новому контейнеру. Но старая задача осталась в истории! docker service ps
показывает всё цепочку смертей и реинкарнаций задач, и в нашем случае видно, что самая старая задача с айди pj77brhfhsjm
помечена как упавшая, и через docker inspect
можно узнать почему:
1 2 |
$ docker inspect pj77 | jq '.[].Status.Err' # "task: non-zero exit (137): dockerexec: unhealthy container" |
«Unhealthy container», вот почему.
Мораль
Проверки состояния контейнеров в Docker — это возможность регулярно запускать сторонние скрипты и экзешники внутри контейнера, чтобы узнать, достаточно ли живо его содержимое. В однохостовом режиме докер просто пометит проблемные контейнеры как unhealthy и сгенерирует health_status событие. В облачном же режиме он ещё и отключит этот контейнер и заменит его на новый, всё ещё здоровый. Быстро и автоматически.