处理SIGTERM信号

一、exec-app.shapp.sh,(两种形式都不建议使用):

  1. 使用如下脚本当中指定需要使用#!/bin/bash+ exec 的形式启动进程
 1[root@3114-db-mysql ubuntu]# cat exec-app.sh
 2#!/bin/bash
 3exec /gosignal
 4
 5# 进入到容器内查看进程结构
 6
 7root@4d40c42c130e:/# ps -auxf
 8USER         PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
 9root          14  1.0  0.0   4628  3528 pts/0    Ss   15:55   0:00 bash
10root          22  0.0  0.0   7064  1588 pts/0    R+   15:55   0:00  \_ ps -auxf
11root           1  0.3  0.0   2892  1000 ?        Ss   15:54   0:00 /bin/sh -c /exec-app.sh
12root           7  0.0  0.0 1225800 2892 ?        Sl   15:54   0:00 /gosignal
13
14# 此时gosignal的进程是脱离于/bin/sh 存在而运行的,PID为7
15
16# 当使用 kill -15 7 时,由于gosignal退出,pid 为1 的进程没有了执行程序,因此容器会退出。
  1. 修改启动脚本exe-app.sh使用#!/bin/sh + exec执行
 1root@826eced13ef9:/# cat exec-app.sh
 2#!/bin/sh
 3exec /gosignal
 4
 5root@826eced13ef9:/# ps -auxf
 6USER         PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
 7root          14  0.5  0.0   4628  3756 pts/0    Ss   17:22   0:00 bash
 8root          22  0.0  0.0   7064  1668 pts/0    R+   17:22   0:00  \_ ps -auxf
 9root           1  0.1  0.0   2892   956 ?        Ss   17:22   0:00 /bin/sh -c /exec-app.sh
10root           7  0.0  0.0 1225288 2888 ?        Sl   17:22   0:00 /gosignal
11# 可以发现此时不会再出bash,1号进程直接执行exec-app.sh脚本,gosignal 程序父进程为1号进程。
12
13[root@3114-db-mysql ubuntu]# docker logs -f 826eced13ef9
14Starting application,Pid is :7, PPid is: 1
15
16# 但是执行# docker stop 826eced13ef9  或者 kill -15 1 的操作时候。 gosignal无法感知到SIGTERM信号。
  1. 启动脚本当中没有exec的参数,但是脚本申明 #!/bin/bash
 1[root@3114-db-mysql ubuntu]# cat app.sh
 2#!/bin/bash
 3/gosignal
 4
 5# 进入到容器内查看进程结构
 6root@c23faa311be9:/# ps -auxf
 7USER         PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
 8root          14  0.5  0.0   4628  3576 pts/0    Ss   16:06   0:00 bash
 9root          22  0.0  0.0   7064  1556 pts/0    R+   16:06   0:00  \_ ps -auxf
10root           1  0.1  0.0   2892   960 ?        Ss   16:06   0:00 /bin/sh -c /app.sh
11root           7  0.0  0.0   4364  3328 ?        S    16:06   0:00 /bin/bash /app.sh
12root           8  0.0  0.0 1225800 2888 ?        Sl   16:06   0:00  \_ /gosignal
13[root@3114-db-mysql ubuntu]# docker logs -f c23faa311be9
14Starting application,Pid is :8, PPid is: 7
15#此时可以看到,exec-app.sh 脚本会新启一个bash 进行执行gosignal,且其自己进程为8,父进程为7
16
17#使用kill -15 8 则直接进程被Terminated ,没有触发gosignal的型号捕捉
18[root@3114-db-mysql ubuntu]# docker logs -f c23faa311be9
19Starting application,Pid is :8, PPid is: 7
20Terminated
  1. 启动脚本当中没有exec的参数,但是脚本申明 #!/bin/sh
 1[root@3114-db-mysql ubuntu]# cat app.sh
 2#!/bin/sh
 3/gosignal
 4
 5# 进程结构与使用bash的方式一样
 6[root@3114-db-mysql gosignal]# docker exec -it 3f1 bash
 7root@3f188daf5b79:/# ps -auxf
 8USER         PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
 9root          15  1.0  0.0   4628  3760 pts/0    Ss   17:34   0:00 bash
10root          23  0.0  0.0   7064  1612 pts/0    R+   17:34   0:00  \_ ps -auxf
11root           1  0.0  0.0   2892   996 ?        Ss   17:34   0:00 /bin/sh -c /app.sh
12root           7  0.0  0.0   2892   952 ?        S    17:34   0:00 /bin/sh /app.sh
13root           8  0.0  0.0 1225544 2892 ?        Sl   17:34   0:00  \_ /gosignal
14
15# 由于gosignal 不是1号进程,因此也无法感知SIGTERM信号

