Kubernetes网络自学系列 | Pod的核心:pause容器
Posted COCOgsta
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Kubernetes网络自学系列 | Pod的核心:pause容器相关的知识,希望对你有一定的参考价值。
素材来源:《Kubernetes网络权威指南》
一边学习一边整理内容,并与大家分享,侵权即删,谢谢支持!
附上汇总贴:Kubernetes网络自学系列 | 汇总_COCOgsta的博客-CSDN博客
3.3 Pod的核心:pause容器
当检查Kubernetes集群的节点时,在节点上执行命令docker ps,用户可能会注意到一些被称为pause的容器,例如:
Kubernetes中的pause容器可以说是网络模型的精髓,理解pause容器能够更好地理解Kubernetes Pod的设计初衷。
我们知道Kubernetes的Pod抽象基于Linux的namespace和cgroups,为容器提供了隔离的环境。从网络的角度看,同一个Pod中的不同容器犹如运行在同一个主机上,可以通过localhost进行通信。
为什么要发明Pod呢?直接使用Docker容器不好吗?Docker容器非常适合部署单个软件单元。但是当你想要一起运行多个软件时,尤其是在一个容器里管理多个进程时,这种模式会变得有点麻烦。Kubernetes非常不建议“富容器”这种方式,认为将这些应用程序部署在部分隔离并且部分共享资源的容器组中更为有用。为此,Kubernetes为这种使用场景提供了一个称为Pod的抽象。
原则上,任何人都可以配置Docker来控制容器组之间的共享级别——只需创建一个父容器,并创建与父容器共享资源的新容器,然后管理这些容器的生命周期。在Kubernetes中,pause容器被当作Pod中所有容器的“父容器”,并为每个业务容器提供以下功能:
· 在Pod中,它作为共享Linux namespace(Network、UTS等)的基础;
· 启用PID namespace共享,它为每个Pod提供1号进程,并收集Pod内的僵尸进程。pause容器源码在Kubernetes中,pause容器运行着一个非常简单的进程,它不执行任何功能,基本上是永远“睡觉”的,源代码在Kubernetes项目的build/pause/目录中。它比较简单,完整的源代码如下所示:
如上所示,这个pause容器运行一个非常简单的进程,它不执行任何功能,一启动就永远把自己阻塞住了(见pause()系统调用)。正如你看到的,它当然不会只知道“睡觉”。它执行另一个重要的功能——即它扮演PID 1的角色,并在子进程成为“孤儿进程”的时候,通过调用wait()收割这些僵尸子进程。这样就不用担心我们的Pod的PID namespace里会堆满僵尸进程了。这也是为什么Kubernetes不随便找个容器(例如nginx)作为父容器,让用户容器加入的原因。
1. 从namespace看pause容器
第1章介绍过,在Linux系统中运行新进程时,该进程从父进程继承了其namespace。在 namespace中运行进程的方法是通过取消与父进程的共享namespace,从而创建一个新的namespace。以下是使用unshare工具在新的PID、UTS、IPC和Mount namespace中运行shell的示例。
一旦进程运行,用户可以将其他进程添加到该进程的namespace中以形成一个Pod,Pod中的容器在其中共享namespace。读者可以使用第1章提到的setns系统调用将新进程添加到现有namespace,Docker也提供命令行功能自动完成此过程。下面来看一下如何使用pause容器和共享namespace创建Pod。
首先,我们使用Docker启动pause容器,以便可以将其他容器添加到Pod中,如下所示:
然后,我们在Pod中运行其他容器,分别是Nginx代理和ghost博客应用。
Nginx代理的后端配置成http://127.0.0.1:2368,也就是ghost进程监听的地址,如下所示:
为ghost博客应用程序创建另一个容器,如下所示:
在这个例子中,我们将pause容器指定为要加入其namespace的容器。如果访问http://localhost:8080/,那么应该能够看到ghost通过Nginx代理运行,因为pause、Nginx和ghost容器之间共享networknamespace,如图3-9所示。
通过Pod,Kubernetes屏蔽了以上所有复杂度。
图3-9 Pod的底层实现原理
2. 从PID看pause容器
在UNIX系统中,PID为1的进程是init进程,即所有进程的父进程。init进程比较特殊,它维护一张进程表并且不断地检查其他进程的状态。init进程的其中一个作用是当某个子进程由于父进程的错误退出而变成了“孤儿进程”,便会被init进程“收养”并在该进程退出时回收资源。
进程可以使用fork和exec两个系统调用启动其他进程。当启动了其他进程后,新进程的父进程就是调用fork系统调用的进程。fork用于启动正在运行的进程的另一个副本,而exec则用于启动不同的进程。每个进程在操作系统进程表中都有一个条目。这将记录有关进程的状态和退出代码。当子进程运行完成后,它的进程表条目仍然保留,直到父进程使用wait系统调用获得其退出代码后才会清理进程条目。这被称为“收割”僵尸进程,并且僵尸进程无法通过kill命令清除。
僵尸进程是已停止运行但进程表条目仍然存在的进程,父进程尚未通过wait系统调用进行检索。从技术层面来说,终止的每个进程都算是一个僵尸进程,尽管只是在很短的时间内发生的。当用户程序写得不好并且简单地省略wait系统调用,或者当父进程在子进程之前异常退出并且新的父进程没有调用wait检索子进程时,会出现较长时间的僵尸进程。系统中存在过多僵尸进程将占用大量操作系统进程表资源。
当进程的父进程在子进程完成前退出时,OS将子进程分配给init进程。init进程“收养”子进程并成为其父进程。这意味着当子进程退出时,新的父进程(init进程)必须调用wait获取其退出代码,否则其进程表项将一直保留,并且它也将成为一个僵尸进程。同时,init进程必须拥有“信号屏蔽”功能,不能处理某个信号逻辑,从而防止init进程被误杀。所以不是随随便便一个进程都能当init进程的。
容器使用PID namespace对PID进行隔离,因此每个容器中均可以有独立的init进程。当在主机上发送SIGKILL或者SIGSTOP(也就是docker kill或者docker stop命令)强制终止容器的运行时,其实就是在终止容器内的init进程。一旦init进程被销毁,同一PID namespace下的进程也随之被销毁。
在容器中,必须要有一个进程充当每个PID namespace的init进程,使用Docker的话,ENTRYPOINT进程是init 进程。如果多个容器之间共享PID namespace,那么拥有PID namespace的那个进程须承担init进程的角色,其他容器则作为init进程的子进程添加到PID namespace中。
为了给读者一个直观的印象,下面给出一个例子来说明用户容器和pause容器的PID关系。
先启动一个pause容器:
再运行一个busybox容器,加入pause容器的namespace(Network、PID、IPC)中:
上述这种加入pause容器的方式也是Kubernetes启动Pod的原理。
接下来,让我们进入busybox容器查看里面的进程,发现里面PID=1的进程是/pause:
我们完全可以在父容器中运行Nginx,并将ghost添加到Nginx容器的PID命名空间:
在这种情况下,Nginx将承担PID 1的作用,并将ghost添加为Nginx的子进程。虽然这样貌似不错,但从技术角度看,Nginx需要负责ghost进程的所有子进程。例如,如果ghost在其子进程完成之前异常退出,那么这些子进程将被Nginx收养。但是,Nginx并不是设计用来作为一个init进程运行并收割僵尸进程的。这意味着将会有很多这种僵尸进程,并且这种情况将持续整个容器的生命周期。
最后总结一句,Pod的init进程,pause容器舍它其谁?
3. 在Kubernetes中使用PID namespace共享 / 隔离
共享/隔离Pod内容器的PID namespace是一个见仁见智的问题。支持共享的人觉得方便了进程间通信,例如可以在容器中给另外一个容器内的进程发送信号,还不用担心僵尸进程回收问题。
在Kubernetes 1.8版本之前,默认是启用PID namespace共享的,除非将Kubelet的标志--docker-disable-shared-pid设置成true,来禁用PID namespace共享。然而在Kubernetes 1.8版本 以后,情况刚好相反,在默认情况下,Kubelet标志--docker-disable-sharedpid设置成true,如果要开启,还要设置成false。下面就来看看Kubernetes提供的关于是否共享PID namespace的downward API。
如上所示,podSpec.shareProcessNamespace指示了是否启用PID namespace共享。
通过前文的讨论,我们知道Pod内容器共享PID namespace是很有意义的,那为什么还要开放这个禁止PID namesapce共享的开关呢?那是因为当应用程序不会产生其他进程,而且僵尸进程带来的问题可以忽略不计时,就用不到PID namespace的共享了。在有些场景下,用户希望Pod内容器能够与其他容器隔离PID namespace,例如下面两个场景:
(1)PID namespace共享时,由于pause容器成了PID=1,其他用户容器就没有PID 1了。但像systemd这类镜像要求获得PID 1,否则无法正常启动。有些容器通过kill -HUP 1命令重启进程,然而在由pause容器托管init进程的Pod里,上面这条命令只会给pause容器发信号。
(2)PID namespace共享Pod内不同容器的进程对其他容器是可见的。这包括/proc中可见的所有信息,例如,作为参数或环境变量传递的密码,这将带来一定的安全风险。
以上是关于Kubernetes网络自学系列 | Pod的核心:pause容器的主要内容,如果未能解决你的问题,请参考以下文章
Kubernetes网络自学系列 | 终于等到你:Kubernetes网络
Kubernetes网络自学系列 | 打通CNI与Kubernetes:Kubernetes网络驱动