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