结论:如论使用何种方式进行都无法让gosignal收到 15的停止信号。

二、entrypoint-signal.sh形式

  1. 未启用DockerfileSHELL ["/bin/bash","-c"]参数时,bash不会为1号进程
 1[root@3114-db-mysql gosignal]#cat /entrypoint-signal.sh
 2#!/bin/bash
 3/gosignal & PID1="$!"
 4echo "gosignal started with pid $PID1"
 5handle_sigterm(){
 6        echo "[INFO] Received SIGTERM"
 7        kill -SINTERM $PID1
 8        wait $PID1
 9}
10trap handle_sigterm SIGTERM
11wait
12
13# 进程形式:
14root@17b995db94df:/# ps -auxf
15USER         PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
16root          16  0.6  0.0   4628  3708 pts/0    Ss   16:58   0:00 bash
17root          24  0.0  0.0   7064  1544 pts/0    R+   16:58   0:00  \_ ps -auxf
18root           1  0.0  0.0   2892   964 ?        Ss   16:57   0:00 /bin/sh -c /entrypoint-signal.sh
19root           7  0.0  0.0   4364  3244 ?        S    16:57   0:00 /bin/bash /entrypoint-signal.sh
20root           8  0.0  0.0 1225800 2888 ?        Sl   16:57   0:00  \_ /gosignal
21# 使用docker stop 该进程,无法得到响应,最终被强制删掉

2.启用DockerfileSHELL ["/bin/bash","-c"]参数。

 1[root@3114-db-mysql ubuntu]# cat Dockerfile
 2FROM ubuntu:22.04
 3LABEL "author"="kid"
 4# 注意这里使用了SHELL为BASH,如果不加,则1号进程会变为/bin/sh -c的形式
 5SHELL ["/bin/bash","-c"]
 6ENV TINI_VERSION v0.19.0
 7ADD https://github.com/krallin/tini/releases/download/${TINI_VERSION}/tini /tini
 8RUN chmod +x /tini
 9COPY gosignal/gosignal /gosignal
10COPY entrypoint-signal.sh /entrypoint-signal.sh
11RUN chmod +x /entrypoint-signal.sh
12COPY exec-app.sh /exec-app.sh
13RUN chmod +x /exec-app.sh
14ENTRYPOINT /entrypoint-signal.sh
15
16#此时的1号进程发生了变化,
17root@c82450e19085:/# ps -auxf
18USER         PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
19root          15  0.5  0.0   4628  3688 pts/0    Ss   17:06   0:00 bash
20root          23  0.0  0.0   7064  1608 pts/0    R+   17:06   0:00  \_ ps -auxf
21root           1  0.0  0.0   4364  3120 ?        Ss   17:06   0:00 /bin/bash /entrypoint-signal.sh
22root           7  0.0  0.0 1225800 2892 ?        Sl   17:06   0:00 /gosignal
23
24
25#对1号进程进行执行kill -15 1 或者docker stop c82 的的操作是进程均或感知到SIGTERM的信号。
26[root@3114-db-mysql ubuntu]# docker logs -f c82
27gosignal started with pid 7
28Starting application,Pid is :7, PPid is: 1
29[INFO] Received SIGTERM
30已捕捉到信号:SIGTERM, 执行关闭,sig is : terminated
31Exiting!

三、使用tini工具

  1. Tini作为ENTRYPOINT, gosignal应用程序作为CMD的入参传入
 1[root@3114-db-mysql ubuntu]# cat Dockerfile
 2FROM ubuntu:22.04
 3LABEL "author"="kid"
 4#SHELL ["/bin/bash","-c"]
 5ENV TINI_VERSION v0.19.0
 6ADD https://github.com/krallin/tini/releases/download/${TINI_VERSION}/tini /tini
 7RUN chmod +x /tini
 8COPY gosignal/gosignal /gosignal
 9COPY entrypoint-signal.sh /entrypoint-signal.sh
