容器收不到 SIGTERM 速查:docker stop 等满 10 秒强杀的分层排查
更新时间:2026-10-24。FAQ 速查页,面向「
docker stop每次都要卡满 10 秒、日志里应用根本没有 shutdown 记录、数据库类容器隔三差五起不来要做崩溃恢复」的同学。容器收不到 SIGTERM 的本质一句话:docker stop只给容器的 PID 1 发 SIGTERM,而大多数「收不到」的真相是 PID 1 根本不是你的应用——是个 shell、脚本或包装进程,信号到它为止、传不下去,10 秒宽限期一到,dockerd 直接 SIGKILL 全场。病灶收敛在六根轴上。容器直接崩溃退出的看 docker 段错误崩溃速查;停不下来的僵死看 docker 超时无响应速查。
本文要回答的问题
docker stop到底做了什么,10 秒宽限期从哪来、怎么改?- ENTRYPOINT 的 shell 形态和 exec 形态差在哪,为什么一个传信号一个不传?
sh -c "myapp"为什么收不到信号,exec一个词的差别是什么?- 应用注册了 SIGTERM 处理器,为什么日志里还是看不到优雅退出?
- compose 里的
stop_grace_period配多少合适,数据库类应用怎么处理?
一、报错原文与典型现象
收不到信号的指纹不是报错,是「该快的不快、该有的没有」——stop 卡满 10 秒、应用无 shutdown 日志(路径以 /proj/demo 占位,镜像源用 registry.example.com 占位):
# 指纹一:stop 卡满默认 10 秒宽限期 —— SIGTERM 没人理,最后 SIGKILL 收场
$ time docker stop app
app
real 0m10.2s# 指纹二:PID 1 是 shell 不是应用 —— 信号发给 sh,sh 不转发
$ docker exec app ps -ef
PID USER COMMAND
1 root sh -c /usr/local/bin/myapp --config /etc/app.yaml
8 root /usr/local/bin/myapp --config /etc/app.yaml# 指纹三:容器日志里没有 shutdown 字样 —— 应用从未被通知
$ docker logs app --tail 20
[info] serving requests...
[info] serving requests... # 然后就没有然后了,被 SIGKILL 直接抹掉读现象抓三条判据:先 docker exec <容器> ps -ef 看 PID 1 是谁——是 shell 或包装脚本,信号传递链断在第一环;stop 耗时精确等于宽限期(默认 10 秒)说明 SIGTERM 完全无效;凡是「数据库容器重启后总要做 crash recovery」的,几乎都能追到死等强杀这条链上。
二、病因处置速查表
| 报错现象 / 场景 | 可能原因 | 快速验证 | 处置 |
|---|---|---|---|
| stop 卡满 10 秒 | PID 1 是 shell 不转发信号 | exec 进容器 ps -ef | ENTRYPOINT 改 exec 形态 |
| sh -c 启动收不到 | 子 shell 不转发信号给子进程 | ps 看 sh 与 app 两个进程 | 命令前加 exec 替换 shell |
| 应用是 PID 8 而非 1 | 启动脚本包了多层 | ps 看进程树层级 | 脚本最后一行 exec 应用 |
| 有 PID 1 仍不退出 | 应用未注册 SIGTERM 处理器 | 看应用代码 signal 处理 | 注册处理器做优雅退出 |
| 处理器有但仍被强杀 | 清理耗时超宽限期 | 看 stop 日志时间差 | 加大 stop_grace_period |
| 应用明确忽略 SIGTERM | 代码里 SIG_IGN 或框架吞掉 | strace 看信号投递 | 改代码响应 SIGTERM |
| compose stop 仍旧慢 | stop_grace_period 没生效 | docker inspect 看配置 | 服务级配置后重建容器 |
三、排查决策图
这张图是标准排查路线:红是收不到信号的现象与根因,蓝是排查动作,绿是验证通过,橙是修复,紫是防复发沉淀。
读这张图抓住关键一步:信号问题先看进程树,后看代码——PID 1 不对,改什么代码都白搭;PID 1 对了再查应用有没有注册处理器、清理时间有没有超过宽限期。三层都通了,优雅退出才是完整的。
四、逐条排查展开
shell 形态 ENTRYPOINT 是信号丢失的第一大坑。 Dockerfile 的 ENTRYPOINT/CMD 有两种写法,行为天差地别:
# shell 形态:docker 实际跑的是 sh -c "myapp",PID 1 是 sh,信号到此为止
ENTRYPOINT myapp --config /etc/app.yaml
# exec 形态:myapp 直接是 PID 1,SIGTERM 直达应用
ENTRYPOINT ["myapp", "--config", "/etc/app.yaml"]shell 形态下 docker stop 的 SIGTERM 发给 sh,sh 收到后默认行为是等子进程结束、自己不退出也不转发——应用从头到尾不知道有人要它停。验证方法永远是 ps -ef 看 PID 1。
启动脚本里差一个 exec,信号就断。 很多镜像用 entrypoint.sh 做初始化(生成配置、等待依赖),最后一行启动应用:
#!/bin/sh
render_config.sh
myapp --config /etc/app.yaml # 错:myapp 是 sh 的子进程
exec myapp --config /etc/app.yaml # 对:exec 用应用替换 shell,应用成为 PID 1exec 让应用进程替换掉 shell 进程本身,PID 1 直接是应用,信号通路打通。这一个词是 entrypoint 脚本的生死线。
应用是 PID 1,还得它会理 SIGTERM。 第二个条件是应用注册了信号处理器:收到 SIGTERM 后停止接收新请求、等存量请求处理完、刷盘、关连接池。Python 里就是 signal.signal(signal.SIGTERM, handler);Go 里 signal.Notify。没注册处理器的进程收到 SIGTERM 的默认动作是直接退出——倒不算坏事(至少 stop 秒回),但做不到「优雅」;注册了但处理器里死循环或忘记退出的,stop 照样卡满宽限期。
清理需要时间,宽限期要给够。 默认 10 秒对 web 服务够用,对数据库远远不够——PostgreSQL、MySQL 做一次干净关库要刷脏页、写检查点,几十秒很正常。compose 里这样配:
services:
db:
image: registry.example.com/team/postgres:16
stop_grace_period: 60sdocker run 对应 --stop-timeout 60。改完注意:compose 的 stop_grace_period 是容器级配置,改配置后必须 docker compose up -d 重建容器才生效,直接 docker compose stop 用的还是旧值。
该忽略的信号别乱忽略。 个别框架或遗留代码里有 SIG_IGN 把 SIGTERM 整个吞掉,这种进程只能靠 SIGKILL——docker stop -t 0 直接强杀是运维兜底,但代码里的 SIG_IGN 要当 bug 修。排查工具:strace -e signal -p 1 在容器里看信号投递,确认信号到了、是应用不理。
docker stop 的完整流程值得背下来。 第一步 dockerd 向容器 PID 1 发 SIGTERM;第二步等宽限期(docker stop 默认 10 秒,docker stop -t 可调);第三步时间一到直接 SIGKILL,SIGKILL 不可捕获不可忽略,进程当场蒸发——缓冲区没刷的数据、没写完的事务、没关的连接,全部按崩溃处理。理解了这条链就明白「等满 10 秒」意味着什么:你的 SIGTERM 在这 10 秒里无人理会,最后被杀掉的方式和断电没有区别。数据库类容器长期这么停,总有一天要在启动时做冗长的崩溃恢复。
确实需要 PID 1 是脚本时,用 init 垫片。 少数场景(要在容器里跑多个进程、或启动前必须做特权初始化)确实没法让应用直接当 PID 1,这时上专用 init:tini 或 dumb-init。tini 作为 PID 1 运行,负责把收到的信号转发给子进程、收割僵尸进程;用法是在 exec 形态 ENTRYPOINT 里包一层:ENTRYPOINT ["tini", "--", "/entrypoint.sh"]。官方镜像(如 jenkins、elasticsearch)基本都内置了 tini,自建镜像可以用 docker run --init 直接让 dockerd 注入 tini,零镜像改动拿到正确的信号转发。
五、规范做法与预防
- exec 形态进镜像模板:ENTRYPOINT 一律 JSON 数组写法,shell 形态在评审中打回。
- entrypoint 脚本最后一行必须 exec:写成模板注释固化,「少了 exec 信号断」。
- 应用必须注册 SIGTERM 处理器:停止接收新请求、存量排空、资源释放,三段齐全。
- 宽限期按应用类型配:web 服务 10 秒,有状态服务 30-60 秒,写进 compose 模板。
- 部署流水线验证优雅退出:发版前跑 stop 计时加日志断言,卡满宽限期即失败。
- 数据库类容器挂载健康检查:避免 crash recovery 发生在业务高峰,起不来先告警。
这一篇在知识体系里的位置
本篇是 docker 排错速查簇的信号与生命周期篇,定位「停不住、停不干净」:容器段错误直接崩看 docker 段错误崩溃速查;容器与命令超时僵死看 docker 超时无响应速查;容器资源耗尽被 OOM 杀看 docker 资源耗尽速查;镜像缺料起不来或退出码异常看 docker 镜像缺依赖速查;容器内时间相关的另一类坑看 docker 容器时区不对速查。exec 形态、信号处理器、宽限期适配,是优雅退出的三要素。
一句话总结
容器收不到 SIGTERM 的规律是「信号只发给 PID 1,链断在 shell 或包装进程」:docker stop 卡满 10 秒先 ps 看 PID 1 是谁,shell 形态 ENTRYPOINT 改 JSON 数组 exec 形态,启动脚本最后一行必须 exec 应用;PID 1 对了再确认应用注册了 SIGTERM 处理器、清理耗时在 stop_grace_period 以内。排查的通式是进程树、处理器、宽限期三层依次过。治本靠 exec 形态镜像模板、entrypoint 脚本 exec 固化、按应用类型配宽限期、发版流水线验证优雅退出,让 docker stop 从「等满强杀」变回「秒回净退」。