“运维就要无所不能,无所不会”
大家好,我是Stanley「史丹利」,今天聊技术:容器优雅关闭方案 。
公司某服务接入效能平台后,发布过程中,页面偶尔会出现5003报错,开始以为是Nacos
没有及时的将服务反注册,即POD
在已经正常关闭的情况下,注册中心依然有POD
信息,请求依然到已经关闭的POD
中导致
5003报错
5003-error-2
2.1 首先找开发同学,协助排查了反注册逻辑及相关日志,没有发现什么异常
2.2 后来偶然发现POD
中的主进程PID不为1,而PID为1的进程为shell进程,这会导致容器关闭时业务进程无法接受k8s发送的SIGTERM信号,只能在等待15秒后被强行杀死
process-shell
2.3 修改了程序启动参数,通过EXEC启动模式,使应用主进程PID为1
process-exec
2.4 重新发布验证,5003报错问题修复
通常Dockerfile中CMD和ENTRYPOINT来启动应用,启动应用有两种模式,shell 模式和 exec 模式,对应的使用 shell 模式,PID 为 1 的进程为 shell,使用 exec 模式 PID 为 1 的进程为业务本身。
SHELL模式
FROM golang as builder
WORKDIR /go/
COPY app.go .
RUN go build app.go
FROM ubuntu
WORKDIR /root/
COPY --from=builder /go/app .
CMD ./app
这种方式构建的镜像应用启动后PID为1的进程是shell进程
EXEC模式
FROM golang as builder
WORKDIR /go/
COPY app.go .
RUN go build app.go
FROM ubuntu
WORKDIR /root/
COPY --from=builder /go/app .
CMD ["./app"]
这种方式构建的镜像应用启动后PID为1的进程是应用进程
在实际生产环境中,因为应用启动命令后会接很多启动参数,所以通常我们会使用一个启动脚本来启动应用,方便我们启动应用。对应的在容器内 PID 为 1 的进程为 shell 进程但 shell 程序不转发 signals,也不响应退出信号。所以在容器应用中如果应用容器中启动 shell,占据了 pid=1 的位置,那么就无法接收 k8s 发送的 SIGTERM
信号,只能等超时后被强行杀死了。启动脚本 start.sh
start.sh
$ cat > start.sh<< EOF
#!/bin/sh
sh -c /root/app
EOF
Dockerfile
FROM golang as builder
WORKDIR /go/
COPY app.go .
RUN go build app.go
FROM alpine
WORKDIR /root/
COPY --from=builder /go/app .
ADD start.sh /root/
CMD ["/bin/sh","/root/start.sh"]
在前面脚本启动的dockerfile
基础上,定义一个优雅关闭的脚本,通过k8s-prestop
在关闭 POD 前调用优雅关闭脚本,实现 pod 优雅关闭。
stop.sh
#!/bin/sh
ps -ef|grep app|grep -v grep|awk '{print $1}'|xargs kill -15
通过 yaml 部署到 k8s 中
stop.sh
apiVersion: apps/v1
kind: Deployment
metadata:
name: app-prestop
labels:
app: prestop
spec:
replicas: 1
selector:
matchLabels:
app: prestop
template:
metadata:
labels:
app: prestop
spec:
containers:
- name: prestop
image: xx/app:v1.0-prestop
lifecycle:
preStop:
exec:
command:
- sh
- /root/stop.sh
修改start.sh脚本
stop.sh
#!/bin/sh
exec ./app
shell 中添加一个 exec 即可让应用进程替代当前 shell 进程,可将 SIGTERM 信号传递到业务层,让业务实现优雅关闭。
使用 dump-init
或 tini
做为容器的主进程,在收到退出信号的时候,会将退出信号转发给进程组所有进程。主要适用应用本身无关闭信号处理的场景。docker –init
本身也是集成的 tini
。
stop.sh
FROM golang as builder
WORKDIR /go/
COPY app.go .
RUN go build app.go
FROM alpine
WORKDIR /root/
COPY --from=builder /go/app .
ADD start.sh tini /root/
RUN chmoad a+x start.sh && apk add --no-cache tini
ENTRYPOINT ["/sbin/tini", "--"]
CMD ["/root/tini", "--", /root/start.sh"]
1、对于容器化应用启动命令建议使用 EXEC 模式。
2、对于应用本身代码层面已经实现了优雅关闭的业务,但有 shell 启动脚本,容器化后部署到 k8s 上建议使方案一和方案二。
3、对于应用本身代码层面没有实现优雅关闭的业务,建议使用方案三。