Kubernetes Pod SHELL传递信号

Posted by     "Pyker" on Monday, May 17, 2021

TOC

背景

在 Kubernetes 中,Pod 停止时 kubelet 会先给容器中的主进程发 SIGTERM 信号来通知进程进行 shutdown 以实现优雅停止,如果超时进程还未完全停止则会使用 SIGKILL 来强行终止。

但有时我们会遇到一种情况: 业务逻辑处理了 SIGTERM 信号,但 Pod 停止时好像没收到信号导致优雅停止逻辑不生效。

通常是因为我们的业务进程是在脚本中启动的,容器的启动入口使用了脚本,所以容器中的主进程并不是我们所希望的业务进程而是 shell 进程,比如下面的 Dockerfile:

FROM centos:7
ADD start.sh /
...
CMD ["/start.sh"]

在 start.sh 脚本中运行二进制以启动业务进程:

#! /bin/bash
...

/bin/yourapp # 脚本中执行二进制

当 Pod 停止时,kubelet 发送 SIGTERM 信号给容器主进程,即 shell 进程,但 shell 进程并没有自动传递信号给子进程的能力,导致我们业务进程收不到信号,从而无法实现优雅停止,只能一直等到 terminationGracePeriodSeconds 超时时间 (默认 30s) 被强制杀死。

使用 exec 启动

在 shell 中启动二进制的命令前加一个 exec 即可让该二进制启动的进程代替当前 shell 进程,即让新启动的进程成为主进程:

#! /bin/bash
...

exec /bin/yourapp # 脚本中执行二进制

然后业务进程就可以正常接收所有信号了,实现优雅退出也不在话下。

多进程场景: 使用 trap 传递信号

通常我们一个容器只会有一个进程,也是 Kubernetes 的推荐做法。但有些时候我们不得不启动多个进程,比如从传统部署迁移到 Kubernetes 的过渡期间,使用了富容器,即单个容器中需要启动多个业务进程,这时也只能通过 shell 启动,但无法使用上面的 exec 方式来传递信号,因为 exec 只能让一个进程替代当前 shell 成为主进程。

这个时候我们可以在 shell 中使用 trap 来捕获信号,当收到信号后触发回调函数来将信号通过 kill 传递给业务进程,脚本示例:

#! /bin/bash

/bin/app1 & pid1="$!" # 启动第一个业务进程并记录 pid
echo "app1 started with pid $pid1"

/bin/app2 & pid2="$!" # 启动第二个业务进程并记录 pid
echo "app2 started with pid $pid2"

handle_sigterm() {
  echo "[INFO] Received SIGTERM"
  kill -SIGTERM $pid1 $pid2 # 传递 SIGTERM 给业务进程
  wait $pid1 $pid2 # 等待所有业务进程完全终止
}
trap handle_sigterm SIGTERM # 捕获 SIGTERM 信号并回调 handle_sigterm 函数

wait # 等待回调执行完,主进程再退出

附上信号表:

$ kill -l
 1) SIGHUP	 2) SIGINT	 3) SIGQUIT	 4) SIGILL	 5) SIGTRAP
 6) SIGABRT	 7) SIGBUS	 8) SIGFPE	 9) SIGKILL	10) SIGUSR1
11) SIGSEGV	12) SIGUSR2	13) SIGPIPE	14) SIGALRM	15) SIGTERM
16) SIGSTKFLT	17) SIGCHLD	18) SIGCONT	19) SIGSTOP	20) SIGTSTP
21) SIGTTIN	22) SIGTTOU	23) SIGURG	24) SIGXCPU	25) SIGXFSZ
26) SIGVTALRM	27) SIGPROF	28) SIGWINCH	29) SIGIO	30) SIGPWR
31) SIGSYS	34) SIGRTMIN	35) SIGRTMIN+1	36) SIGRTMIN+2	37) SIGRTMIN+3
38) SIGRTMIN+4	39) SIGRTMIN+5	40) SIGRTMIN+6	41) SIGRTMIN+7	42) SIGRTMIN+8
43) SIGRTMIN+9	44) SIGRTMIN+10	45) SIGRTMIN+11	46) SIGRTMIN+12	47) SIGRTMIN+13
48) SIGRTMIN+14	49) SIGRTMIN+15	50) SIGRTMAX-14	51) SIGRTMAX-13	52) SIGRTMAX-12
53) SIGRTMAX-11	54) SIGRTMAX-10	55) SIGRTMAX-9	56) SIGRTMAX-8	57) SIGRTMAX-7
58) SIGRTMAX-6	59) SIGRTMAX-5	60) SIGRTMAX-4	61) SIGRTMAX-3	62) SIGRTMAX-2
63) SIGRTMAX-1	64) SIGRTMAX

完美方案: 使用 init 系统

前面一种方案实际是用脚本实现了一个极简的 init 系统 (或 supervisor) 来管理所有子进程,只不过它的逻辑很简陋,仅仅简单的透传指定信号给子进程,其实社区有更完善的方案,dumb-init 和 tini 都可以作为 init 进程,作为主进程 (PID 1) 在容器中启动,然后它再运行 shell 来执行我们指定的脚本 (shell 作为子进程),shell 中启动的业务进程也成为它的子进程,当它收到信号时会将其传递给所有的子进程,从而也能完美解决 SHELL 无法传递信号问题,并且还有回收僵尸进程的能力。

这里以 dumb-init 为例制作镜像,下面是 Dockerfile 示例:

dumb-init github: https://github.com/Yelp/dumb-init

FROM ubuntu:latest
RUN apt-get update && apt-get install -y dumb-init
ADD start.sh /
ADD app1 /bin/app1
ADD app2 /bin/app2
ENTRYPOINT ["dumb-init", "--"]
CMD ["/start.sh"]

start.sh 脚本示例:

#! /bin/bash
/bin/app1 &
/bin/app2 &
wait

参考资料

「真诚赞赏,手留余香」

云原生与运维

真诚赞赏,手留余香

使用微信扫描二维码完成支付


comments powered by Disqus