之前对僵尸进程确实是一知半解,没有好好研究过。这次本着学习的目的,梳理了僵尸进程的有关知识点以及在 k8s 容器中的应用。分享给大家,希望大家也能有所了解,别像我之前那样云里雾里。
本文主要是介绍僵尸进程以及在容器中预防僵尸进程的一些方法。大概分为以下几部分:
-
僵尸进程的介绍
-
容器内为什么更容易产生僵尸进程
-
容器内如何预防僵尸进程
-
k8s pause 容器如何处理僵尸进程
首先来介绍下僵尸进程是什么。
僵尸进程的介绍
为了更好地理解僵尸进程,我们先来看下进程结束后的流程,如下图所示:
图1 进程退出流程
一个正常的进程退出流程是这样的:
-
子进程退出后,给父进程发送一个SIGCHLD的信号
-
父进程收到这个信号后,会通过wait系统调用来回收子进程
这里有个术语叫“僵死状态”,指的是子进程退出后,在父进程使用wait对它进行回收之前的状态。所以,僵尸进程就比较好理解了,即在子进程退出后,父进程没有回收它,它一直处于僵死状态,就成为了一个僵尸进程。
僵尸进程在linux下长什么样?应该大部分同学都遇到过,如下图所示:
图2 僵尸进程展示(图片来源https://cloud.tencent.com/developer/article/1722245)
僵尸进程是kill不掉的,所以遇到这种进程,不了解的同学就会感觉很无助,为什么进程还kill不掉。那为什么僵尸进程kill不掉呢?因为它实际上并没有在运行了,只是在进程表内占了个坑(进程号)。而这个坑必须由它的父进程来回收,不然它就会永远占着坑。
有同学可能会认为,既然都不占资源(CPU、Memory等),那僵尸进程也没什么危害。实际上,还是有危害的。在linux中进程号是有限的,也就是进程表的大小是有限的。如果坑被占满了,就创建不了新的进程。即使资源充足,那也没有坑给新进程。所以,如果因为程序bug会不断产生僵尸进程,那最后系统也会被打挂。
既然有危害,又kill不掉,那应该怎么处理僵尸进程?有两个办法:
-
粗暴一点,重启机器。这个方法很简单,但代价也大。不宜经常使用。
-
找到僵尸进程的父进程,把父进程kill掉,僵尸进程就会变成孤儿进程,从而会被linux的init进程给回收了。
不过,笔者也遇到过父进程不好kill的情况,处理起来就很麻烦。所以,要尽量避免僵尸进程的产生,提前做好预防。虽然linux有保底的init进程,但也需要有办法让僵尸进程的父进程变成init进程。另外,如果僵尸进程是在容器内产生的,就更好不处理了。
容器内为什么更容易产生僵尸进程
接下来讲讲容器内的僵尸进程怎么不好处理。
以 Docker Container 为例,容器创建后,默认情况下是不会共享宿主机的 PID Namespace,它会自己创建一个 PID Namespace。这就是为什么在容器内执行 ps -ef 命令,只会看到容器内的进程,看不到宿主机的进程。容器就是要从宿主机上隔离出来一个命名空间,否则不就能轻易地侵入宿主机。如下图所示:
图3 容器内的进程
当我们执行docker run命令创建一个容器时,这个容器会有一个自己的 PID Namespace,而这个 PID Namespace 的1号进程就是我们创建容器时指定的entrypoint。图中为 node run.js。
根据linux进程回收的原理,孤儿进程都会被init进程接管,并由init进程来回收。那在docker容器自己创建出来的 PID Namespace 中,init 进程就是指定的 entrypoint 产生的进程。所以,孤儿进程的父进程都会变成 entrypoint 产生的进程。如果这个进程不会主动去回收僵尸进程,那一个会产生僵尸进程的容器,里面的僵尸进程就一直得不到回收。这就是容器内为什么会有僵尸进程的原因。
实际上,如果容器使用不当,是很容易产生僵尸进程的。因为很多命令都不具备主动回收僵尸进程的能力。不像在宿主机,只要僵尸进程被init进程接管就能得到回收,在容器内还要看init进程是谁。所以,使用容器需要特别注意预防僵尸进程。
容器内如何预防僵尸进程
我们知道了容器内为什么更容易产生僵尸进程,接下来我们讲讲预防手段。知道了原理,预防方式就比较简单:让具备僵尸进程回收能力的进程充当容器的init进程。
方法有三:
-
用 bash 命令来启动实际要运行的 entrypoint
-
借助专门的init进程,docker自带这个能力
-
借助成熟好用的 tini,官方地址:https://github.com/krallin/tini
下面说说三种方式的区别。
首先,直接使用 bash。bash 是自带僵尸进程回收能力的,不了解的同学可能会说,我创建的容器为什么就没有出现僵尸进程呢?可能是你已经用了 bash。所以,比较简单的方式就是直接用 bash 去启动容器,僵尸进程就会被 bash 回收。
后面两种方式可以一起说下。实际上,从 Docker 1.13 版本开始,docker 的 init 进程用的就是 tini。
第二种方式就是启动容器的时候带上个 –init 参数就好,docker 就会用它自带的 init 进程作为容器的1号进程。
第三种方式和 bash 类似,就是通过 tini 来启动实际要运行的 entrypoint。那跟 bash 的区别是什么?主要是容器能否做到优雅关闭。bash 不会将收到的信号传递给它的子进程,它只管自己接收就完事了。这样在容器收到停止信号的时候,bash 只管自己退出,不会去通知子进程先退出。而 tini 是会的,这就是用 tini 可以做到容器优雅退出的原因,它会传递信号到子进程。
tini更多的好处可以参考:https://github.com/krallin/tini/issues/8
笔者在实际工作中,也是使用 tini 来包装了容器启动命令,解决了容器内会产生僵尸进程的问题。
k8s pause 容器如何处理僵尸进程
说到容器,自然要说到 k8s。
这里默认大家对 k8s pod 是有所了解的,不了解的可以看下笔者之前的文章:Kubernetes 批调度。一个 pod 是一组容器的集合,容器会共享 pod 的网络命名空间。
那 pod 是如何做到让
对 k8s pause 的介绍就这么多。实际上,笔者觉得这种方式也需要慎用。要利用 pause 回收僵尸进程,就意味着容器要共享 PID Namespace。在不能共享 PID Namespace 的情况下,pause 回收僵尸进程的机制作用就不大。笔者还是推荐使用 tini 来封装容器的启动命令。
one more thing
对于 tini 回收僵尸进程,笔者还想多说一点。tini 并不能回收所有的僵尸进程,如下图所示:
tini 只能回收它的曾孙僵尸进程,即图中的【进程2】。当【进程1】挂掉后,【进程2】变成孤儿进程会被 tini 接管并回收。还有一种情况,如下图所示:
图6 tini 回收不了的僵尸进程
如果 entrypoint 产生的子进程变成了僵尸进程,那 tini 是回收不了的。因为要让【进程1】被 tini 接管,就必须把它的父进程 entrypoint 杀掉,而杀掉 entrypoint 也就意味着容器需要被重启。所以,这种情况下的僵尸进程 tini 是处理不了的。这种情况,也只能通过重启容器来清理僵尸进程。
TRANSLATE with x
English
TRANSLATE with
COPY THE URL BELOW
Back
EMBED THE SNIPPET BELOW IN YOUR SITE
Enable collaborative features and customize widget: Bing Webmaster Portal
Back
原创文章,作者:,如若转载,请注明出处:https://blog.ytso.com/276828.html