容器活着,服务却死了
一句话总结
进程仍然存活与它能够处理请求是两回事。健康检查弥合两者之间的差距,重启策略则决定失败时该怎么做。
为什么需要它
docker ps 显示 Up 3 hours,用户却收到 500 错误。进程虽然还活着,但可能是数据库连接池已耗尽、线程陷入死锁,或磁盘已满而无法写入。
Docker 只知道一件事:PID 1 还没有退出。仅凭这一点判断服务正常,会使故障漏报数小时。
工作原理
HEALTHCHECK --interval=30s --timeout=3s --start-period=40s --retries=3 \
CMD curl -fsS http://localhost:8080/healthz || exit 1
| 选项 | 含义 | 设置错误时的后果 |
|---|---|---|
interval |
检查周期 | 太短会增加负载,太长会延迟发现 |
timeout |
单次检查的时间限制 | 太短会把正常情况判为失败 |
start-period |
启动宽限期——此时的失败不计入重试 | 缺少时,启动较慢的应用会不断退出 |
retries |
允许连续失败的次数 | 设为 1 时,瞬时延迟也会导致 unhealthy |
状态会按 starting → healthy / unhealthy 变化。可通过 docker inspect --format '{{.State.Health.Status}}' 查看。
重要陷阱:单独使用 Docker 时,即使变成 unhealthy,也不会重启容器,只会标记状态。重启由编排器负责,例如 Compose 的 depends_on: condition: service_healthy、Swarm 或 Kubernetes 的 liveness probe。不能因为加入了健康检查,就以为服务会自行恢复。
如何设计健康检查端点
如果 /healthz 无条件返回 200 OK,就等于什么也没检查。反过来,如果同时检查数据库、缓存和外部 API,当某个外部 API 出现波动时,我们的整个服务都会变成 unhealthy。
边界应这样划分。
- liveness(是否存活)——进程自身是否处于无法恢复的状态。只检查必须通过重启解决的问题。
- readiness(是否就绪)——当前是否可以接收流量,包括数据库连接等。
如果不作区分,就会陷入“数据库短暂变慢 → liveness 失败 → 重启 → 重新建立连接使其更慢 → 再次失败”的反馈循环。这在实际环境中经常发生。
重启策略
| 策略 | 行为 |
|---|---|
no(默认) |
退出后保持停止 |
on-failure[:N] |
仅在退出码非 0 时重启,最多 N 次 |
always |
始终重启,守护进程重启后也重新启动 |
unless-stopped |
与 always 相同,但人工停止后保持停止 |
always 可能造成无限重启循环。典型情况是容器因配置错误立即退出,每秒重启数次并不断填充日志。Docker 会逐步延长重启间隔(退避)来缓解,但这不是根本解决方案。
depends_on 只保证顺序
services:
app:
depends_on: [db] # db 컨테이너가 '시작'된 뒤 app 을 시작할 뿐
它不会检查 db 是否已准备好接收请求。PostgreSQL 进程启动后,初始化还需要几秒。如果应用在此期间连接,就会失败。必须通过 condition: service_healthy 与健康检查结合才有意义。
健康检查反而造成故障的情况
健康检查本是安全机制,但设计错误时会比没有更糟。应避开以下情况。
把依赖项纳入检查会导致一起退出。 如果 /health 查询数据库,数据库短暂变慢时,所有实例会同时变为 unhealthy 并重启。重启后的实例又同时连接数据库,使情况进一步恶化。检查是否存活时应只检查自身。
杀死启动较慢的程序。 JVM 或加载大型模型的服务可能需要一分钟启动,如果检查在 30 秒时失败,就会永远反复重启。Docker 用 --start-period,Kubernetes 用 startupProbe 提供这段宽限时间。
HEALTHCHECK --interval=30s --timeout=3s --start-period=60s --retries=3 CMD curl -fsS http://localhost:8080/healthz || exit 1
重启策略掩盖失败。 restart: always 会在每次退出后重新启动容器,因此,因配置错误每 30 秒退出一次的服务表面上仍像是在“运行”。采用 on-failure:5 这类次数限制后,持续失败时会停止,从而让人发现问题。Kubernetes 的 CrashLoopBackOff 含义也相同:以指数方式减慢重启,同时让问题暴露出来。
镜像中可能没有检查所需的工具。 distroless 镜像既没有 curl 也没有 wget,健康检查会始终失败。此时,应让应用程序二进制文件提供自检模式,或像 Kubernetes 的 HTTP 探针那样采用从外部检查的方式。
拆分两种检查。 是否已准备好接收流量(readiness)与是否仍然存活(liveness)是两个不同问题。前者失败时只移除流量,后者失败时才重启。合并两者,会使只是暂时繁忙的实例被重启。
实际现场中的情况
- “重启后就好了”反复发生 → liveness 通过,但服务实际不可用。
- 只在部署后立即失败 → 没有
start-period,启动过程被健康检查判为失败。 - 日志充满重启消息 →
always与立即失败组合。
下一项检查要关注什么
接下来的测验将要求区分运行中(running)与健康(healthy),并判断应把 start-period、重启策略和 Compose 健康条件应用于哪些故障。请以前一实验中容器的状态变化和退出码为依据作答。