10RUN chmod +x /entrypoint-signal.sh
11COPY exec-app.sh /exec-app.sh
12RUN chmod +x /exec-app.sh
13ENTRYPOINT ["/tini", "--"]
14CMD ["/gosignal"]
15
16# 容器内进程结构:
17root@91ed8c1f402d:/# ps -auxf
18USER         PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
19root          14  0.6  0.0   4628  3572 pts/0    Ss   17:42   0:00 bash
20root          22  0.0  0.0   7064  1584 pts/0    R+   17:42   0:00  \_ ps -auxf
21root           1  0.0  0.0   2780   996 ?        Ss   17:41   0:00 /tini -- /gosignal
22root           7  0.0  0.0 1225800 2888 ?        Sl   17:41   0:00 /gosignal
23
24# 执行docker stop 或 kill -15 1 此时信号量能够传入到可执行程序当中。
25[root@3114-db-mysql ubuntu]# docker stop tini-without-shell
26tini-without-shell
27
28[root@3114-db-mysql ubuntu]# docker logs -f tini-without-shell
29Starting application,Pid is :7, PPid is: 1
30已捕捉到信号:SIGTERM, 执行关闭,sig is : terminated
31Exiting!
  1. tini作为1号进程,通常不会直接以CMD的形式运行二进制程序,企业在使用过程当中会在CMD后写一个运行脚本,在脚本内执行业务、与启动代码逻辑,此时脚本使用的是bash的话,bash如果不作trap的处理又会出现无法传导SIGTERM给应用程序的情况。

    模拟以上场景,在ENTRYPOINT ["/tini", "--"] 后调用exec-app.shapp.sh两种脚本

 1# 场景1
 2[root@3114-db-mysql ubuntu]# cat exec-app.sh
 3#!/bin/bash
 4exec /gosignal
 5[root@3114-db-mysql ubuntu]# cat Dockerfile
 6FROM ubuntu:22.04
 7LABEL "author"="kid"
 8#SHELL ["/bin/bash","-c"]
 9ENV TINI_VERSION v0.19.0
10ADD https://github.com/krallin/tini/releases/download/${TINI_VERSION}/tini /tini
11RUN chmod +x /tini
12COPY gosignal/gosignal /gosignal
13COPY entrypoint-signal.sh /entrypoint-signal.sh
14RUN chmod +x /entrypoint-signal.sh
15COPY exec-app.sh /exec-app.sh
16RUN chmod +x /exec-app.sh
17COPY app.sh /app.sh
18RUN chmod +x /app.sh
19ENTRYPOINT ["/tini", "--"]
20CMD ["/exec-app.sh"]
21
22# 进程结构:
23root@375509e8492e:/# ps -auxf
24USER         PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
25root          15  1.0  0.0   4628  3740 pts/0    Ss   17:57   0:00 bash
26root          23  0.0  0.0   7064  1644 pts/0    R+   17:57   0:00  \_ ps -auxf
27root           1  0.0  0.0   2780  1056 ?        Ss   17:57   0:00 /tini -- /exec-app.sh
28root           7  0.0  0.0 1225800 2888 ?        Sl   17:57   0:00 /gosignal
29
30# 执行docker stop 动作
31[root@3114-db-mysql ubuntu]# docker stop tini-exec-app
32tini-exec-app
33[root@3114-db-mysql ubuntu]# docker logs -f tini-exec-app
34Starting application,Pid is :7, PPid is: 1
35已捕捉到信号:SIGTERM, 执行关闭,sig is : terminated
36Exiting!
37#结论:在该进程结构下,应用程序能够正常接收到SIGTERM信号.
38
39# 场景2 
40[root@3114-db-mysql ubuntu]# cat app.sh
41#!/bin/sh
42/gosignal
43[root@3114-db-mysql ubuntu]# cat Dockerfile
44FROM ubuntu:22.04
45LABEL "author"="kid"
46ENV TINI_VERSION v0.19.0
47ADD https://github.com/krallin/tini/releases/download/${TINI_VERSION}/tini /tini
48RUN chmod +x /tini
49COPY gosignal/gosignal /gosignal
50COPY entrypoint-signal.sh /entrypoint-signal.sh
51RUN chmod +x /entrypoint-signal.sh
52COPY exec-app.sh /exec-app.sh
53RUN chmod +x /exec-app.sh
54COPY app.sh /app.sh
55RUN chmod +x /app.sh
56ENTRYPOINT ["/tini", "--"]
57CMD ["/app.sh"]
58
59# 进程结构:
60root@dff1564118e0:/# ps -auxf
61USER         PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
62root          15  1.0  0.0   4628  3780 pts/0    Ss   18:04   0:00 bash
63root          23  0.0  0.0   7064  1560 pts/0    R+   18:04   0:00  \_ ps -auxf
64root           1  0.0  0.0   2780   976 ?        Ss   18:02   0:00 /tini -- /app.sh
65root           7  0.0  0.0   2892  1000 ?        S    18:02   0:00 /bin/sh /app.sh
66root           8  0.0  0.0 1225544 2888 ?        Sl   18:02   0:00  \_ /gosignal
67# 执行docker stop 动作
68[root@3114-db-mysql ubuntu]# docker stop tini-app
69tini-app
70[root@3114-db-mysql ubuntu]# docker logs -f tini-app
71Starting application,Pid is :8, PPid is: 7
72# 结论:此时由于gosignal的父进程是PID为7的sh,sh无法给gosingal信号,虽然TINI进行了信号传递,但是结果是gosingal被暴力的杀死,没有处理SIGTERM的过程。
  1. tini作为1号进程,CMD脚本(这里使用entrypoint-signal.sh)具备trap SIGTERM的逻辑,能够传递信号量给自己的子进程当中
 1[root@3114-db-mysql ubuntu]# cat entrypoint-signal.sh
 2#!/bin/bash
 3/gosignal & PID1="$!"
 4echo "gosignal started with pid $PID1"
 5handle_sigterm(){
 6        echo "[INFO] Received SIGTERM"
 7        kill -SIGTERM $PID1
 8        wait $PID1
 9
10}
11trap handle_sigterm SIGTERM
12wait
13
14[root@3114-db-mysql ubuntu]# cat Dockerfile
15FROM ubuntu:22.04
16LABEL "author"="kid"
17ENV TINI_VERSION v0.19.0
18ADD https://github.com/krallin/tini/releases/download/${TINI_VERSION}/tini /tini
19RUN chmod +x /tini
20COPY gosignal/gosignal /gosignal
21COPY entrypoint-signal.sh /entrypoint-signal.sh
22RUN chmod +x /entrypoint-signal.sh
23COPY exec-app.sh /exec-app.sh
24RUN chmod +x /exec-app.sh
25COPY app.sh /app.sh
26RUN chmod +x /app.sh
27ENTRYPOINT ["/tini", "--"]
28CMD ["/entrypoint-signal.sh"]
29
30# 进程结构
31root@d24562e8169a:/# ps -auxf
32USER         PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
33root          15  0.6  0.0   4628  3800 pts/0    Ss   18:14   0:00 bash
34root          24  0.0  0.0   7064  1596 pts/0    R+   18:14   0:00  \_ ps -auxf
35root           1  0.0  0.0   2780   980 ?        Ss   18:12   0:00 /tini -- /entrypoint-signal.sh
36root           7  0.0  0.0   4364  3276 ?        S    18:12   0:00 /bin/bash /entrypoint-signal.sh
37root           8  0.0  0.0 1225544 2888 ?        Sl   18:12   0:00  \_ /gosignal
38# 此时gosignal进程作为bash的子进程形式存在。
39
40# 执行docker stop 动作
41[root@3114-db-mysql ubuntu]# docker stop tini-entrypoint-signal
42tini-entrypoint-signal
43[root@3114-db-mysql ubuntu]# docker logs  -f tini-entrypoint-signal
44gosignal started with pid 8
45Starting application,Pid is :8, PPid is: 7
46[INFO] Received SIGTERM
47已捕捉到信号:SIGTERM, 执行关闭,sig is : terminated
48Exiting!
49
50# 结论:可以看到,脚本处理了SIGTERM的信号

