Docker进阶(二): 启动进程


众所周知,k8s中以Pod作为调度的基本单位,一个Pod中一般有多个Container。那为什么k8s不能像Docker一样以Container一样作为调度单位呢?那是因为,在Docker中,一个Container中最好只运行一个进程,这也是Docker官网建议的做法。至于为什么只运行一个进程为好,就要了解下Container的进程管理了。

PID namespace

在Docker中,进程管理的基础就是Linux内核中的PID namespace技术,也就是PID名空间。在不同的PID namespace中,进程ID是相互独立的,也就是说:

两个不同的PID namespace下,进程可以拥有相同的PID

Linux内核为所有的PID namespace维护了一个树状结构:最顶层的是系统初始化创建的root namespace,再创建的新PID namespace就称之为child namespace,而原先的PID namespace就是其Parent namespace。通过这种方式,系统中的PID namespace就会形成一个层级体系。

父namespace中可以看到子namespace中的所有进程,并通过信号等方式对其产生影响,比如kill掉。但是反过来,子无法看见父,也不能影响父。

可以通过lsns查看Linux中所有的namespace,包括PID namespace。如果只看pid,使用下述命令即可:

lsns | grep pid

可用列(用于 –output):
NS namespace identifier (inode number)
TYPE kind of namespace
PATH path to the namespace
NPROCS number of processes in the namespace
PID lowest PID in the namespace
PPID PPID of the PID
COMMAND command line of the PID
UID UID of the PID
USER username of the PID

在Docker中,每个Container都是Docker Daemon的子进程,每个Container在建立时都会缺省建立一个新的PID namespace,所以每个Container都有自己的PID namespace。通过这种机制,Docker实现了容器间的进程隔离。

另外,Docker Daemon也会利用PID namespace的树状结构,实现了对容器中的进程交互、监控和回收。当然,Docker还利用了其他namespace,比如UTS、IPC、USER等等,实现了各种系统资源的隔离。

docker run一个Container时,其中的启动进程在该PID namespace中的PID为1。比如,这里,我们创建两个redis容器:

[root@master test]$ docker run -d --name=redis1 redis
ad704440fbe91fdf113fa551ebf687482d6468b26e240af6f9bf7e09842a6d2e
[root@master test]$ docker run -d --name=redis2 redis
6113d916add9615fd154f70e9f74016a53898d8a7b4bf3914913727037edd863

随后,给两个容器都装上ps工具包,在容器内使用命令:

$ apt-get update
$ apt-get install procps

安装完毕后,分别在两个容器内部执行ps -ef

[root@master test]$ docker exec redis1 ps -ef
UID         PID   PPID  C STIME TTY          TIME CMD
redis         1      0  0 08:32 ?        00:00:00 redis-server *:6379
root        362      0  0 08:39 ?        00:00:00 ps -ef
[root@master test]$ docker exec redis2 ps -ef
UID         PID   PPID  C STIME TTY          TIME CMD
redis         1      0  0 08:32 ?        00:00:00 redis-server *:6379
root        353      0  0 08:39 ?        00:00:00 ps -ef

可以看到,两个容器的启动进程都是redis-server,且PID都是1。更重要的是,这些进程的PPID都为0,说明他们的父进程并不在Container中。那么,在宿主机中分别查看下这俩Container中的进程信息:

[root@master test]$ docker top redis1
UID                 PID                 PPID                C                   STIME               TTY                 TIME                CMD
polkitd             45068               45047               0                   16:32               ?                   00:00:00            redis-server *:6379
[root@master test]$ docker top redis2
UID                 PID                 PPID                C                   STIME               TTY                 TIME                CMD
polkitd             45210               45189               0                   16:32               ?                   00:00:00            redis-server *:6379

可以看到,两个容器的redis-server *:6379在这个PID namespace中有了新的PID,分别为45068和45210。


启动进程

现在,在redis1中再启动一个进程,这个进程的pid当然是随机的:

[root@master test]$ docker exec -d redis1 sleep 10000
[root@master test]$ docker exec redis1 ps -ef
UID         PID   PPID  C STIME TTY          TIME CMD
redis         1      0  0 08:32 ?        00:00:03 redis-server *:6379
root        416      0  0 09:22 ?        00:00:00 sleep 10000
root        423      0  0 09:22 ?        00:00:00 ps -ef
[root@master test]$ docker top redis1
UID                 PID                 PPID                C                   STIME               TTY                 TIME                CMD
polkitd             74762               74741               0                   17:30               ?                   00:00:00            redis-server *:6379
root                74847               74741               0                   17:30               ?                   00:00:00            sleep 10000

问题来了,如果在宿主机上kill掉Container的启动进程,也就是上述redis-server *:6379,会发生什么?

[root@master test]$ kill 45068
[root@master test]$ docker top redis1
Error response from daemon: Container ad704440fbe91fdf113fa551ebf687482d6468b26e240af6f9bf7e09842a6d2e is not running

可以看到,当杀掉启动进程后,整个Container直接停掉了。这就意味着,这个Container中的所有进程全部被杀了,包括刚才的sleep进程:

[root@master test]# ps -ef | grep 74847
root      75781   7737  0 17:32 pts/0    00:00:00 grep --color=auto 74847

