进程标识符 (PID) 是Linux 内核为每一个进程提供的惟一标识符。熟悉docker的同窗都知道, 全部的进程 PID都属于某一个PID namespaces, 也就是说容器具备一组本身的 PID,这些 PID 映射到主机系统上的 PID。启动Linux内核时启动的第一个进程具备 PID 1,通常来讲该进程就是 init 进程,例如 systemd 或 SysV。一样,在容器中启动的第一个进程也会得到该PID namespaces内的 PID 1。Docker 和 Kubernetes 使用信号与容器内的进程通讯,来终止容器的运行, 只能向容器内 PID 1 的进程发送信号。nginx
在容器的环境中,PID 和 Linux 信号会产生两个须要考虑的问题。git
问题 1:Linux 内核如何处理信号
对于具备 PID 1 的进程,Linux 内核处理信号的方式与其余进程有所不一样。系统不会自动为此进程注册信号处理函数,SIGTERM 或 SIGINT 等信号默认被忽略,必须使用 SIGKILL 来终止进程。使用 SIGKILL 可能会致使应用程序没法平滑退出,例如正在写入的数据出现不一致或正在处理的请求异常结束。github
问题 2:经典 init 系统如何处理孤立进程
宿主机上的init进程(如 systemd)也用来回收孤儿进程。孤儿进程(其父级已结束的进程)会从新附加到 PID 1 的进程,PID 1进程会在这些进程结束时回收它们。但在容器中,这一职责由具备 PID 1 的进程承担,若是该进程没法正确处理回收,则可能会出现耗尽内存或一些其余资源的风险。docker
上述问题对于一些应用程序可能无足轻重,并不须要关注,可是对于一些面向用户或者处理数据的应用程序却极为关键。须要严格防止。 对此有如下几种解决方案:shell
最简单方法是使用 Dockerfile 中的 CMD 或 ENTRYPOINT 指令来启动进程。例如,在如下 Dockerfile 中,nginx 是第一个也是惟一一个要启动的进程。ubuntu
FROM debian:9 RUN apt-get update && \ apt-get install -y nginx EXPOSE 80 CMD [ "nginx", "-g", "daemon off;" ]
nginx 进程会注册本身的信号处理程序。若是是咱们本身写的程序则须要本身在代码中执行相同操做。centos
由于咱们的进程就是PID 1进程,因此能够保证可以正确的收到并处理信号。 这种方式能够轻松地解决了第一个问题,可是对于第二个问题却没法解决。 若是你的应用程序不会产生多余的子进程,则第二个问题也不存在。 能够直接采用这种相对简单的解决方案。bash
此处须要注意,有时候咱们可能一不当心就让咱们的进程不是容器内首进程了,例如以下Dockerfile:函数
FROM tagedcentos:7 ADD command /usr/bin/command CMD cd /usr/bin/ && ./command
咱们只是想执行启动命令而已,却发现此时首进程变为了shell:ui
[root@425523c23893 /]# ps -ef UID PID PPID C STIME TTY TIME CMD root 1 0 1 07:05 pts/0 00:00:00 /bin/sh -c cd /usr/bin/ && ./command root 6 1 0 07:05 pts/0 00:00:00 ./command
docker会自动地判断你当前启动命令是否由多个命令组成,若是是多个命令则会用shell来解释。若是是单个命令则就算外面包了一层shell容器内首进程也直接是业务进程。例如若是将dockerfile写成CMD bash -c "/usr/bin/command"
,容器内首进程仍是业务进程,以下:
[root@c380600ce1c4 /]# ps -ef UID PID PPID C STIME TTY TIME CMD root 1 0 2 13:09 ? 00:00:00 /usr/bin/command
因此正确地书写Dockerfile也可让咱们避免掉不少问题。
有时,咱们可能须要在容器中准备环境,以便进程可以正常运行。在此状况下,通常咱们会让容器在启动时执行一个 shell 脚本。此 shell 脚本的任务是准备环境和启动主进程。可是,若是采用此方法,shell脚本将是PID 1 而不是咱们的进程。所以必须使用内置的 exec 命令从 shell 脚本启动进程。exec 命令会将脚本替换为咱们所需的程序, 这样咱们的业务进程将成为 PID 1。
正如在传统宿主机所作的那样,还可使用init进程来处理这些问题。可是, 传统的init进程(例如 systemd 或 SysV)太过复杂而庞大,建议使用专为容器建立的init进程(例如 tini)。
若是使用专用 init 进程,则 init 进程具备 PID 1 并执行如下操做:
能够经过使用 docker run 命令的 --init 选项在 Docker 中使用此解决方案。可是目前kubernetes还不支持直接使用该方案,须要在启动命令前手动指定。
上面两种解决方案看似美好,实则在实施的过程当中仍是存在不少弊端。
方案一须要严格保证用户进程是首进程
而且不能fork出多余的其余进程
。 有时候咱们在启动的时候须要执行一个shell脚本准备环境, 或者须要运行多个命令,例如'sleep 10 && cmd', 此时容器内首进程便为shell,就会碰到问题一, 没法转发信号。 若是咱们限制用户的启动命令不能包含shell语法, 对用户体验也不太好。 而且做为PASS平台,咱们须要为用户提供一个简单友好的接入环境,帮用户处理好相关的问题。 从另一方面考虑, 在容器环境下多进程在所不免,即便咱们在启动时确保只运行一个进程,有时候在运行时过程当中也会fork出进程。 咱们没法确保咱们所使用的第三方组件或者开源的方案不会产生子进程, 咱们稍不注意就会碰到第二个问题,僵尸进程没法回收的囧境。
方案二中须要在容器中有一个init进程负责完成全部的这些任务, 当前业务广泛的作法是, 在构建镜像的时候里面自带init进程,负责处理上面全部的问题。 这种方案当然可行,可是须要让全部人都使用这种方式彷佛有点难以接受。首先对用户镜像有侵入,用户必须修改已有的Dockerfile, 专门增长init进程 或者 只能在包含有该init进程的基础镜像上面进行构建。 其次管理起来比较麻烦,若是init进程升级,意味着所有镜像都得从新build,这彷佛没法接受。即便使用docker默认支持的tini,也有一些其余问题,咱们后面会谈到。
归根结底, 做为PASS平台,咱们想给用户提供一个便捷的接入环境,帮助用户解决这些问题:
若是咱们想要对用户无侵入,则最好使用docker或kubernetes原生支持的方案。
上面已经介绍过了docker run --init选项, docker原生提供的init进程实则为tini。tini支持给进程组传递信号, 经过-g
参数或者TINI_KILL_PROCESS_GROUP
来进行开启该功能。 开启该功能后咱们就能够将tini做为首进程,而后让它传递信号给全部的子进程。问题一就能够轻松解决。 例如咱们执行 docker run -d --init ubuntu:14.04 bash -c "cd /home/ && sleep 100"
就会发现容器内的进程视图以下:
root@24cc26039c4d:/# ps -ef UID PID PPID C STIME TTY TIME CMD root 1 0 2 14:50 ? 00:00:00 /sbin/docker-init -- bash -c cd /home/ && sleep 100 root 6 1 0 14:50 ? 00:00:00 bash -c cd /home/ && sleep 100 root 7 6 0 14:50 ? 00:00:00 sleep 100
此时1号docker-init进程,也就是tini进程, 负责转发信号到全部的子进程,而且回收僵尸进程, tini的子进程为6号bash进程, 它负责执行shell命令,能够执行多个命令。这里有一个问题就是: tini进程只会监听他的直接子进程,若是直接子进程退出则整个容器就视为退出了, 也就是本例中的6号bash进程。 若是咱们往容器中发送SIGTERM,可能用户进程注册了信号处理函数, 收到信号后处理须要必定的时间完成,可是因为bash没有注册SIGTERM信号处理函数,会直接退出,进而致使tini退出,整个容器退出。用户进程的信号处理函数尚未执行完毕就被强制退出了。咱们须要想办法让bash忽略掉这个信号,同事提到bash在交互模式下不会处理SIGTERM信号, 能够一试。 在启动命令前面加上bash -ci
便可。发现使用bash交互模式启动用户进程就可使bash忽略掉SIGTERM,而后等待业务的信号处理函数执行完毕整个容器再退出。
如此便完美解决了上述相关问题。 同时还收获了另一个微不足道的好处:容器退出时更加快速。咱们知道kubernetes中容器退出的逻辑和docker同样,先发送SIGTEMR 而后再发送SIGKILL, 对于大部分用户来讲,都不会处理SIGTERM信号,容器内1号进程收到该信号后默认的行为是忽略该信号, 因而SIGTERM信号白白地被浪费掉,须要等待terminationGracePeriodSeconds
以后才被删除。既然用户不处理SIGTERM,为何不直接在收到SIGTERM以后就退出呐? 在当前咱们的解决方案下若是用户有注册该信号处理函数,则能正常处理。 若是没有注册则容器在收到SIGTERM以后就立刻退出,能够加快退出速度。
目前因为kubernetes中CRI并无直接提供能够设置docker tini的方法,因此要想在kubernetes中使用tini就只能改代码了,笔者的集群中就是经过改代码来实现的。为了解决用户的痛点,咱们有能力也有义务为合理的需求改代码,何况这个改动足够小,很是简单。
在容器落地的过程当中会碰到各类实际的问题,开源的方案可能没法覆盖到咱们全部的需求,须要咱们在精通社区的实现基础上进行轻微的变形便可完美适应企业内部的场景。