我主要参考了这篇博客: Trapping signals in Docker containers, 推荐大家 有任何问题应该先参考官方stop文档官方kill文档.

stop与kill操作

stop操作比较简单, 是向容器中的主进程发送SIGTERM信号, 在规定时间如果没有关闭 则发送SIGKILL信号.

docker的kill操作比是类似Linux的kill操作, 可以发送各种信号到容器中. 比如SIGHUP一般是指定应用重载: docker kill --signal=SIGHUP my_container

为什么要讨论stop或是kill操作呢?

官方文档的说明中有这么一句The main process inside the container被发送信号, 主进程是docker容器中PID为1的进程, 那其他非主进程怎么办呢?

我们先来创建这样一个容器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// main.js
'use strict';
var http = require('http');
var server = http.createServer(function (req, res) {
res.writeHead(200, {'Content-Type': 'text/plain'});
res.end('Hello World\n');
}).listen(3000, '0.0.0.0');
console.log('server started');
var signals = {
'SIGINT': 2,
'SIGTERM': 15
};
function shutdown(signal, value) {
server.close(function () {
console.log('server stopped by ' + signal);
process.exit(128 + value);
});
}
Object.keys(signals).forEach(function (signal) {
process.on(signal, function () {
shutdown(signal, signals[signal]);
});
});
1
2
3
#!/bin/sh

node main.js
1
2
3
4
5
6
7
8
9
10
11
12
FROM node:10.17.0-alpine

COPY main.js main.js
COPY run.sh /run.sh
RUN chmod +x /run.sh

# ENTRYPOINT ["node", "main.js"]
ENTRYPOINT ["/run.sh"]

# docker build -t test .
# docker run --name my-kill -ti test
# docker stop my-kill

现在让我们启动容器之后, exec进入容器, 查看一下进程.

1
2
3
4
5
6
7
docker exec -ti my-kill /bin/sh
/ # ps
PID USER TIME COMMAND
1 root 0:00 {run.sh} /bin/sh /run.sh
6 root 0:00 node main.js
18 root 0:00 /bin/sh
23 root 0:00 ps

可以看下kill之后的两种结果:

1
2
3
# 使用了`ENTRYPOINT ["node", "main.js"]`
server started
server stopped by SIGTERM
1
2
3
# 使用了`ENTRYPOINT ["/run.sh"]`
server started
# stop后停顿了10s

如果你直接使用ENTRYPOINT ["node", "main.js"]时, 应用会收到SIGTERM信号并 会由程序自己关闭.

当你发送DockerStop命令时docker stop 37594c3a1250, 应用会等待10s, 而后 无声的退出, 这表明了一件事, node main.js这个程序收到的是SIGKILL, 它意味着程序必须被强制关闭, 也就是说程序无法做任何事, 只能直接关闭.

结合官方文档来理解, 该现象出现的原因是stop发送信号时, 只会向主进程发送, 在这里也就是进程号为1的这个脚本进程. 但不幸的是, 这个脚本没有将信号传递给 它的子进程, 导致子进程无法收到SIGTERM信号, 最终被发SIGKILL关闭.

如何优雅的关闭容器

为什么出现子进程没收到SIGTERM信号呢? 原因是主进程没有传递过去? 既然如此, 我们 想要优雅的关闭程序, 那么你所需要的就是主进程收到了信号后传递给子进程. 有两种方法分享给大家:

shell脚本优化

既然简单的shell脚本无法实现, 我们可以考虑把为shell脚本增加传递信号的功能:

下面脚本截取自program.sh(只能处理单进程):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#!/usr/bin/env bash
set -x

pid=0

# SIGUSR1 -handler
my_handler() {
echo "my_handler"
}

# SIGTERM -handler
term_handler() {
if [ $pid -ne 0 ]; then
kill -SIGTERM "$pid"
wait "$pid"
fi
exit 143; # 128 + 15 -- SIGTERM
}

# setup handlers
# on callback, kill the last background process, which is `tail -f /dev/null` and execute the specified handler
trap 'kill ${!}; my_handler' SIGUSR1
trap 'kill ${!}; term_handler' SIGTERM

# run application
node main.js &
pid="$!"

# wait forever
while true
do
tail -f /dev/null & wait ${!}
done

使用supervisor或是S6这样的进程管理工具

这里是S6官网.

这些工具有个特点就是能够管理子进程, 像是发送信号这样的功能是完全可以做到的.

总结

通过上面的例子和文档, 我想表明的是, Docker设计的初衷是每个容器仅运行一个进程, 这样Docker可以很好的管理它. 这篇文章的目的不是想要推荐大家去写奇怪的shell脚本, 也不是在Docker中运行多进程, 只是为了表明Docker有运行多进程的能力, 但是请不要假定Docker会帮你管理所有子进程.

请在合理的位置使用多进程, 或是尝试下kubernetes, 它的pods可以由多个container组成.