并且,当我们重新启动Container后,之前那些进程并不会保留,而是从头也就是从启动进程重新运行:

[root@master test]$ docker start redis1
redis1
[root@master test]$ docker top redis1
UID                 PID                 PPID                C                   STIME               TTY                 TIME                CMD
polkitd             75999               75979               0                   17:32               ?                   00:00:00            redis-server *:6379

可以看到,重启Container之后,启动进程在该PID namespace中的PID也发生了变化,说明和之前完全是两个进程。

杀掉启动进程后,容器会直接停掉,其中的所有进程也会结束。

这也就解释了,为什么Docker最好一个Container只运行一个进程,也就是启动进程。

实际上,PID=1的进程对任何OS都是无比重要的。Linux的PID=1进程是init进程,以守护进程方式运行,是所有其他进程的祖先,具有完整的进程声明周期管理能力,这点在OS理论课上提到过。而对于Docker Container而言,PID=1进程就是启动进程,它也会负责容器内部进程管理的工作。

总结一下:

  • 每个Container都有自己的PID namespace;
  • Container的声明周期和其启动进程的生命周期一致。

指定启动进程(exec/shell)

Container的启动进程可以被Dockerfile中的ENTRYPOINTCMD指令所指令,意味着也可以被docker run命令的启动参数所覆盖。也就是说,容器建立时运行的那个进程,就是它的启动进程,PID为1。

来看看redis的ENTRYPOINTCMD

...
ENTRYPOINT ["docker-entrypoint.sh"]
...
CMD ["redis-server"]
...

所以,如果不覆盖CMD的话,redis容器的启动进程就是执行docker-entrypoint.sh redis-server 后启动的redis-server。

如果覆盖了CMD,那么启动进程就会更改。举个最常见的例子-/bin/bash。在之前的博客里有提到过,后台服务类的Container都不要以-it ... bash的方式启动,因为这样并不会运行相关服务。现在可以更进一步理解,不仅相关服务没有运行,该Container的启动进程也会变为bash

[root@master test]$ docker run -it --name=redis3 redis bash
root@48646a552559:/data$ exit         
exit

[root@master test]# docker top redis3
UID                 PID                 PPID                C                   STIME               TTY                 TIME                CMD
root                79982               79960               0                   17:40               pts/0               00:00:00            bash
[root@master test]$ docker start redis3
redis3
[root@master test]$ docker exec -it redis3 bash
...# 容器安装procps
[root@master test]$ docker exec redis3 ps -ef
UID         PID   PPID  C STIME TTY          TIME CMD
root          1      0  0 09:40 pts/0    00:00:00 bash
root        405      0  0 09:53 ?        00:00:00 ps -ef

不过,上面的Dockerfile中ENTRYPOINTCMD采用的是exec格式,那如果用shell格式会怎么样,仅仅是编写格式不一样吗?为了检验,这里编写一个新的Dockerfile,CMD格式改为shell:

FROM ubuntu:14.04
RUN apt-get update && apt-get -y install redis-server && rm -rf /var/lib/apt/lists/*
EXPOSE 6379
CMD "/usr/bin/redis-server"

然后构建一个新的镜像 redis_shell:

docker build -t redis_shell -f Dockerfile_redis_shell .

运行新的镜像,因为ubuntu镜像本身自带procps,所以不需另外安装,直接ps -ef查看其启动进程:

[root@master test]# docker run -d --name=redis4 redis_shell
4ca1d8bf78201fd9d8b2c5dbc9535c774aff5a82fe8ce3620f2e319f39febdac
[root@master test]# docker exec redis4 ps -ef
UID         PID   PPID  C STIME TTY          TIME CMD
root          1      0  0 10:18 ?        00:00:00 /bin/sh -c "/usr/bin/redis-server"
root          8      1  0 10:18 ?        00:00:00 /usr/bin/redis-server *:6379
root         11      0  0 10:18 ?        00:00:00 ps -ef

很神奇的是,/usr/bin/redis-server *:6379并没有成为该Container的启动进程,取而代之的是启动它的一条shell指令。而最核心的redis-server进程由shell命令启动,自然而然的称为了启动进程的子进程。如果是exec格式,根本不会有第一个进程,/usr/bin/redis-server *:6379会直接成为启动进程。这点是shell格式和exec格式一个很重要的不同点。

不过即使是shell格式,CMD也会被docker run覆盖,即启动进程仍然会被覆盖

[root@master test]$ docker run -it --name=redis5 redis_shell /bin/bash
root@13e3466d688b:/$ ps -ef
UID         PID   PPID  C STIME TTY          TIME CMD
root          1      0  0 10:23 pts/0    00:00:00 /bin/bash
root         16      1  0 10:23 pts/0    00:00:00 ps -ef

总结一下:

在ENTRYPOINT和CMD字段中,提供两种不同的进程执行方式: shell 和 exec

  • shell 方式下,shell命令是启动进程,其运行出的应用是子进程;
  • exec方式下,应用就是启动进程。

通常而言,建议使用exec格式,这样可以将核心进程直接设为启动进程。这也就是为什么各大Image的Dockerfile都使用该格式。


文章作者: SrcMiLe
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 SrcMiLe !
评论
  目录