Bash脚本中SIGINT可即时捕获但SIGTERM延迟响应的问题
问题描述
在CI流水线中用Bash脚本封装Docker容器运行Ansible命令时,遇到任务中止(如收到SIGTERM信号)的场景,脚本无法及时响应,要等容器内命令执行完毕才触发清理逻辑,此时容器已停止,会抛出Cannot kill container错误,且存在容器失控继续运行的风险。
当前使用的脚本:
#!/bin/bash CONTAINER=ansible function kill_container() { echo "$0 caught $1" >&2 docker kill ${CONTAINER} exit $? } trap 'kill_container SIGINT' SIGINT trap 'kill_container SIGTERM' SIGTERM function ansible_base() { docker run -d --rm --name ${CONTAINER} someorg/ansible:latest $@ docker logs --follow ${CONTAINER} } ansible_base $@
测试复现:
- 执行
./run.sh sleep 30,在另一个终端用pkill run.sh发送SIGTERM,脚本会等待30秒后才处理信号,此时容器已停止,报错容器未运行。 - CI环境中表现一致:进程收到SIGTERM后30秒无响应,被系统发送SIGKILL,脚本终止但Docker容器仍在运行。
问题原因
核心问题出在docker logs --follow命令上:当脚本执行到该命令时,Bash会将其作为前台进程运行,此时SIGTERM信号会被docker logs进程捕获,但它默认不会转发信号或主动退出,导致脚本的trap逻辑无法及时触发,直到docker logs因容器停止而退出后,脚本才会处理之前收到的SIGTERM信号。
解决方案
提供两种可行的修复方案,可根据场景选择:
方案1:优化后台日志跟踪逻辑
通过将docker logs后台运行+主进程wait的方式,让脚本主进程能及时响应信号,同时增加容器状态检查避免错误:
#!/bin/bash CONTAINER=ansible function cleanup() { local SIGNAL=$1 echo "$0 caught ${SIGNAL}" >&2 # 先检查容器是否仍在运行,再执行kill操作 if docker ps --filter "name=${CONTAINER}" --format "{{.Names}}" | grep -q "${CONTAINER}"; then docker kill "${CONTAINER}" local KILL_EXIT=$? exit ${KILL_EXIT} fi exit 0 } # 覆盖更多中止信号场景 trap 'cleanup SIGINT' SIGINT trap 'cleanup SIGTERM' SIGTERM trap 'cleanup SIGQUIT' SIGQUIT function ansible_base() { # 启动容器并后台运行 docker run -d --rm --name "${CONTAINER}" someorg/ansible:latest "$@" # 后台跟踪日志并记录进程PID docker logs --follow "${CONTAINER}" & LOGS_PID=$! # 主进程等待日志进程,此时能及时捕获信号 wait ${LOGS_PID} } ansible_base "$@"
关键改动说明:
- 用
"$@"替代$@,避免参数含空格时出现解析错误; - 将
docker logs后台运行,主进程通过wait等待,确保信号能被脚本主进程及时捕获; - 清理函数中先检查容器状态,避免容器已停止时的kill报错。
方案2:前台运行容器(更简洁)
放弃-d后台启动容器和docker logs跟踪,直接前台运行容器,配合--init参数确保信号能正确转发到容器内进程:
#!/bin/bash CONTAINER=ansible function cleanup() { local SIGNAL=$1 echo "$0 caught ${SIGNAL}" >&2 docker stop "${CONTAINER}" exit $? } trap 'cleanup SIGINT' SIGINT trap 'cleanup SIGTERM' SIGTERM function ansible_base() { # 前台运行容器,--init确保容器内进程能正确接收信号 docker run --rm --name "${CONTAINER}" --init someorg/ansible:latest "$@" } ansible_base "$@"
方案优势:
- 脚本逻辑更简单,无需处理后台进程和日志跟踪;
--init参数让Docker启动init进程作为容器PID 1,确保容器内进程能正确响应SIGTERM等信号,避免僵尸进程或不响应中止命令的情况;docker stop会先向容器发送SIGTERM等待优雅停止,超时后发送SIGKILL,符合标准的中止流程。
内容的提问来源于stack exchange,提问作者Sammitch
相关产品推荐
相关产品推荐

