Здесь показаны различия между двумя версиями данной страницы.
| Предыдущая версия справа и слева Предыдущая версия Следующая версия | Предыдущая версия | ||
|
docker:healthcheck:main [2023/12/21 23:12] werwolf [Проверка состояния контейнера в Docker] |
docker:healthcheck:main [2023/12/21 23:20] (текущий) werwolf [Мораль] |
||
|---|---|---|---|
| Строка 22: | Строка 22: | ||
| Представьте, что у нас есть контейнер, в котором живёт веб-сервер, способность отвечать на запросы которого мы хотим проконтролировать. Например, раз в 5 секунд мы будем отправлять ему запрос, давать максимум 10 секунд на то, чтобы тот ответил, и если 3 раза подряд он слажает — будем подозревать неладное. Так как в Dockerfile есть инструкция HEALTHCHECK, и её формат — прост до безобразия ( ''HEALTHCHECK [OPTIONS] CMD command)'', то при помощи какого-нибудь ''curl'' нашу проверку можно сделать так: | Представьте, что у нас есть контейнер, в котором живёт веб-сервер, способность отвечать на запросы которого мы хотим проконтролировать. Например, раз в 5 секунд мы будем отправлять ему запрос, давать максимум 10 секунд на то, чтобы тот ответил, и если 3 раза подряд он слажает — будем подозревать неладное. Так как в Dockerfile есть инструкция HEALTHCHECK, и её формат — прост до безобразия ( ''HEALTHCHECK [OPTIONS] CMD command)'', то при помощи какого-нибудь ''curl'' нашу проверку можно сделать так: | ||
| - | INI | + | <code ini> |
| FROM ... | FROM ... | ||
| Строка 28: | Строка 28: | ||
| HEALTHCHECK --interval=5s --timeout=10s --retries=3 CMD curl -sS 127.0.0.1 || exit 1 | HEALTHCHECK --interval=5s --timeout=10s --retries=3 CMD curl -sS 127.0.0.1 || exit 1 | ||
| ... | ... | ||
| - | + | </code> | |
| - | | 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. | Команда будет запускаться изнутри контейнера, так что обращаться к серверу можно и по 127.0.0.1. | ||
| Строка 37: | Строка 36: | ||
| Она практически никак не отличается от оной в Dockerfile: | Она практически никак не отличается от оной в Dockerfile: | ||
| - | YAML | + | <code ini> |
| ... | ... | ||
| healthcheck: | healthcheck: | ||
| Строка 46: | Строка 44: | ||
| retries: 3 | retries: 3 | ||
| ... | ... | ||
| - | + | </code> | |
| - | | 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 ==== | ==== Проверка в docker run и service create ==== | ||
| У этих двух синтаксис проверок одинаковый и тоже предсказуемо понятный: | У этих двух синтаксис проверок одинаковый и тоже предсказуемо понятный: | ||
| - | Shell | + | <code bash> |
| docker run --health-cmd='curl -sS http://127.0.0.1 || exit 1' \ | docker run --health-cmd='curl -sS http://127.0.0.1 || exit 1' \ | ||
| --health-timeout=10s \ | --health-timeout=10s \ | ||
| Строка 60: | Строка 55: | ||
| --health-interval=5s \ | --health-interval=5s \ | ||
| .... | .... | ||
| - | + | </code> | |
| - | | 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''. | Кстати, если в Dockerfile образе, который мы запускаем, уже была проверка, то на этой стадии её можно переопределить или даже выключить с помощью ''--no-healthcheck=true''. | ||
| Строка 72: | Строка 66: | ||
| server.js | server.js | ||
| - | + | <code javascript> | |
| - | JavaScript | + | |
| "use strict"; | "use strict"; | ||
| - | const http = require%%('%%http'); | + | const http = require('http'); |
| function createServer () { | function createServer () { | ||
| - | return http.createServer(function (req, res) { | + | return http.createServer(function (req, res) { |
| - | res.writeHead(200, %%{'%%Content-Type': 'text/plain'}); | + | res.writeHead(200, {'Content-Type': 'text/plain'}); |
| - | res.end%%('%%OK\n'); | + | res.end('OK\n'); |
| - | }).listen(8080); | + | }).listen(8080); |
| } | } | ||
| Строка 88: | Строка 80: | ||
| http.createServer(function (req, res) { | http.createServer(function (req, res) { | ||
| - | res.writeHead(200, %%{'%%Content-Type': 'text/plain'}); | + | res.writeHead(200, {'Content-Type': 'text/plain'}); |
| - | if (server) { | + | if (server) { |
| - | server.close(); | + | server.close(); |
| - | server = null; | + | server = null; |
| - | res.end%%('%%Shutting down...\n'); | + | res.end('Shutting down...\n'); |
| - | } else { | + | } else { |
| - | server = createServer(); | + | server = createServer(); |
| - | res.end%%('%%Starting up...\n'); | + | res.end('Starting up...\n'); |
| - | } | + | } |
| }).listen(8081); | }).listen(8081); | ||
| - | + | </code> | |
| - | | 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); | | + | |
| Примерно так: | Примерно так: | ||
| - | + | <code bash> | |
| - | Shell | + | |
| $ node server.js | $ node server.js | ||
| # switch to another terminal | # switch to another terminal | ||
| Строка 117: | Строка 106: | ||
| curl 127.0.0.1:8080 | curl 127.0.0.1:8080 | ||
| # OK | # OK | ||
| - | + | </code> | |
| - | | 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'' и запустим: | Теперь положим этот server.js в Dockerfile, добавим туда HEALTHCHECK, соберём это всё в образ по имени ''server'' и запустим: | ||
| Dockerfile | Dockerfile | ||
| - | + | <code ini> | |
| - | INI | + | |
| FROM node | FROM node | ||
| Строка 135: | Строка 121: | ||
| CMD [ "node", "/server.js" ] | CMD [ "node", "/server.js" ] | ||
| + | </code> | ||
| - | | 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" ] | | + | <code bash> |
| - | + | ||
| - | Shell | + | |
| $ docker build . -t server:latest | $ docker build . -t server:latest | ||
| # Lots, lots of output | # Lots, lots of output | ||
| Строка 146: | Строка 130: | ||
| $ curl 127.0.0.1:8080 | $ curl 127.0.0.1:8080 | ||
| # OK | # OK | ||
| - | + | </code> | |
| - | | 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'' — нам будет достаточно, чтобы к нему обратиться, так что самое время перейти к проверкам. | Первых трём символов айдишки контейнера — ''ec3'' — нам будет достаточно, чтобы к нему обратиться, так что самое время перейти к проверкам. | ||
| Строка 155: | Строка 138: | ||
| Основная команда для запроса состояния контейнера в Docker — ''docker inspect''. Она много чего может рассказать, но нам всего-то нужно свойство ''State.Health'': | Основная команда для запроса состояния контейнера в Docker — ''docker inspect''. Она много чего может рассказать, но нам всего-то нужно свойство ''State.Health'': | ||
| - | Shell | + | <code bash> |
| + | $ 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 | ||
| + | </code> | ||
| - | $ 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 проверки подряд), то кое-что изменится в интересную сторону. | Вполне предсказуемо, контейнер — в ‘healthy’ состоянии, а в ''Log'' даже можно посмотреть, чем отзывался сервер на запросы. Но если мы теперь отправим запрос на 8081 и подождём 3*5 секунд (3 проверки подряд), то кое-что изменится в интересную сторону. | ||
| - | Shell | + | <code bash> |
| $ curl 127.0.0.1:8081 | $ curl 127.0.0.1:8081 | ||
| # Shutting down... | # Shutting down... | ||
| Строка 194: | Строка 169: | ||
| # ] | # ] | ||
| #} | #} | ||
| - | + | </code> | |
| - | | 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’. | Я промазал мимо 15 секунд и пропустил аж четыре проверки, поэтому значение в ''FailingStreak'' стало равно четырём. Но в остальном Нострадамус не врал — контейнер перешёл в разряд ‘unhealthy’. | ||
| Строка 201: | Строка 175: | ||
| Правда, достаточно лишь одной успешной проверки, чтобы контейнер официально воскрес: | Правда, достаточно лишь одной успешной проверки, чтобы контейнер официально воскрес: | ||
| - | Shell | + | <code bash> |
| $ curl 127.0.0.1:8081 | $ curl 127.0.0.1:8081 | ||
| # Starting up... | # Starting up... | ||
| $ docker inspect ec3 | jq '.[].State.Health.Status' | $ docker inspect ec3 | jq '.[].State.Health.Status' | ||
| # "healthy" | # "healthy" | ||
| - | + | </code> | |
| - | | 1 2 3 4 | $ curl 127.0.0.1:8081 # Starting up... $ docker inspect ec3 %%|%% jq '.[].State.Health.Status' # "healthy" | | + | |
| ==== Узнаём статус контейнера из Docker events ==== | ==== Узнаём статус контейнера из Docker events ==== | ||
| Кроме как спрашивать контейнер о его здоровье напрямую, можно спросить у его соседей — ''docker events'': | Кроме как спрашивать контейнер о его здоровье напрямую, можно спросить у его соседей — ''docker events'': | ||
| - | Shell | + | <code bash> |
| $ docker events --filter event=health_status | $ 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: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) | # 2017-06-27T00:23:23.998693118-04:00 container health_status: unhealthy ec36579aa452bf683cb17ee44cbab663d148f327be369821ec1df81b7a0e104b (image=server, name=eager_swartz) | ||
| - | + | </code> | |
| - | | 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'' самостоятельно не завершится и будет спамить в консоль до скончания веков. | Обычно событий у Docker достаточно много, так что пришлось приглушить ненужные параметром ''--filter''. Сама ''docker events'' самостоятельно не завершится и будет спамить в консоль до скончания веков. | ||
| Строка 228: | Строка 197: | ||
| Чтобы начать играться с сервисами, мне пришлось временно перевести локальный Docker в Swarm режим через ''docker swarm init''. Зато теперь наш ''server'' образ можно запускать вот так: | Чтобы начать играться с сервисами, мне пришлось временно перевести локальный Docker в Swarm режим через ''docker swarm init''. Зато теперь наш ''server'' образ можно запускать вот так: | ||
| - | Shell | + | <code bash> |
| $ docker service create -p 8080:8080 -p8081:8081 \ | $ docker service create -p 8080:8080 -p8081:8081 \ | ||
| --name server \ | --name server \ | ||
| Строка 241: | Строка 209: | ||
| #ohkvwbsk06vkjyx69434ndqij | #ohkvwbsk06vkjyx69434ndqij | ||
| - | + | </code> | |
| - | | 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 не очень любит локально созданные образы, поэтому-то и выплюнул в консоль несколько ошибок со своим недовольством. Но сервис всё-таки создал и вернул его айдишку: | Оказывается, Swarm не очень любит локально созданные образы, поэтому-то и выплюнул в консоль несколько ошибок со своим недовольством. Но сервис всё-таки создал и вернул его айдишку: | ||
| - | Shell | + | <code bash> |
| docker service ls | docker service ls | ||
| #ID NAME MODE REPLICAS IMAGE | #ID NAME MODE REPLICAS IMAGE | ||
| #ohkvwbsk06vk server replicated 1/1 server | #ohkvwbsk06vk server replicated 1/1 server | ||
| - | + | </code> | |
| - | | 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. Как же так? | ''curl 127.0.0.1:8080'' теперь снова будет возвращать OK (я проверял), а запрос на порт 8081 временно заткнёт сервер. Но в отличие от первого примера, примерно через пол минуты после того, как сервер был явно отключён, он снова начнёт отзываться на 8080. Как же так? | ||
| Строка 258: | Строка 223: | ||
| Прикол в том, что как только мы отключили сервер и его контейнер получил статус unhealthy, это тут же заметил Swarm менеджер и понял, что заявленная конфигурация сервиса больше не выполняется. А так нельзя, поэтому он быстренько прибил проблемный сервисный контейнер и заменил его на новый, рабочий. Следы этого можно увидеть, посмотрев историю задач для всего сервиса: | Прикол в том, что как только мы отключили сервер и его контейнер получил статус unhealthy, это тут же заметил Swarm менеджер и понял, что заявленная конфигурация сервиса больше не выполняется. А так нельзя, поэтому он быстренько прибил проблемный сервисный контейнер и заменил его на новый, рабочий. Следы этого можно увидеть, посмотрев историю задач для всего сервиса: | ||
| - | Shell | + | <code bash> |
| $ docker service ps server | $ docker service ps server | ||
| #ID NAME IMAGE NODE DESIRED STATE CURRENT STATE ERROR PORTS | #ID NAME IMAGE NODE DESIRED STATE CURRENT STATE ERROR PORTS | ||
| #mt67hkhp7ycr server.1 server moby Running Running 50 seconds ago | #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…" | #pj77brhfhsjm \_ server.1 server moby Shutdown Failed about a minute ago "task: non-zero exit (137): do…" | ||
| - | + | </code> | |
| - | | 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'' можно узнать почему: | Маленькая предыстория: для каждого контейнера в сервисе Swarm создаёт задачу. То есть сначала идёт задача, а она уже приводит к контейнеру. Наш первый контейнер «упал», поэтому менеджер создал новую задачу, и она привела к новому контейнеру. Но старая задача осталась в истории! ''docker service ps'' показывает всё цепочку смертей и реинкарнаций задач, и в нашем случае видно, что самая старая задача с айди ''pj77brhfhsjm'' помечена как упавшая, и через ''docker inspect'' можно узнать почему: | ||
| - | Shell | + | <code bash> |
| $ docker inspect pj77 | jq '.[].Status.Err' | $ docker inspect pj77 | jq '.[].Status.Err' | ||
| # "task: non-zero exit (137): dockerexec: unhealthy container" | # "task: non-zero exit (137): dockerexec: unhealthy container" | ||
| - | + | </code> | |
| - | | 1 2 | $ docker inspect pj77 %%|%% jq '.[].Status.Err' # "task: non-zero exit (137): dockerexec: unhealthy container" | | + | |
| «Unhealthy container», вот почему. | «Unhealthy container», вот почему. | ||
| - | ===== Мораль ===== | ||
| - | |||
| - | Проверки состояния контейнеров в Docker — это возможность регулярно запускать сторонние скрипты и экзешники внутри контейнера, чтобы узнать, достаточно ли живо его содержимое. В однохостовом режиме докер просто пометит проблемные контейнеры как unhealthy и сгенерирует health_status событие. В облачном же режиме он ещё и отключит этот контейнер и заменит его на новый, всё ещё здоровый. Быстро и автоматически. | ||
| - | |||
| - | ==== Поделиться ссылкой: ==== | ||
| - | |||
| - | * [[https://dotsandbrackets.com/docker-health-check-ru/?share=facebook&nb=1|Нажмите, чтобы открыть на Facebook (Открывается в новом окне)]] | ||
| - | * [[https://dotsandbrackets.com/docker-health-check-ru/?share=twitter&nb=1|Нажмите, чтобы поделиться на Twitter (Открывается в новом окне)]] | ||
| - | * [[https://dotsandbrackets.com/docker-health-check-ru/?share=linkedin&nb=1|Нажмите, чтобы поделиться на LinkedIn (Открывается в новом окне)]] | ||
| - | * [[https://dotsandbrackets.com/docker-health-check-ru/?share=telegram&nb=1|Нажмите, чтобы поделиться в Telegram (Открывается в новом окне)]] | ||
| - | Метки[[https://dotsandbrackets.com/tag/container/|container]][[https://dotsandbrackets.com/tag/docker/|docker]][[https://dotsandbrackets.com/tag/monitoring/|monitoring]][[https://dotsandbrackets.com/tag/quick-guide/|quick guide]] | ||