很多刚接触 Docker 的人都会看这个命令:
dockerps只要看到:
STATUS Up 10 seconds就觉得:
服务启动成功了。
但实际上:
容器启动成功,不代表容器里的服务已经可以正常使用。
这也是 Docker 项目里一个非常常见的问题。
最近我在维护开源项目 ShiyuAdmin:
https://github.com/Rodert/ShiyuAdmin里面就用到了一个非常实用的 Docker 功能:
Healthcheck今天只讲这一个知识点。
一、先看一个真实问题
假设我们的系统有三个服务:
Go Backend PostgreSQL Redis通过 Docker Compose 一起启动:
dockercompose up-dDocker 可能按照下面的顺序启动:
PostgreSQL 容器启动 Redis 容器启动 Go 容器启动看起来没有问题。
但是这里有一个坑。
PostgreSQL 容器虽然已经启动了:
Container Running但 PostgreSQL 自己可能还在:
初始化数据库 创建用户 加载数据文件 启动数据库服务这个时候 Go 后端马上连接:
PostgreSQL:5432就可能直接报:
connection refused所以我们需要区分两个概念。
容器启动成功和:
容器里的服务已经准备好完全不是一回事。
二、Healthcheck 是干什么的?
Docker Healthcheck 就是用来检查:
容器里面的服务到底还能不能正常工作。
比如 PostgreSQL。
我们可以这样配置:
services:postgres:image:postgres:15healthcheck:test:["CMD-SHELL","pg_isready -U postgres"]interval:10stimeout:5sretries:5Docker 会定期在容器里面执行:
pg_isready-Upostgres如果 PostgreSQL 已经可以接受连接:
healthy如果还没有准备好:
starting如果连续多次检查失败:
unhealthy三、Docker 容器其实有三种健康状态
配置 Healthcheck 以后,我们执行:
dockerps可能看到:
Up 10 seconds (health: starting)过一会:
Up 30 seconds (healthy)如果服务异常:
Up 2 minutes (unhealthy)所以容器状态不再只是:
Running还多了一层:
starting healthy unhealthy这就比单纯判断进程是否存在准确很多。
四、ShiyuAdmin 是怎么做的?
以 PostgreSQL 为例:
shiyu-postgres:image:postgres:15healthcheck:test:["CMD-SHELL","pg_isready -U shiyu -d shiyu_admin_scaffold"]interval:10stimeout:5sretries:5这里:
interval:10s表示:
每隔 10 秒检查一次timeout:5s表示:
一次检查超过 5 秒算失败retries:5表示:
连续失败 5 次以后认为 unhealthy整体逻辑就是:
Docker ↓ 每 10 秒 ↓ 执行 pg_isready ↓ 成功? ┌───────┴───────┐ 是 否 ↓ ↓ healthy 继续重试五、Redis 更简单
Redis 可以直接使用:
redis-cliping正常情况下会返回:
PONG因此 Docker Compose 可以写:
shiyu-redis:image:redis:7healthcheck:test:["CMD","redis-cli","ping"]interval:10stimeout:5sretries:5这其实就是 Docker Healthcheck 最核心的思想:
找一个真正能代表服务正常工作的命令。
六、Web 服务怎么检查?
假设我们的 Go 后端提供:
/api/v1/system/health这个接口正常时返回:
{"status":"ok"}我们就可以直接访问这个接口判断服务状态。
比如:
healthcheck:test:["CMD-SHELL","wget --quiet --tries=1 --spider http://127.0.0.1/api/v1/system/health || exit 1"]interval:30stimeout:10sretries:3这比判断:
Go 进程是否存在靠谱得多。
因为下面这种情况非常常见:
Go 进程存在 数据库连接挂了 Redis 挂了 接口已经不能正常访问从操作系统角度:
Process Running但是从用户角度:
Service DownHealthcheck 应该尽可能检查:
服务能力而不仅仅是:
进程能力七、Healthcheck 最大的搭档:depends_on
Healthcheck 单独使用已经很有价值。
但是在 Docker Compose 里,它还有一个经典搭配:
depends_on比如:
shiyu-app:depends_on:shiyu-postgres:condition:service_healthyshiyu-redis:condition:service_healthy什么意思?
Go 后端不会简单地等:
PostgreSQL 容器启动而是等:
PostgreSQL 健康检查通过Redis 同理。
于是启动顺序变成:
PostgreSQL 启动 ↓ Healthcheck ↓ PostgreSQL healthy Redis 启动 ↓ Healthcheck ↓ Redis healthy ↓ Go Backend 启动这就可以减少很多:
connection refused之类的问题。
八、没有 Healthcheck 会发生什么?
如果只写:
depends_on:-postgres-redis它通常只能保证:
postgres 容器先启动 redis 容器先启动但是不能保证:
PostgreSQL 已经可以连接 Redis 已经可以访问比如:
00:00 PostgreSQL 容器启动 00:01 Go Backend 启动 00:02 Go 尝试连接数据库 00:03 PostgreSQL 还在初始化 00:03 connection refused 00:05 PostgreSQL 才真正 Ready这就是为什么:
启动顺序和:
服务就绪顺序不是一个概念。
九、自己写一个健康检查接口
如果你是 Java、Go、Node.js 开发者,我非常建议自己的项目提供一个:
/health接口。
最简单的 Go 示例:
packagemainimport("encoding/json""net/http")funchealthHandler(w http.ResponseWriter,r*http.Request){w.Header().Set("Content-Type","application/json")json.NewEncoder(w).Encode(map[string]string{"status":"ok",})}funcmain(){http.HandleFunc("/health",healthHandler)http.ListenAndServe(":8080",nil)}然后 Docker:
healthcheck:test:["CMD-SHELL","wget --quiet --spider http://127.0.0.1:8080/health || exit 1"]interval:30stimeout:5sretries:3这样就完成了一个最简单的服务健康检查。
十、再进一步:不要只返回 ok
真正生产环境里,可以检查:
数据库 Redis 磁盘 关键依赖比如:
funchealthHandler(w http.ResponseWriter,r*http.Request){iferr:=db.Ping();err!=nil{w.WriteHeader(http.StatusServiceUnavailable)json.NewEncoder(w).Encode(map[string]string{"status":"error","database":"down",})return}json.NewEncoder(w).Encode(map[string]string{"status":"ok","database":"ok",})}这样:
Go 进程活着但是数据库挂了的时候:
/health依然会返回失败。
Docker 就能检测出来:
unhealthy十一、怎么查看具体健康检查结果?
先执行:
dockerps如果看到:
unhealthy可以进一步执行:
dockerinspect 容器名或者:
dockerinspect\--format='{{json .State.Health}}'\容器名里面可以看到最近几次 Healthcheck 的:
执行时间 退出码 输出结果排查起来非常方便。
十二、一个完整例子
下面是一个简化版 Docker Compose:
services:postgres:image:postgres:15environment:POSTGRES_PASSWORD:123456healthcheck:test:["CMD-SHELL","pg_isready -U postgres"]interval:10stimeout:5sretries:5redis:image:redis:7healthcheck:test:["CMD","redis-cli","ping"]interval:10stimeout:5sretries:5app:image:my-app:latestdepends_on:postgres:condition:service_healthyredis:condition:service_healthyhealthcheck:test:["CMD-SHELL","wget --quiet --spider http://127.0.0.1:8080/health || exit 1"]interval:30stimeout:5sretries:3整个启动逻辑非常清晰:
PostgreSQL ↓ healthy Redis ↓ healthy ↓ App ↓ /health ↓ healthy这已经是一套非常标准的 Docker 服务启动方案。
十三、Healthcheck 解决的其实不是 Docker 问题
表面上看:
Healthcheck是一个 Docker 配置。
但它解决的实际上是一个非常重要的分布式系统问题:
服务存在不等于:
服务可用以后学 Kubernetes,你还会看到类似的东西:
Liveness Probe Readiness Probe Startup Probe其实思路都是一样的:
你不能只判断一个进程有没有启动。 你还要知道: 它能不能真正提供服务。所以如果你的 Docker Compose 里现在只有:
image:ports:environment:volumes:可以看看自己的项目是不是应该补一个:
healthcheck:这个配置看起来只有几行。
但对于数据库、Redis、后端服务这些存在启动依赖的系统来说,往往非常有价值。
Docker 容器 Running,只能证明容器还活着。
Healthcheck Healthy,才更接近我们真正想知道的事情:服务能不能用。