docker 容器卡死:无响应与健康检查挂起速查
更新时间:2026-10-24。FAQ 速查页,面向容器「活着但没反应」——
docker stop转圈十秒才杀掉、健康检查一直 unhealthy、请求超时无响应的同学。docker 容器卡死与崩溃是两类病:崩溃是进程死了(退出码说话),卡死是进程还在但不干活——大多数卡死的根子不在「应用挂了」,而在信号传递链断了:主进程不是 PID 1、被 shell 包装了一层、信号收不到,docker stop只能等满 10 秒强杀。病灶收敛在六根轴上:PID 1 信号问题、shell 包装吞信号、healthcheck 脚本自身卡死、stop 宽限期机制误解、容器内死锁或资源耗尽、restart 策略把卡死放大成雪崩。容器直接崩掉的场景看 docker 容器段错误速查。
本文要回答的问题
- 为什么
docker stop总是等满 10 秒才结束,容器在等什么? - 「主进程必须是 PID 1」到底影响什么?
CMD ["sh", "-c", "./app"]这种写法为什么会导致信号丢失?- healthcheck 一直 unhealthy,但进容器手动执行检查命令又是通的?
- 容器卡死后
restart: always为什么反而让故障更难恢复?
一、报错原文与典型现象
卡死类问题没有单一报错原文,典型现象是三条线(路径以 /proj/demo 占位):
# 信号链断裂——stop 等了 10 秒才靠 SIGKILL 收场:
$ time docker stop demo-app
demo-app
real 0m10.3s # 等满默认宽限期,说明 SIGTERM 没被处理# 健康检查挂起——容器在跑但状态 unhealthy:
$ docker ps
CONTAINER ID IMAGE STATUS PORTS NAMES
7c2b9e4a1d05 myapp Up 2 hours (unhealthy) demo-app
$ docker inspect demo-app --format '{{json .State.Health}}'
{"Status":"unhealthy","FailingStreak":12,
"Log":[{"ExitCode":-1,"Output":"Health check exceeded timeout (10s)"}]}# 请求无响应——端口在监听但连接挂起:
$ curl --max-time 5 http://localhost:8080/healthz
curl: (28) Operation timed out after 5001 milliseconds读现象抓三个信号:docker stop 耗时——秒回 = 信号链正常,等满 10 秒 = SIGTERM 没人接;Health.Log 里的 ExitCode: -1 加 exceeded timeout = 检查脚本自己卡死了,不是应用不通;curl 超时但端口在监听 = 应用接受连接但不处理,典型的死锁或线程池打满。
二、病因处置速查表
| 报错现象 / 场景 | 可能原因 | 快速验证 | 处置 |
|---|---|---|---|
| docker stop 固定等 10 秒 | 主进程不处理 SIGTERM | docker top 看 PID 1 是谁 | 入口用 exec 直起进程或加 tini |
| PID 1 是 /bin/sh | shell 包装吞掉了信号 | 同上,CMD 是 shell 形式 | 改 exec 形式 CMD ["./app"] |
| healthcheck ExitCode -1 | 检查脚本自身卡死/超时太短 | inspect 看 Health.Log | 脚本加 timeout,拉长 interval |
| 检查时通、健康状态 unhealthy | 检查依赖的端点受负载影响 | 手动跑检查命令计时 | 健康端点与业务线程池隔离 |
| 端口监听但请求挂起 | 线程池打满/死锁/GC 长停 | exec 进容器看线程与负载 | 抓线程栈,扩容或修死锁 |
| stop 后数据损坏 | 宽限期不够,优雅退出没走完 | 应用日志看 shutdown 是否完整 | --stop-timeout 加大 + 应用侧提速 |
| 卡死后反复重启 | restart always 拉起同样卡死的实例 | events 看 restart 循环 | 改 on-failure + 健康检查联动 |
| 多容器连环卡死 | 上游卡死拖垮下游调用方 | 看调用链超时配置 | 下游设超时与熔断 |
三、排查决策图
下面这张图是标准排查路线,颜色按角色区分:红是报错入口与命中的根因,蓝是排查动作,绿是通过分支,橙是修复动作,紫是防复发沉淀。
读这张图抓住排查顺序:先看信号链(docker top),再看健康检查(inspect .State.Health),最后才怀疑应用内部。卡死类问题八成收在前两站——信号链与检查脚本都是「容器配置层」的病,改 Dockerfile 就好;只有这两站都正常,才需要 dive 进应用看线程与资源。
四、逐条排查展开
PID 1 的信号特殊性是万恶之源。 Linux 对 PID 1 有特权:没有显式安装 handler 的信号,内核不给它默认动作——别的进程收到 SIGTERM 默认退出,PID 1 收到后直接忽略。所以容器主进程如果是个没写信号处理的程序,docker stop 发出的 SIGTERM 石沉大海,只能等满宽限期由 SIGKILL 收场。先验证:
docker top demo-app
# 第一行 PID 列对应的 COMMAND:是 ./app 本体,还是 /bin/sh -c 开头?shell 形式的 CMD 是信号丢失的具体形态。 CMD ./start.sh 或 CMD ["sh", "-c", "./app"] 会让 PID 1 是 shell、应用是它的子进程——shell 不转发信号,应用永远收不到 SIGTERM。处置两条路:Dockerfile 改用 exec 形式 CMD ["./app"] 让应用直接当 PID 1(前提应用自己处理 SIGTERM);或者在入口脚本里用 exec ./app 替换 shell 进程,或在镜像里内置 tini 这类微型 init:ENTRYPOINT ["tini", "--", "./app"],由它转发信号并收割僵尸子进程。
docker stop 的强杀链要会算时间。 默认流程:发 SIGTERM → 等 10 秒 → 发 SIGKILL。应用做优雅退出(关连接、落盘、flush 队列)需要更久时,用 --stop-timeout 30 或 compose 的 stop_grace_period 拉长;但别无限拉——宽限期是把双刃剑,发布时容器半天不退出会拖垮滚动更新节奏。正路是应用侧把优雅退出做快,宽限期只是兜底。
healthcheck 卡死的三张面孔。 一是脚本自身卡死:检查命令里 curl 没加 --max-time,下游慢一次就挂住整个检查,ExitCode: -1 加 exceeded timeout 就是铁证——检查命令一律自带超时且短于 healthcheck 的 timeout。二是检查端点与业务共用线程池:业务打满时健康端点也排不上队,应用其实活着——健康端点要独立线程或独立端口。三是 interval/timeout/start_period 配比失当:启动慢的 Java 应用没给够 start_period,容器还没就绪就被判 unhealthy 连环重启。
容器内死锁与资源耗尽的确认姿势。 信号链与健康检查都正常、请求仍挂起时,进容器看内部:
docker exec demo-app sh -c 'top -b -n1 | head -20' # CPU 打满还是全 idle
docker stats demo-app --no-stream # 内存是否顶到 limitCPU 100% 多半是自旋/死循环;全 idle 但请求挂起多半是锁等待或连接池耗尽——Java 应用这时 jstack、Go 应用 pprof goroutine 页,线程栈会直接指认凶手。内存顶到 limit 未必 OOM 被杀,也可能先陷入疯狂 GC 的「假卡死」。
restart 策略别替卡死背锅也别放大它。 restart: always 对「进程退出」有效,对「进程活着但卡死」完全无用——Docker 只盯进程在不在,不管它干不干活。正确联动是:healthcheck 判 unhealthy → 编排层(compose 的 depends_on: condition: service_healthy、k8s 的 livenessProbe)据此重启。裸 restart: always 配卡死型故障,表现是容器从不重启、故障永远挂着,比崩溃更难被发现——监控要盯 unhealthy 状态本身,不只是盯退出。
五、规范做法与预防
- 入口一律 exec 形式:Dockerfile 禁 shell 形式 CMD;必须用脚本就
exec结尾或内置 tini。 - 应用显式处理 SIGTERM:优雅退出流程(拒新请求→排干在途→释放资源)写进框架模板。
- 健康检查三件套:检查命令自带超时、健康端点独立线程池、start_period 按应用启动实测配。
- stop 宽限期与应用匹配:优雅退出实测耗时 + 50% 裕量写进 compose/k8s 配置。
- 监控盯 unhealthy:告警规则覆盖「Up 但 unhealthy」与「stop 耗时异常」两个信号。
- 压测含 kill 演练:CI 里跑「负载下 docker stop」用例,验证退出干净且数据不丢。
这一篇在知识体系里的位置
本篇是 docker 排错速查簇的运行期卡死篇,定位「进程活着但不干活」:进程直接崩掉(退出码非零)看 docker 容器段错误速查 与 docker 容器缺动态库速查;卡死表现为端口连不上时与 docker 容器端口连接被拒速查 互参(拒与挂起是两种语义);构建/启动期的路径与权限问题看 docker 挂载与 COPY 路径不存在速查 与 docker 权限被拒速查。PID 1 信号机制、exec 形式入口、健康检查三件套,是容器生命周期治理的通用底盘。
一句话总结
docker 容器卡死按「信号链 → 健康检查 → 应用内部」顺序排:先 docker top 看 PID 1——是 shell 包装就改 exec 形式 CMD 或内置 tini,这是 docker stop 固定等满 10 秒的头号根因(PID 1 默认忽略无 handler 的 SIGTERM);再 inspect .State.Health——ExitCode: -1 加 exceeded timeout 是检查脚本自己卡死,检查命令一律自带超时、健康端点与业务线程池隔离;两站都正常才进容器看线程栈与资源。restart: always 治不了「活着但卡死」,监控要盯 unhealthy 状态本身。入口 exec 化、SIGTERM 显式处理、stop 宽限期按优雅退出实测配,这类卡死即可基本绝迹。