入口脚本里到底该写 exec 还是 trap?为什么 docker stop 总要等满超时?这篇用同一个会打印信号的 Python 应用,对比不 exec、exec、两种 trap 写法和 --init,并看 HUP 这类非 TERM 信号怎么到达应用。
A 不用 exec(sh 启动 python):docker stop -t 5 用时 5.2 秒;Status=exited ExitCode=137;日志:entrypoint 启动,PID 1 | app 启动,PID 7 B exec python:docker stop -t 5 用时 0.7 秒;Status=exited ExitCode=0;日志:entrypoint 启动,PID 1 | app 启动,PID 1 | 收到信号 SIGTERM | 清理完成,退出 C1 trap 转发 TERM,只 wait 一次:docker stop -t 5 用时 0.1 秒;Status=exited ExitCode=143;日志:entrypoint 启动,PID 1 | app 启动,PID 7 | entrypoint 结束,第一次 wait 返回 143 | 收到信号 SIGTERM C2 trap 转发 TERM,被信号打断后再 wait 一次:docker stop -t 5 用时 0.6 秒;Status=exited ExitCode=0;日志:entrypoint 启动,PID 1 | app 启动,PID 7 | 收到信号 SIGTERM | 清理完成,退出 | entrypoint 结束,python 退出码 0
入口脚本最后一行写 exec 应用,应用取代 sh 成为 PID 1,收到 SIGTERM 直接处理;前提是应用自己处理信号(PID 1 没有默认动作,见 c6)。要保留脚本时用 trap 转发:但 wait 被信号打断时立刻返回 128+信号编号,必须再 wait 一次才能拿到子进程的真正退出码并等它清理完;只写一次 wait 的脚本会比应用先退出,应用被容器终止时直接杀掉(C1)。
A + --init(sh 脚本不 exec):docker stop -t 5 用时 0.1 秒;Status=exited ExitCode=143;日志:entrypoint 启动,PID 7 | app 启动,PID 8 B + --init(exec python):docker stop -t 5 用时 0.6 秒;Status=exited ExitCode=0;日志:entrypoint 启动,PID 7 | app 启动,PID 7 | 收到信号 SIGTERM | 清理完成,退出
--init 让 tini 当 PID 1,它把信号转给它的子进程(这里是 sh);sh 没装 trap,TERM 的默认动作就是退出,孙子进程 python 没有机会清理。--init 的价值在收尸和 PID 1 的特殊信号语义(c31、c32),不能代替入口脚本去转发。
A 不用 exec,发 HUP:docker kill -s HUP 用时 1.1 秒;Status=running ExitCode=0;日志:entrypoint 启动,PID 1 | app 启动,PID 7 B exec python,发 HUP:docker kill -s HUP 用时 1.1 秒;Status=running ExitCode=0;日志:entrypoint 启动,PID 1 | app 启动,PID 1 | 收到信号 SIGHUP C2 trap 只转发 TERM/INT,发 HUP:docker kill -s HUP 用时 1.0 秒;Status=running ExitCode=0;日志:entrypoint 启动,PID 1 | app 启动,PID 7
热加载常用的 HUP:exec 形式直达;trap 脚本要把 HUP 也写进 trap 列表才会转发。