通过发送SIGTERM停止正在运行的Docker容器

16
我有一个非常简单的Go应用程序,正在监听8080端口。
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
    w.WriteHeader(200)
    w.Header().Set("Content-Type", "text-plain")
    w.Write([]byte("Hello World!"))
})
log.Fatal(http.ListenAndServe(":8080", http.DefaultServeMux))

我将它安装在Docker容器中,并像这样启动它:

FROM golang:alpine
ADD . /go/src/github.com/myuser/myapp
RUN go install github.com/myuser/myapp
ENTRYPOINT ["/go/bin/myapp"]
EXPOSE 8080

然后使用docker run运行容器:

docker run --publish 8080:8080 first-app

我希望,像大多数程序一样,我可以向运行docker run的进程发送SIGTERM信号,这将导致容器停止运行。但事实上,我发现发送SIGTERM信号没有效果,相反我需要使用像docker killdocker stop这样的命令。

这是预期的行为吗?我已经在论坛和IRC上询问了但没有得到答案


说实话,我不确定百分之百,但我似乎记得将SIGTERM发送到PID 1的进程并不会像您期望的那样立即终止,因为您需要显式地捕获信号并对其进行处理(有关dumb-init背后的原理请参见https://github.com/Yelp/dumb-init)。尝试通过`sh`启动进程(将入口点指定为字符串,而不是使用数组语法),或者使用`dumb-init`来看看是否有所帮助。 - slugonamission
我认为OP并不是在谈论向容器中的PID1发送SIGTERM信号。我认为他是在谈论向主机上运行的docker run的pidof发送SIGTERM信号? - johnharris85
1
@JHarris - 我也这么认为;我只是不知道发送 docker run 信号实际上会做什么,所以只是瞎猜。不过我刚才看了一下,似乎 docker run 进程默认情况下会通过代理传递所有信号(在此处搜索 sig-proxy)。 - slugonamission
@JHarris 正确,向主机上运行docker的pid发送SIGTERM。 - Kevin Burke
3个回答

21
任何使用docker运行的进程都必须自己处理信号。
或者使用--init标志运行tini init作为PID 1
根据您指定的命令(CMD)方式,sh shell可以成为PID 1进程。
详细信息:
默认情况下,SIGTERMdocker run命令传播到Docker守护程序,但除非在Docker运行的主进程中专门处理该信号,否则不会生效。
在容器中运行的第一个进程将在该容器上下文中具有PID 1。这是Linux内核对待的特殊进程。除非该进程安装了该信号的处理程序,否则它将不会收到信号。它也是PID 1的工作将信号转发到其他子进程。

docker run和其他命令是Docker守护进程托管的远程API的API客户端。Docker守护进程作为一个独立的进程运行,并且是在容器上下文中运行的命令的父进程。这意味着run和守护进程之间没有直接发送信号的方式,而是采用标准Unix方式。

docker rundocker attach命令有一个--sig-proxy标志,默认情况下将信号代理设置为true。如果需要,可以关闭该功能。

docker exec不会代理信号

在编写 Dockerfile 时,如果不想让 sh 成为 PID 1 进程(Kevin Burke),请注意在指定 CMDENTRYPOINT 的默认值时使用 "exec 形式"。
CMD [ "executable", "param1", "param2" ]

信号处理Go示例

使用这里的Go示例代码:https://gobyexample.com/signals

运行一个不处理信号的常规进程和捕获信号并将其放入后台的Go守护程序。我使用sleep因为它很简单且不处理"守护进程"信号。

$ docker run busybox sleep 6000 &
$ docker run gosignal &

使用具有“树形”视图的ps工具,您可以看到两个不同的进程树。一个是在sshd下的docker run进程树。另一个是在docker daemon下的实际容器进程树。
$ pstree -p
init(1)-+-VBoxService(1287)
        |-docker(1356)---docker-containe(1369)-+-docker-containe(1511)---gitlab-ci-multi(1520)
        |                                      |-docker-containe(4069)---sleep(4078)
        |                                      `-docker-containe(4638)---main(4649)
        `-sshd(1307)---sshd(1565)---sshd(1567)---sh(1568)-+-docker(4060)
                                                          |-docker(4632)
                                                          `-pstree(4671)

docker主机进程的细节:

$ ps -ef | grep "docker r\|sleep\|main"
docker    4060  1568  0 02:57 pts/0    00:00:00 docker run busybox sleep 6000
root      4078  4069  0 02:58 ?        00:00:00 sleep 6000
docker    4632  1568  0 03:10 pts/0    00:00:00 docker run gosignal
root      4649  4638  0 03:10 ?        00:00:00 /main

杀死进程

我无法杀死 docker run busybox sleep 命令:

$ kill 4060
$ ps -ef | grep 4060
docker    4060  1568  0 02:57 pts/0    00:00:00 docker run busybox sleep 6000

我可以杀掉带有陷阱处理程序的 docker run gosignal 命令:
$ kill 4632
$ 
terminated
exiting

[2]+  Done                       docker run gosignal

通过 docker exec 发送信号

如果我在已经运行的 sleep 容器中使用 docker exec 命令启动一个新的 sleep 进程,我可以发送 ctrl-c 信号来中断 docker exec 命令本身,但这不会转发到实际的进程:

$ docker exec 30b6652cfc04 sleep 600
^C
$ docker exec 30b6652cfc04 ps -ef
PID   USER     TIME   COMMAND
    1 root       0:00 sleep 6000   <- original
   97 root       0:00 sleep 600    <- execed still running
  102 root       0:00 ps -ef

5
所以这里有两个因素:
1)如果您为入口点指定一个字符串,就像这样:
```js entry: 'path/to/entry/file.js' ```
那么Webpack将生成一个单独的chunk,其中包含所有在该入口文件中导入的模块。
2)如果您使用动态导入,Webpack将根据需要生成额外的chunks。例如,如果您在代码中使用了`import()`,Webpack将为此生成一个新的chunk,并在需要时异步加载它。
ENTRYPOINT /go/bin/myapp

Docker使用/bin/sh -c 'command'命令来运行脚本。这个中间层脚本会接收到SIGTERM信号,但不会将其发送给正在运行的服务器应用程序。

为了避免使用中间层,可以将入口点(entrypoint)指定为一个字符串数组。

ENTRYPOINT ["/go/bin/myapp"]

2) 我使用以下字符串构建了我想运行的应用程序:

docker build -t first-app .

这段代码给容器打上了名为“first-app”的标签。不幸的是,当我尝试重新构建/运行容器时,我运行了以下命令:
docker build .

之前我没有覆盖标签,所以我的更改没有被应用。

当我完成这两件事后,我就能够使用ctrl+c杀死进程并关闭正在运行的容器。


我没有想到在Dockerfile中使用Shell表单而不是Exec表单! 由于人们已经点赞了另一个回答,所以我添加了一个注释。 - Matt

1

这个问题的非常全面的描述和解决方案可以在这里找到: https://vsupalov.com/docker-compose-stop-slow

在我的情况下,我的应用程序期望接收SIGTERM信号以进行优雅的关闭,但由于进程是通过从dockerfile调用的bash脚本启动的,形式为:ENTRYPOINT ["/path/to/script.sh"]

因此,脚本没有将SIGTERM传播到应用程序。 解决方案是从脚本中使用exec运行启动应用程序的命令: 例如 exec java -jar ...


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接