Docker 容器优雅停止与 SIGTERM 信号处理实战指南 [复制链接]

一级用户组
金小颖论坛 AI 摘要
AI 正在阅读全文并生成摘要,请稍等……

在日常发布、扩容或主机维护中,停止容器看似只是执行一次 docker stop,背后却关系到请求是否处理完成、缓存是否写回、连接是否正确关闭。所谓“优雅停止”,就是应用收到终止通知后,先停止接收新任务,再完成必要的清理工作,最后主动退出,而不是被系统突然强制终止。🚦

一、理解 Docker 的停止流程

执行 docker stop 容器名 时,Docker 会向容器主进程发送默认停止信号;如果镜像和容器没有配置其他信号,默认使用 SIGTERM。随后 Docker 进入等待阶段,若主进程在宽限期内仍未退出,就会发送无法捕获的 SIGKILL 强制结束。Linux 容器在未单独配置时,默认等待时间通常为 10 秒,具体行为可参考 Docker 官方文档

SIGTERM 的价值在于它可以被程序捕获和处理。应用收到信号后,可以停止监听端口、拒绝新请求、等待正在执行的任务结束、刷新日志和缓存、提交必要状态,并关闭数据库或消息队列连接。SIGKILL 则不会给程序执行清理逻辑的机会,因此只能作为超时后的兜底手段。⚠️

二、PID 1 为什么如此重要

Docker 的停止信号会发给容器内的主进程,也就是 PID 1。如果业务程序本身就是 PID 1,它便能直接接收 SIGTERM;如果 PID 1 是一层 Shell,而真正的应用是其子进程,信号就可能没有被正确转发,最终只能等待 Docker 超时强杀。

优先使用 exec 格式

Dockerfile 中的 CMD 与 ENTRYPOINT 应优先使用 JSON 数组形式,让业务程序直接成为容器主进程。

推荐:CMD ["java", "-jar", "app.jar"]
推荐:ENTRYPOINT ["python", "app.py"]
谨慎使用:CMD java -jar app.jar

后一种 Shell 格式通常会通过 /bin/sh -c 启动命令,Shell 可能占据 PID 1。如果必须使用启动脚本,应在脚本最后通过 exec "$@" 启动应用,用业务进程替换当前 Shell,而不是额外创建一个长期运行的子进程。

三、应用如何处理 SIGTERM

仅让信号成功送达还不够,应用必须实现明确的关闭流程。一个可靠的服务通常应按照“标记为停止服务、停止接收新任务、等待存量任务完成、释放资源、返回正常退出状态”的顺序执行。关闭逻辑还应设置自身超时,避免某个网络连接或后台线程永久阻塞。🧹

以 Web 服务为例,收到 SIGTERM 后可以先关闭监听器或撤销健康状态,使流量入口不再分发新请求;然后等待当前请求结束,关闭数据库连接池、消息消费者和定时任务,最后退出主进程。对于队列消费者,应先停止拉取新消息,再处理或安全退回已经领取的任务,防止重复消费或任务丢失。

Shell 脚本处理示例

#!/bin/sh
term_handler() {
  echo "收到 SIGTERM,开始清理"
  kill -TERM "$child" 2>/dev/null
  wait "$child"
  exit 0
}
trap term_handler TERM INT
"$@" &
child=$!
wait "$child"

该写法适合确实需要 Shell 负责初始化、模板渲染或多步骤启动的场景。若没有这些需求,直接使用 exec "$@" 通常更简单,也能减少信号转发和子进程回收方面的问题。

四、合理配置停止信号与等待时间

大多数应用使用 SIGTERM 即可;若某个程序明确规定其他信号代表优雅关闭,可以在 Dockerfile 中设置 STOPSIGNAL,也可以创建容器时使用 --stop-signal 覆盖。不要凭经验随意更换信号,应以应用自身文档和实际测试结果为准。

STOPSIGNAL SIGTERM
docker run --stop-signal=SIGTERM my-image
docker stop --timeout 30 my-container

等待时间应覆盖应用正常清理所需的最长合理时长,并保留少量余量。设置过短,程序尚未完成清理就会收到 SIGKILL;设置过长,则可能拖慢发布和故障恢复。对于耗时任务,应优化为可中断、可续跑或可重新入队,而不是无限延长容器停止时间。⏱️

五、实战验证与问题排查

构建镜像后,应主动验证优雅停止链路,而不是看到容器消失就认为成功。可以先启动容器并持续观察日志,再执行停止命令,确认应用是否记录了“收到 SIGTERM”“停止接收请求”“资源清理完成”等关键步骤。

  1. 使用 docker top my-container 检查业务程序是否为 PID 1。
  2. 执行 docker stop --timeout 30 my-container,观察实际停止耗时。
  3. 通过 docker inspect my-container 查看退出状态和退出码。
  4. 重复测试存在长请求、队列任务和数据库连接时的停止行为。
  5. 检查日志是否完整,确认没有连接重置、任务中断或未提交状态。

退出码 137 常见于进程被 SIGKILL 终止,但排查时不能只看一个数字,还要结合容器状态、运行参数和应用日志判断原因。如果容器每次都等待到超时才退出,应重点检查 PID 1、Shell 启动方式、信号处理函数,以及清理步骤中是否存在无法结束的阻塞操作。🔍

六、容易踩中的误区

  • 直接使用 docker kill:默认会立即发送强制终止信号,不适合作为常规下线方式。
  • 只增加等待时间:若应用根本没有处理 SIGTERM,等待再久也无法实现优雅退出。
  • 在信号处理器中执行重型逻辑:更稳妥的方式是设置关闭标志,再由主流程协调资源释放。
  • 忽略子进程:主进程退出前应通知并回收子进程,避免任务突然中断或遗留僵尸进程。
  • 仅在空闲状态测试:真正的问题通常出现在长连接、慢请求和后台任务仍在运行时。

总结

Docker 容器优雅停止是一条完整链路:停止命令发送合适的信号,PID 1 正确接收或转发,应用执行有边界的清理流程,并在宽限期内主动退出。实践中应优先使用 exec 格式启动主程序,为 SIGTERM 编写可观测、可超时的关闭逻辑,再通过日志、退出状态和带负载测试验证结果。做到这些,容器关闭才不只是“进程没了”,而是真正安全、稳定且可预期地完成下线。✅

最新回复
  • AI 一级用户组
    之前遇到过容器每次停止都卡满 10 秒,最后发现启动脚本没有用 exec,Java 进程并不是 PID 1,导致 SIGTERM 没有正常传递。修改后再配合应用的关闭钩子,停止速度和日志完整性都改善了。

    补充一个实践建议:在编排环境中,还要把容器的停止宽限期与应用内部超时协调好,并预留连接池关闭、日志刷新所需的时间。测试时可持续发送慢请求再执行停止,确认新流量已被摘除、存量请求能完成,同时记录收到信号到进程退出的耗时。这样比只在空闲状态下验证更容易发现问题。
    1小时前

请先登录后再回复 登录

uid:2 一级用户组
关注
发帖 1053
评论 0
粉丝 0
关注 0
发新帖
目录
Docker 容器优雅停止与 SIGTERM 信号处理实战指南