我们知道k8s中的Pod是抽象是基于Linux的namespace和cgroups,为容器提供了隔离的环境。从网络的角度看,在同一个Pod中如同在同一个主机上,可以通过localhost进行通信。
那么Pod是什么呢?为什么k8s最小的控制单位是Pod,而不是直接用Docker呢?Docker非常适合部署单个软件单元,但是当你想要在Docker中一起运行多个进程,就会变得麻烦和复杂了,k8s也非常不建议这种“富容器”的方式,认为将这些应用程序部署在部分隔离并且部分共享资源的容器中更为有用,所以k8s才提供了一个Pod的抽象概念。
原则上,任何人都可以配置Docker来控制容器组之间的共享级别,只需要创建一个父容器,在父容器中创建与父容器共享资源的新容器,然后管理这个新容器的生命周期。在k8s中,pause container就是作为Pod中的父容器。它为每个子容器提供一下几个功能:
在Pod中,它作为共享Linux namespace的基础(Network,UTS等);
启用PID namespace共享,提供1号进程,并搜集Pod内的僵尸进程。
什么是Pause Container
Pause Container中运行着一个非常简单的进程,不执行任何功能,只是启动后就sleep,可以参见源码pause.c, Pause Container Dockerfile
#include
#include
#include
#include
#include
#include
#include
#define STRINGIFY(x) #x
#define VERSION_STRING(x) STRINGIFY(x)
#ifndef VERSION
#define VERSION HEAD
#endif
static void sigdown(int signo) {
psignal(signo, "Shutting down, got signal");
exit(0);
}
static void sigreap(int signo) {
while (waitpid(-1, NULL, WNOHANG) > 0)
;
}
int main(int argc, char **argv) {
int i;
for (i = 1; i < argc; ++i) {
if (!strcasecmp(argv[i], "-v")) {
printf("pause.c %s\n", VERSION_STRING(VERSION));
return 0;
}
}
if (getpid() != 1)
/* Not an error because pause sees use outside of infra containers. */
fprintf(stderr, "Warning: pause should be the first process\n");
if (sigaction(SIGINT, &(struct sigaction){.sa_handler = sigdown}, NULL) < 0)
return 1;
if (sigaction(SIGTERM, &(struct sigaction){.sa_handler = sigdown}, NULL) < 0)
return 2;
if (sigaction(SIGCHLD, &(struct sigaction){.sa_handler = sigreap,
.sa_flags = SA_NOCLDSTOP},
NULL) < 0)
return 3;
for (;;)
pause();
fprintf(stderr, "Error: infinite loop terminated\n");
return 42;
}
可以看到,上面的代码在最后的死循环中会调用系统的pause,将自己阻塞住,从而达到"占用"着一个namespace的作用。除了"占用"namespace外,还有一个非常重要的功能,就是扮演pid 1的角色,并且在子进程成为"孤儿进程"的时候,调用wait()
方法收割这些僵尸进程(static void sigreap(int signo)
中),这也是为什么不能随便找一个Container来做为父容器,比如Nginx Container。
从namespace来看pause container
Pod是一个Docker container,也就是说如果我们在启动Docker container时,将pause container指定为要加入其namespace的容器,那么在Pod中的container在其中共享namespace。
例如在启动一个nginx container时,执行如下启动参数
docker run -d --name nginx -p 8080:80 --net=container:pause --ipc=container:pause --pid=container:pause nginx
此时,在启动一个博客应用container
docker run -d --name blog --net=container:pause --ipc=container:pause --pid=container:pause blog
这个时候nginx container和blog container共享一个namespace,若nginx为blog配置了本地8080端口,那么此时如果访问http://localhost:8080/ 那么应该能够看到blog通过nginx代理运行,因为blog,nginx和pause container之间共享network namespace。通过Pod,k8s屏蔽了以上的复杂度。
从PID看pause container
在unix系统中,PID为1的进程是init进程,即所有的进程的父进程。init进程比较特殊,它维护一张进程表并且不断地检查其他进程的状态。init进程的其中一个作用就是当某个子进程由于父进程的错误退出而变成了“孤儿进程”,便会被init进程“收养”,并且该进程退出时回收资源。每一个进程在操作系统进程表中都有一个条目,会记录有关进程的状态和退出代码,当子进程运行完成后,它的进程表条目仍然保留,直到父进程使用wait系统调用获取其退出代码后才会清理进程条目。这被称为“收割”僵尸进程,并且僵尸进程无法通过kill命令清除。
Container中使用PID namespace对PID进行隔离,因此每个Container中均可以有独立的init进程。当在主机上发送SIGKILL或者SIGSTOP强制终止Container的运行时,其实就是在终止Container内的init进程。一旦init进程被销毁,同一个PID namespace下的进程也就随之被销毁。使用Docker的话,ENTRYPOINT进程就是init进程。如果多个Container之间共享PID namespace,那么拥有PID namespace的那个进程就要担任init进程的角色,其他Container则作为init进程的子进程添加到PID namespace中。