S6:容器的健康检查以及依赖检查
声明:所有的实验示例大部分来自《learn-docker-in-a-month-of-lunches》的作者Elton Stoneman,但运行结果并不都是照搬,大部分实验结果可能与原书不同
一、健康检查(health checks)和依赖检查(dependency checks)概述
Q--什么是健康检查(health checks)?有什么用?
A--健康检查用于判断一个容器是否正常工作,容器处于运行状态并不代表容器是正常工作的,因为容器表现的行为可能完全不符合预期,这样得容器我们称之为不正常工作的容器,而健康检查就用于这方面的检查。健康检查可以仅仅只是一个普通指令,也可以是一个脚本,一串逻辑,具体什么样的返回结果才算正常,由你自己决定。如果容器被标记为不健康后,会产生一个事件,这样运行该容器的平台就可以根据该事件做出相应的响应,比如启动一个新的容器替换这个不健康的容器。
Q--什么是依赖检查(denpendency checks)?有什么用?
A--依赖检查用于在容器启动时(没错!只在启动时)检查该容器依赖的服务是否已经运行了,如果没有,该容器进入退出状态。作用就是保持容器与容器之间的依赖关系以及启动顺序。依赖检查可以很简单,比如单纯的通过curl指令访问一下目标服务,如果有正确的响应则算通过,当然实际上检测标准应当更加完善才好。
Q--Docker Compose有depends_on
设置项设置容器间的依赖关系以及启动顺序,为什么还需要依赖检查?
A--因为Docker Compose仅仅只能在一台机器上控制依赖关系。如果应用太大,就需要被分布在许多台机器上,这时候启动时的依赖关系可就很难维护了。再者强硬的使用依赖关系规定A容器必须运行在B容器之前,这样也不是很好,比如,假设A有50个容器,那么B想要运行则必须等这50个全部运行后才可以运行,然而此时恰恰就有1个容器怎么都运行不起来,那么这段时间B也运行不起来,整个应用在这段时间都是不正常的。若是使用依赖检查的话,只要通过了依赖检查容器就可以启动,而不需要管其他什么乱七八糟的,比如,只要有20个A容器启动了,A服务就"表现"正常了,那么B在启动时就可以通过依赖检查,因此就可以启动了。
二、健康检查
- HEALTHCHECK指令
//...
# app image
FROM diamol/dotnet-aspnet
ENTRYPOINT ["dotnet", "/app/Numbers.Api.dll"]
HEALTHCHECK CMD curl --fail http://localhost/health
WORKDIR /app
//...
这是某个镜像的Dockerfile中的一部分,健康检查相关的部分就定义在这里,HEALTHCHECK后面接的指令就是用于健康检查的指令。该指令每隔一段时间就执行一次,如果执行的返回值异常超过一定次数,则该容器会被视为不健康的。默认是每30秒执行一次,3次连续异常则被判断为不健康的。
- 运行一个带健康检查的容器
$ cd diamol/ch08/exercises/numbers
$ docker image build -t ch08-numbers-api -f ./numbers-api/Dockerfile.v2 .
$ docker container run -d -p 8080:80 --health-interval 5s ch08-numbers-api:v2
docker image build
命令中的-f
选项用于指定创建镜像所需要依据的Dockerfile
docker container run
命令中其实可以指定和健康检查的相关的配置,比如:
--health-cmd
指定要执行的健康检查的命令
--health-interval
指定健康检查命令执行的时间间隔,默认ms
--health-retries
指定失败多少次,容器会被标记为不健康的
--health-start-period
指定在多少秒后才正式开始计算失败次数
--health-timeout
健康检查的超时时间,超时会被认为是检查失败
- 查看容器的状态
$ docker container ls
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
2589f5acff74 ch08-numbers-api:v2 "dotnet /app/Numbers…" 24 minutes ago Up 24 minutes (healthy) 0.0.0.0:8080->80/tcp competent_sanderson
在STATUS一栏中能看到(healthy)的状态,则代表该容器是健康的。
因为该应用有一个人为的bug,即访问localhost:8080/rng
三次后再访问会报错,所以接下来我们将人为触发这个bug,然后再查看容器的状态
- 触发bug,查看容器状态
$ curl localhost:8080/rng
$ curl localhost:8080/rng
$ curl localhost:8080/rng
//等待15秒左右
$ docker container ls
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
2589f5acff74 ch08-numbers-api:v2 "dotnet /app/Numbers…" 37 minutes ago Up 37 minutes (unhealthy) 0.0.0.0:8080->80/tcp competent_sanderson
可以看到该容器被标记为不健康了
- 查看健康检查的日志
$ docker container inspect $(docker container ls --last 1 -q)
//...
"State": {
"Status": "running",
"Running": true,
"Paused": false,
"Restarting": false,
"OOMKilled": false,
"Dead": false,
"Pid": 32116,
"ExitCode": 0,
"Error": "",
"StartedAt": "2020-03-17T14:07:33.190455031Z",
"FinishedAt": "0001-01-01T00:00:00Z",
"Health": {
"Status": "unhealthy",
"FailingStreak": 128,
"Log": [
{
"Start": "2020-03-17T22:55:33.399820121+08:00",
"End": "2020-03-17T22:55:33.542807814+08:00",
"ExitCode": 22,
"Output": " % Total % Received % Xferd Average Speed Time Time Time Current\n Dload Upload Total Spent Left Speed\n\r 0 0 0 0 0 0 0 0 --:--:-- --:--:-- --:--:-- 0\r 0 0 0 0 0 0 0 0 --:--:-- --:--:-- --:--:-- 0\ncurl: (22) The requested URL returned error: 500 Internal Server Error\n"
},
{
"Start": "2020-03-17T22:55:38.572044692+08:00",
"End": "2020-03-17T22:55:38.715793476+08:00",
"ExitCode": 22,
"Output": " % Total % Received % Xferd Average Speed Time Time Time Current\n Dload Upload Total Spent Left Speed\n\r 0 0 0 0 0 0 0 0 --:--:-- --:--:-- --:--:-- 0\r 0 0 0 0 0 0 0 0 --:--:-- --:--:-- --:--:-- 0\ncurl: (22) The requested URL returned error: 500 Internal Server Error\n"
},
{
"Start": "2020-03-17T22:55:43.74781023+08:00",
"End": "2020-03-17T22:55:43.887389369+08:00",
"ExitCode": 22,
"Output": " % Total % Received % Xferd Average Speed Time Time Time Current\n Dload Upload Total Spent Left Speed\n\r 0 0 0 0 0 0 0 0 --:--:-- --:--:-- --:--:-- 0\r 0 0 0 0 0 0 0 0 --:--:-- --:--:-- --:--:-- 0\ncurl: (22) The requested URL returned error: 500 Internal Server Error\n"
},
//...
--last
是选择最近运行的那个容器,在State下的Health下的Log项中记录了每次检查失败的记录
三、依赖检查
注意:依赖检查不像健康检查一样有特殊的指令可以指定,所以依赖检查实际上就是一个简单的条件语句,如果检查通过,则执行运行服务所需要的关键命令,如果没通过,则会因为没有什么其他命令可执行而进入到退出状态。比如:
CMD curl --fail http://numbers-api/rng && dotnet Numbers.Web.dll
&&表示如果
curl --fail http://numbers-api/rng
(即依赖检查)没有出错,则执行dotnet Numbers.Web.dll
,否则不执行该命令,由于该命令是整个该容器(服务)的启动命令,不执行该命令就等效于容器任务执行完成(因为没有其他命令可以执行),故而进入到退出状态。
四、健康检查与依赖检查在Docker Compose工具中的应用
- Docker Compose中健康检查的配置
services:
numbers-api:
image: ch08-numbers-api:vmtk
ports:
- "8087:80"
healthcheck:
test: ["CMD" , "curl" , "--fail" , "http://localhost/health"]
interval: 5s
timeout: 1s
retries: 2
start_period: 5s
networks:
- app-net
//...
这是compose文件中的一部分,healthcheck项下就是关于健康检查的配置:
test
后面接一个数组,放的是健康检查的命令,每个字符串占用一个数组元素
interval
、timeout
、retries
、start_period
前面已经介绍过了,不再赘述
- Docker Compose中依赖检查的配置
//...
numbers-web:
image: ch08-numbers-web:v3
restart: on-failure
environment:
- RngApi__Url=http://numbers-api/rng
ports:
- "8088:80"
healthcheck:
test: ["CMD", "dotnet", "Utilities.HttpCheck.dll", "-t", "150"]
interval: 5s
timeout: 1s
retries: 2
start_period: 10s
networks:
- app-net
//...
numbers-web服务是依赖于numbers-api服务的,但是我们可以发现它并没有配置
depends_on
设置项,原因在开头已经解释过了。这里最关键的配置在于restart: on-failure
一项,它告诉Docker Compose如果该容器启动失败则重启该容器,这样当容器因为依赖检查失败而进入到退出状态时,Docker Compose会重启它,而此时该容器所依赖的容器可能已经成功运行了,因此该容器也就会通过依赖检查而成功启动。
五、其他
- 注意事项:
- 健康检查是会耗费计算机资源的,所以务必注意健康检查在资源占用和使用效果上的平衡,如果健康检查太严格,那么将过于耗费计算机资源,如果健康检查太宽松,那么健康检查的实际效果可能会大打折扣,而由于没有及时检测到问题,用户的体验将会持续下降。
- 建议在程序中编写一套自己的工具用于健康检查和依赖检查,而不是使用外部工具,比如
curl
。理由如下:- 减少了需要添加的额外工具,越多的外部工具意味着越多的风险
- 你自己应用的状态如何只有你自己最清楚,利用外部工具所能达到的效果可能并不如意
- 你自己编写的工具,你可以检测任何你想要的东西,并根据这些数据来利用更完善的逻辑去判断服务的健康状态
- 因为是属于程序内部的工具,所以使用的库将是一个统一的库,使用的配置将是一个统一的配置,管理方便
- 将会得到Docker跨平台的优点,检查结果不会因为平台而影响
参考文档:
[1] learn-docker-in-a-month-of-lunches
[2] 官方文档
附:
[1] Elton Stoneman的github项目