15.4.5 容器进程与信号处理

容器中的进程模型以 PID 1 为核心,负责接收并处理信号。多数基础镜像以应用进程直接作为 PID 1 运行,若应用未正确处理信号,可能导致无法优雅退出或产生僵尸进程。因此需要明确容器内进程树、信号传递路径与退出策略。

进程与信号的原理草图(PID 1 作为信号与回收中心):

文章图片

常用命令与含义(配合示例更易理解):
- docker top <container>:查看容器内进程树(宿主机视角)
- docker exec <container> ps -ef:容器内查看完整进程列表
- docker kill --signal=SIGTERM <container>:发送优雅退出信号
- docker kill --signal=SIGKILL <container>:强制终止
- docker stop -t <seconds> <container>:等待超时后强杀
- docker restart <container>:依赖应用对 SIGTERM/SIGINT 的处理

示例:构建一个能处理 SIGTERM 的容器

# 1) 创建示例目录
mkdir -p ~/demo/signal-app
cd ~/demo/signal-app

# 2) 编写应用脚本(演示信号处理与子进程回收)
cat > app.sh <<'EOF'
#!/usr/bin/env bash
set -e
echo "PID=$$"

# 启动一个子进程(模拟后台任务)
sleep 300 &
child=$!

# 处理 SIGTERM/SIGINT,优雅退出
trap 'echo "收到退出信号,清理资源..."; kill -TERM "$child"; wait "$child"; echo "已退出"; exit 0' TERM INT

# 主进程持续运行
while true; do
  echo "工作中..."; sleep 5
done
EOF
chmod +x app.sh

# 3) 编写 Dockerfile,引入 tini 作为 PID 1
cat > Dockerfile <<'EOF'
FROM ubuntu:22.04
RUN apt-get update && apt-get install -y tini bash && rm -rf /var/lib/apt/lists/*
WORKDIR /app
COPY app.sh /app/app.sh
ENTRYPOINT ["tini","--"]
CMD ["/app/app.sh"]
EOF

# 4) 构建与运行
docker build -t signal-app:1.0 .
docker run -d --name signal-demo signal-app:1.0

验证进程与信号转发:

# 查看进程树:tini 作为 PID 1
docker exec signal-demo ps -ef

# 发送 SIGTERM,观察日志
docker kill --signal=SIGTERM signal-demo
docker logs -f signal-demo

预期效果:
- ps -ef 中 PID 1 为 /usr/bin/tini -- /app/app.sh
- docker kill --signal=SIGTERM 后日志出现“收到退出信号,清理资源...已退出”
- 容器状态从 running 变为 exited,退出码为 0

常见问题与排错步骤(含命令):
1. 容器停止缓慢
- 排查应用是否处理 SIGTERM:
bash docker stop -t 5 signal-demo docker logs signal-demo | tail -n 20
- 若无“清理资源”日志,说明应用未处理信号。

  1. 产生僵尸进程
    - 查看进程状态:
    bash docker exec signal-demo ps -o pid,ppid,stat,cmd
    - 若看到 Z 状态,说明 PID 1 未回收子进程,可引入 tinidumb-init

  2. 信号不生效
    - 检查入口脚本是否转发信号:
    bash docker inspect signal-demo --format '{{.Config.Entrypoint}} {{.Config.Cmd}}'
    - 若 PID 1 是 /bin/sh -c,需改为 ENTRYPOINT ["tini","--"]exec 方式启动应用。

安装与替换建议:
- 若基础镜像是 Alpine,可安装 tini

# Dockerfile 中
RUN apk add --no-cache tini
ENTRYPOINT ["tini","--"]
  • 或使用官方 --init 选项(自动启用 tini):
docker run --init -d --name signal-demo signal-app:1.0

运维实践要点:
1. 保证 PID 1 可转发信号、可回收子进程。
2. 应用实现 SIGTERM/SIGINT 处理,优雅关闭连接与刷盘。
3. 设置合理停止超时:docker stop -t 30 <container>,避免强杀数据不一致。
4. 多进程容器中明确主进程对子进程的生命周期管理。

练习:
1. 去掉 tini 后重新构建镜像,观察 ps -ef 中 PID 1 的变化,并对比 SIGTERM 的处理效果。
2. 修改 app.sh 使其不处理 SIGTERM,观察 docker stop 的行为与退出码变化。
3. 在容器中启动多个子进程,模拟僵尸进程场景,使用 ps -o stat 识别并解决。