引言
当你在k8s集群的node节点运行docker ps
命令时,你可能会注意到这些叫pause
的容器。
$ docker ps
CONTAINER ID IMAGE COMMAND ...
...
3b45e983c859 gcr.io/google_containers/pause-amd64:3.0 "/pause" ...
...
dbfc35b00062 gcr.io/google_containers/pause-amd64:3.0 "/pause" ...
...
c4e998ec4d5d gcr.io/google_containers/pause-amd64:3.0 "/pause" ...
...
508102acf1e7 gcr.io/google_containers/pause-amd64:3.0 "/pause" ...
这些 pause
容器是什么?为什么会有这么多的 pause
容器, 这到底是怎么一回事呢?为了回答这些问题,我们需要退后一步,看看k8s是如何实现pod的,尤其是在Docker/container runtime
中。
Docker支持容器,这在部署单体服务时非常有用。但是当要同时运行多个软件时,这个模式就遇到了麻烦,你会经常看到这种情形:开发人员使用supervisord
管理多个进程,但是在实际生产环境,许多人发现将这些应用部署在同一环境下互相隔离的容器组中会更有效。针对这种情况,k8s提供了一个抽象的概念叫做 pod
,它隐藏了docker flag
的复杂性以及容器与共享卷的挂载,同时也隐藏了 container runtime
的差异。举例来说,rkt
天然支持pod,所以使用k8s的你无需担心这些问题。
原则上,任何人都能通过配置docker去控制容器组之间的共享级别--你只需要创建一个父容器,知道怎么去设置正确的flag和如何在同一环境下实现共享,然后管理这些容器的生命周期,而管理所有这些容器的生命周期可能会变得非常复杂。在k8s的每一个pod中,这些pause
容器就担任这些pod的父容器这个角色。pause
容器有两个核心的功能:首先,它在pod中担任Linux命令空间共享的基础;其次,启用pid命名空间,开启init进程。
共享命名空间
在Linux中,当启动一个新进程时,子进程将继承父进程的命名空间,在新的命名空间运行新进程的方式是通过与父进程“取消共享”来实现的,从而达到创建一个新的命名空间的目的。这里有一个例子是使用unshare工具在新的PID, UTS, IPC运行shell的同时挂载新的命名空间。 $ sudo unshare --pid --uts --ipc --mount -f chroot rootfs /bin/sh
一旦这个进程运行起来了,新的进程就可以通过 `setns 系统调用加入到该进程的命名空间从而形成pod,pod中的容器彼此共享namespace,docker可以让这些过程稍稍自动化,因此现在来看一个示例,该示例将演示如何使用pause容器和共享namespace来从头创建一个pod。首先,使用docker创建一个pause容器,以便可以将其他容器加入到该pod。 $ docker run -d --name pause -p 8080:80 gcr.io/google_containers/pause-amd64:3.0
然后运行一个nginx容器,设置nginx容器的转发端口为2368。 注意,同时我将主机的8080端口映射到pause
容器的80端口,而不是nginx容器,因为nginx容器将加入到pause
容器的network namespace。
$ cat <<EOF >> nginx.conf
> error_log stderr;
> events { worker_connections 1024; }
> http {
> access_log /dev/stdout combined;
> server {
> listen 80 default_server;
> server_name example.com www.example.com;
> location / {
> proxy_pass http://127.0.0.1:2368;
> }
> }
> }
> EOF
$ docker run -d --name nginx -v `pwd`/nginx.conf:/etc/nginx/nginx.conf \
--net=container:pause --ipc=container:pause --pid=container:pause nginx
然后,我们将为ghost博客应用程序创建另一个容器,该容器用作我们的应用程序服务器。$ docker run -d --name ghost --net=container:pause --ipc=container:pause --pid=container:pause ghost
在这两种情况下,我们都将pause
容器指定为要加入其命名空间的容器,这将有效地创建我们的pod。访问http://localhost:8080/
应该能够看到通过nginx代理运行的ghost容器,因为网络名称空间在pause
,nginx和ghost容器之间共享。
如果你认为所有这些都很复杂,那么你是对的;而且我们甚至还没有涉及如何监视和管理这些容器的生命周期。Kubernetes的好处是通过Pod可以为您管理所有这些。
收割僵尸进程
在Linux中,PID名称空间中的进程形成一棵树,每个进程都有一个父进程。在树的根部只有一个进程实际上没有父进程,这就是具有PID 1的 init进程。
进程可以使用fork
和exec
系统调用来启动其他进程。当这样做时,新进程的父进程就是调用fork
系统调用的进程。fork
用于启动正在运行的进程的另一个副本,exec
用于用一个新的副本替换当前进程,并保持相同的PID(为了运行一个完全独立的应用程序,需要运行 fork
和 exec
系统调用。一个进程将创建一个新副本本身作为带有新PIDfork
的子进程,然后在子进程运行时检查它是否是子进程并运行exec
将其替换为你实际要运行的那个进程,大多数语言都提供了一种通过单个函数执行此操作的方法。每个进程在OS进程表中都有一个条目,这里面记录有关进程状态和退出代码的信息,子进程完成运行后,将保留其进程表条目,直到父进程使用wait
系统调用检测到其退出码为止,这称为收割僵尸进程。
僵尸进程是已经停止运行的进程,但是它们的进程表条目仍然存在,因为父进程尚未通过wait
系统调用检测到它。从技术上讲,每个终止进程在很短的时间内都是僵尸进程,但僵尸进程可以生存更长的时间。
当wait
子进程完成后,父进程不调用系统调用时,僵尸进程的寿命会更长。发生这种情况的一种情况是,当父进程编写不佳,忽略了wait
调用,或者当父进程在子进程之前死亡而新的父进程没有对其进行调用wait
时。当进程的父进程在子进程之前死亡时,操作系统会将子进程分配给init进程或PID1。即init进程“采用”子进程并成为其父进程。这意味着现在子进程退出时,必须调用新的父进程wait
以获取其退出代码,否则其进程表条目将永远保留并成为僵尸。
在容器中,一个进程必须是每个PID名称空间的初始化进程,对于Docker,每个容器通常都有自己的PID名称空间,而ENTRYPOINT
进程是init进程。但是,正如我在上一篇有关Kubernetes Pod的文章中所指出的那样,可以使一个容器在另一个容器的名称空间中运行。在这种情况下,一个容器必须承担init进程的角色,而其他容器则作为init进程的子级添加到名称空间中。
在Kubernetes Pod上的帖子中,我在容器中运行了Nginx,然后将ghost容器添加到了Nginx容器的PID名称空间中$ docker run -d --name nginx -v pwd`/nginx.conf:/etc/nginx/nginx.conf -p 8080:80 nginx
$ docker run -d --name ghost --net=container:nginx --ipc=container:nginx --pid=container:nginx ghost
在这种情况下,nginx承担PID 1的角色,并且将ghost添加为nginx的子进程,在多数情况下这没什么不好,但从技术上讲,nginx现在需要为任何成为孤儿的孩子负责,例如,如果ghost进程fork
自身或使用来运行子进程exec
,并在子进程完成前崩溃,那么nginx将采用这些子进程。但是,nginx并非旨在能够作为初始化进程运行并获得僵尸,这意味着我们可能有很多这样的容器,它们将在该容器的生命周期内持续使用。
在Kubernetes容器中,容器的运行方式与上述方法基本相同,但是为每个容器创建了一个特殊的pause容器。这个pause容器运行一个非常简单的进程,该进程不执行任何功能,但实际上会永远休眠(请参见pause()下面的调用)。非常简单,我可以在此处撰写本文时包含完整的源代码:
/ *
版权所有2016 The Kubernetes作者。
根据Apache许可证2.0版(“许可证”)获得许可;
除非遵守许可,否则您不得使用此文件。
您可以在
http://www.apache.org/licenses/LICENSE-2.0上
获得许可的副本。除非适用法律要求或以书面形式同意,否则
根据“许可”分发的
软件将按“现状”分发,没有任何明示或暗示的保证或条件。
有关许可下特定的语言管理权限和
限制,
请参见许可。* /
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys / types.h>
#include <sys / wait.h>
#include <unistd.h>
静态 void sigdown (int signo ) {
psignal (signo , “关机,收到信号” ));
退出(0 );
}
静态 无效 sigreap (INT SIGNO ) {
而 (waitpid函数(- 1 , NULL , WNOHANG ) > 0 );
}
int main () {
if (getpid () != 1 )
/ *这不是错误,因为暂停可以在红外线容器之外使用。* /
fprintf (stderr , “警告:暂停应该是第一个进程\ n ” );
if (sigaction (SIGINT , &(struct sigaction ){。sa_handler = sigdown }, NULL ) < 0 )
返回 1 ;
if (sigaction (SIGTERM , &(struct sigaction ){。sa_handler = sigdown }, NULL ) < 0 )
返回 2 ;
如果 (sigaction的(SIGCHLD , &(结构 的sigaction ){。sa_handler = sigreap ,
。其中sa_flags = SA_NOCLDSTOP },
NULL ) < 0 )
返回 3 ;
为 (;;)
暂停();
fprintf (stderr , “错误:无限循环终止\ n ” );
返回 42 ;
}
如你所见,它不只是睡眠,它将执行另一项重要功能:承担PID 1的角色,并且wait
在它们的父进程孤立了它们时将通过调用它们来获得任何僵尸进程(请参阅参考资料sigreap)。这样,我们就不会在Kubernetes Pod的PID namespace中堆积僵尸进程。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。