Docker ENTRYPOINT/FROM 对 docker stop 的影响

背景

之前有同事和我提过,在 Kubernetes 中,删除一个“应用”,有些应用删除比较慢。我们的“应用”,可以理解为一个 Deployment,删除应用,就是删除 Deployment,然后等待 Pod 全部退出。

当时其实没有太在意这个事情,因为 Kubernetes 的删除,从最细的粒度来看,就是删除 Pod,而删除 Pod,其实本质上就是停止容器,停止容器,本身其实会执行的是 docker stop 的过程,超时后,执行 docker kill 逻辑。

docker stop 是 docker daemon 进程,向 docker 容器进程,发送 kill -USR2 信号,而 docker kill,其实是一个 Kill -9 信号。换句话说,先让容器自行退出,一定时间内没有成功,再强制杀死。

当时觉得,有些容器停止慢,应该是容器业务比较繁重造成的,进程自行退出花费时间比较多而已。但今天看偶尔看 ENTRYPOINT 的的东西的时候,发现,也许这个问题,并没有那么简单。

ENTRYPOINT 的用法说明

我们知道,在构建镜像的时候,可以指定程序入口。可以设置 ENTRYPOINT 和 CMD。这2个可以配合使用,也可以完全独立使用。我们本次,不关心CMD,只讨论 ENTRYPOINT 的不同写法,对容器的影响。

Dockerfile 中 ENTRYPOINT 的2个用法

1
2
3
4
5
// 用法1
ENTRYPOINT ["your executable program", "param1", "param2" ...]

// 用法2
ENTRYPOINT command param1 param2 ...

第一种写法,你可以自行定义某个二进制程序以及参数,作为容器的1号进程的相关启动内容。
这种写法,也就是数组写法。

第二种写法,会将所设定的程序,限定在 /bin/sh -c 下执行。
换句话说,这种方式,容器内会有2个进程,一个是 /bin/sh 进程,一个是真正的你的二进制程序的进程,它的进程ID,不是1。

注意1:Docker 官方,推荐第一种写法。

注意2:第二种写法,限定你的进程在 /bin/sh 下,是很多文章提到的,但这个说法,其实并不准确,后边我会测试说明。现在,我们先默认这句话是正确的。

需要特别说明的是:

如果你的进程不是1号进程,/bin/sh 是1号进程的话,会存在一个问题:/bin/sh 进程,不会处理 Linux 信号。这就导致,用第二种 ENTRYPOINT 的写法,就可能出现 docker stop 无法正常停止容器(当时等待 docker stop 超时后,docker daemon还是会发送 kill 信号的,这个可以保证容器停止并退出)。

好了,我们要用第二种写法,测试 docker stop 无法正常停止容器这个过程。

在开始之前,我们明确几个事情

  1. 我们要测试 Dockerfile 中 ENTRYPOINT 写法不同,对容器中进程的影响
  2. 我们的可执行程序,就用 top
  3. 我们要看这个影响,是否会间接影响到 docker stop
  4. 最后,我们看一下,如果基础镜像不同,是否测试结果也会不同,我们先用 ubuntu:latest 做基础镜像测试。最后再用 centos:7.5.1804 作为基础镜像测试。

开始测试

使用 ubuntu 作为基础镜像做测试

Dockerfile 内容如下:

1
2
FROM ubuntu:latest
ENTRYPOINT top -b

执行下面命令,创建测试镜像,并运行为容器:

1
2
3
docker build -t test-centos2 -f Dockerfile .

$ docker run -it test-centos2

可以直接看到如下结果:

1
2
3
4
5
6
7
8
9
top - 11:40:05 up 1 day, 19:34,  0 users,  load average: 0.08, 0.05, 0.01
Tasks: 2 total, 1 running, 1 sleeping, 0 stopped, 0 zombie
%Cpu(s): 0.2 us, 0.3 sy, 0.0 ni, 99.6 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
KiB Mem : 2046932 total, 333164 free, 234772 used, 1478996 buff/cache
KiB Swap: 1048572 total, 1048080 free, 492 used. 1607492 avail Mem

PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
1 root 20 0 4500 696 628 S 0.0 0.0 0:00.17 sh
6 root 20 0 36528 3004 2644 R 0.0 0.1 0:00.01 top

这个结果说明:

  1. 容器内确实起了2个进程
  2. 1号进程就是 /bin/sh
  3. 我们的可执行程序,不是1号进程,而是 /bin/sh 进程的子进程,进程ID为6

那么,我们停止此容器。
停止之前,先说一下,docker stop 会给容器发送SIG信号,让进程自行退出,如果进程不处理此信号,docker stop 会超时,然后 发送 kill 信号。默认超时时间是 10s ,我们执行如下命令:

1
2
3
4
time docker stop -t 30 e58dc2887ef8
//输出:
e58dc2887ef8
docker stop -t 30 e58dc2887ef8 0.39s user 0.09s system 1% cpu 31.515 total

这个输出表明,docker stop 真的等了 30s 后才执行成功,也就是,/bin/sh 确实没有处理 SIG 信号。最后被 kill 掉了。

初期结论和说明

我们要尽量避免上面的现象,就要保证容器的1号进程,是应用的真正可执行程序,不能是 /bin/sh 进程,否则,Kubernetes 删除 pod,就会等待一段时间才能执行成功。另外,我们还是应该尽量使用官方推荐的 ENTRYPOINT 写法(数组写法)

使用centos作为基础镜像测试

但是,问题到此并没有结束。我们之前的 Dockerfile 是使用 ubuntu 作为基础镜像的。我们尝试,换为 centos 作为基础镜像试一下

1
2
FROM centos:7.5.1804
ENTRYPOINT top -b

然后,我们生成新镜像,并运行为容器:

1
2
3
4
5
6
7
8
9
$ docker run -it test-centos3
top - 16:11:26 up 1 day, 20:29, 0 users, load average: 0.00, 0.00, 0.00
Tasks: 1 total, 1 running, 0 sleeping, 0 stopped, 0 zombie
%Cpu(s): 33.3 us, 33.3 sy, 0.0 ni, 33.3 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
KiB Mem : 2046932 total, 328508 free, 237228 used, 1481196 buff/cache
KiB Swap: 1048572 total, 1048080 free, 492 used. 1604860 avail Mem

PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
1 root 20 0 56032 3668 3236 R 0.0 0.2 0:00.05 top

我们能看到,只有1个进程,且进程ID为1。我们直接进入到容器内部看一下:

docker exec -it e8fa215e3ad6link
1
2
3
4
5
6
[root@e8fa215e3ad6 /]# ps -ef
UID PID PPID C STIME TTY TIME CMD
root 1 0 0 16:11 pts/0 00:00:00 top -b
root 6 0 3 16:12 pts/1 00:00:00 /bin/bash
root 20 6 0 16:12 pts/1 00:00:00 ps -ef
[root@e8fa215e3ad6 /]#

从上,我们确实只能看到 top 进程为1号进程,并没有 /bin/sh 进程。而这个 Dockerfile 和之前的 Dockerfile,唯一的区别就是基础镜像不同。

结论

  • /bin/sh 进程是无法处理 Linux 信号的。
  • 不论使用哪种镜像做应用镜像的基础镜像,都要注意构建完应用镜像后测试一下,最好不要让 /bin/sh 成为 1 号进程。
  • 尽量在 Dockerfile 中,为 ENTRYPOINT、CMD、RUN 使用数组方式写法。
Donate comment here