Инструменты пользователя

Инструменты сайта


docker:healthcheck:main

Это старая версия документа!


Проверка состояния контейнера в Docker

PavelАвтор: Pavel 2 июля, 2017 4 июня, 2023

Каким-то непонятным образом я пропустил новость о том, что начиная с версии 1.12 Docker контейнерам можно навешивать проверку состояния. И речь тут идёт не о проверке, запущен ли ещё контейнер, а о том, делает ли он всё ещё то, ради чего создавался. Например, мы можем проверять, отзывается ли веб-сервер внутри контейнера на входящие запросы, вменяемое ли количество памяти он использует, встречаются ли в логах фразы вроде «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 нашу проверку можно сделать так:

INI

FROM … … HEALTHCHECK –interval=5s –timeout=10s –retries=3 CMD curl -sS 127.0.0.1 || exit 1 …

1 2 3 4 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:

YAML

… healthcheck:

test: curl -sS http://127.0.0.1 || exit 1
interval: 5s
timeout: 10s
retries: 3

1 2 3 4 5 6 7 … healthcheck:   test: curl -sS http://127.0.0.1 || exit 1   interval: 5s   timeout: 10s   retries: 3 …

Проверка в docker run и service create

У этих двух синтаксис проверок одинаковый и тоже предсказуемо понятный:

Shell

docker run –health-cmd='curl -sS http://127.0.0.1 || exit 1' \

  1. -health-timeout=10s \
  2. -health-retries=3 \
  3. -health-interval=5s \

….

1 2 3 4 5 docker run –health-cmd='curl -sS http://127.0.0.1 || exit 1' \     –health-timeout=10s \     –health-retries=3 \     –health-interval=5s \     ….

Кстати, если в Dockerfile образе, который мы запускаем, уже была проверка, то на этой стадии её можно переопределить или даже выключить с помощью –no-healthcheck=true.

Большой пример проверки состояний

Жертва

Я сделал маленький node.js сервер, главной целью в жизни которого является отвечать ‘OK’ на запросы к порту 8080, и отключаться/включаться после запросов к порту 8081:

server.js

JavaScript

«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);

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);

Примерно так:

Shell

$ 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

1 2 3 4 5 6 7 8 9 10 11 12 $ 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 и запустим:

Dockerfile

INI

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» ]

1 2 3 4 5 6 7 8 9 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» ]

Shell

$ 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

1 2 3 4 5 6 $ 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:

Shell

$ 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» # }, #… }

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» #    }, #… }

Вполне предсказуемо, контейнер — в ‘healthy’ состоянии, а в Log даже можно посмотреть, чем отзывался сервер на запросы. Но если мы теперь отправим запрос на 8081 и подождём 3*5 секунд (3 проверки подряд), то кое-что изменится в интересную сторону.

Shell

$ 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» # } # ] #}

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» #    } #  ] #}

Я промазал мимо 15 секунд и пропустил аж четыре проверки, поэтому значение в FailingStreak стало равно четырём. Но в остальном Нострадамус не врал — контейнер перешёл в разряд ‘unhealthy’.

Правда, достаточно лишь одной успешной проверки, чтобы контейнер официально воскрес:

Shell

$ curl 127.0.0.1:8081 # Starting up… $ docker inspect ec3 | jq '.[].State.Health.Status' # «healthy»

1 2 3 4 $ curl 127.0.0.1:8081 # Starting up… $ docker inspect ec3 | jq '.[].State.Health.Status' # «healthy»

Узнаём статус контейнера из Docker events

Кроме как спрашивать контейнер о его здоровье напрямую, можно спросить у его соседей — docker events:

Shell

$ 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)

1 2 3 $ 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 образ можно запускать вот так:

Shell

$ docker service create -p 8080:8080 -p8081:8081 \

  1. -name server \
  2. -health-cmd='curl -sS 127.0.0.1:8080' \
  3. -health-retries=3 \
  4. -health-interval=5s \

server #unable to pin image server to digest: errors: #denied: requested access to the resource is denied #unauthorized: authentication required

#ohkvwbsk06vkjyx69434ndqij

1 2 3 4 5 6 7 8 9 10 11 $ 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 не очень любит локально созданные образы, поэтому-то и выплюнул в консоль несколько ошибок со своим недовольством. Но сервис всё-таки создал и вернул его айдишку:

Shell

docker service ls #ID NAME MODE REPLICAS IMAGE #ohkvwbsk06vk server replicated 1/1 server

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 менеджер и понял, что заявленная конфигурация сервиса больше не выполняется. А так нельзя, поэтому он быстренько прибил проблемный сервисный контейнер и заменил его на новый, рабочий. Следы этого можно увидеть, посмотрев историю задач для всего сервиса:

Shell

$ 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…»

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 можно узнать почему:

Shell

$ docker inspect pj77 | jq '.[].Status.Err' # «task: non-zero exit (137): dockerexec: unhealthy container»

1 2 $ docker inspect pj77 | jq '.[].Status.Err' # «task: non-zero exit (137): dockerexec: unhealthy container»

«Unhealthy container», вот почему.

Мораль

Проверки состояния контейнеров в Docker — это возможность регулярно запускать сторонние скрипты и экзешники внутри контейнера, чтобы узнать, достаточно ли живо его содержимое. В однохостовом режиме докер просто пометит проблемные контейнеры как unhealthy и сгенерирует health_status событие. В облачном же режиме он ещё и отключит этот контейнер и заменит его на новый, всё ещё здоровый. Быстро и автоматически.

Поделиться ссылкой:

docker/healthcheck/main.1703189501.txt.gz · Последние изменения: 2023/12/21 23:11 — werwolf