LabHub
学习 学习路径 课程

卷、网络与 Compose

容器活着,服务却死了

在 LabHub 中继续学习

一句话总结

进程仍然存活与它能够处理请求是两回事。健康检查弥合两者之间的差距,重启策略则决定失败时该怎么做。

概念图: 健康检查 · 重启策略 · PID 1 还没有退出 · 重要陷阱

为什么需要它

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

状态会按 startinghealthy / 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 失败 → 重启 → 重新建立连接使其更慢 → 再次失败”的反馈循环。这在实际环境中经常发生。

重启策略

策略 行为
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)是两个不同问题。前者失败时只移除流量,后者失败时才重启。合并两者,会使只是暂时繁忙的实例被重启。

实际现场中的情况

下一项检查要关注什么

接下来的测验将要求区分运行中(running)与健康(healthy),并判断应把 start-period、重启策略和 Compose 健康条件应用于哪些故障。请以前一实验中容器的状态变化和退出码为依据作答。