四、总结

  1. 使用脚本如ENTRYPOINT /entrypoint-signal.sh这种能够处理信号的脚本作为应用启动的1号进程过程当中一定需要记得在Dockerfile 添加与该脚本一致的SHELL ["/bin/bash","-c"]参数,否则很有可能由于默认存在的sh -c 原因导致entrypoint-signal.sh么有办法成为1号进程,从而对信号处理失败。

  2. 在使用ENTRYPOINT ["/tini", "--"]的过程当中,如果使用CMD+脚本启动业务进程,切记此时的程序启动脚本需要能够进行trap handle_sigterm SIGTERM的信号捕捉动作,或者在脚本当中使用exec的方式跳脱bash/sh父进程管控的形式执行,否则使用tini也并无实际意义,依然是粗暴结束业务进程。更建议是以CMD+业务二进制程序的方式,这样一来容器内PID1tini作为所有进程的父进程,在收到到SIGTERM时,会向所有PPID为1的进程广播放送信号。

附:gosignal:

 1package main
 2
 3import (
 4	"fmt"
 5	"os"
 6	"os/signal"
 7	"syscall"
 8	"time"
 9)
10func main() {
11	pid := os.Getpid()
12	ppid := os.Getppid()
13	sigchan := make(chan os.Signal, 1)
14	done := make(chan bool, 1)
15	// 注册一个通道,关注的系统信号量包含TERM、KILL、HUP
16	signal.Notify(sigchan, syscall.SIGTERM, syscall.SIGKILL, syscall.SIGHUP)
17	go func() {
18		sig := <-sigchan
19		fmt.Printf("已捕捉到信号:SIGTERM, 执行关闭,sig is : %v\n", sig)
20		time.Sleep(time.Second * 5)
21		done <- true
22	}()
23	fmt.Printf("Starting application,Pid is :%v, PPid is: %v\n", pid, ppid)
24	<-done
25	fmt.Println("Exiting!")
26}