为什么需要Pod
假设k8s中调度的基本单元就是容器,对于一个非常简单的应用可以直接调度直接使用,这是没有什么问题,但是往往很多应用程序是由多个进程组成的,(理论上可以将这些进程打包到一个容器中),但是Docker管理的进程是pid=1的主进程,其他进程死掉了就会成为僵尸进程,没有办法进行管理。
如果把这个应用的进程进行拆分,拆分成一个个的容器,这样做有可能出现一个应用下面的某个进程容器被调度到了不同的节点上,但是我们的应用内部的进程与进程间的通信(通过IPC或者共享本地文件之类)都要求在本地进程,也就是需要在同一个节点上运行。
所以需要一个更高级别的结构来将这些容器绑定在一起,并将他们作为一个基本的调度单元进行管理,这样就可以保证这些容器始终在同一个节点上,这就是Pod的设计初衷。
Pod原理
在一个Pod下面运行几个关系非常密切的容器进程,这样一来这些进程本身既可以受到容器的管控,又具有几乎一致的运行环境。
其实Pod也只是一个逻辑概念,真正起作用的还是Linux容器的Namespace和Cgroup这两个最基本的概念,Pod被创建出来其实就是一组共享了一些资源的容器而已。首先Pod里面的所有容器都是共享同一个Network Namespace,但是涉及到文件系统的时候,默认情况下Pod里面的容器之间的文件系统是完全隔离的,但是我们可以通过声明来共享同一个Volume。
在Docker网络模式中有一个Container模式,我们可以指定新创建的容器和一个已经存在的容器共享一个Network Namespace,在运行容器的时候只需要指定--net=container:目标容器名这个参数就可以,但是这种模式有一个明显的问题那就是容器的启动有先后顺序,那么Pod是如何解决这个问题的?那就是加入一个中间容器,这个容器叫做infra容器,而且这个容器在Pod中永远是第一个被创建的,这样其他容器都加入到这个infra容器就可以了,这样就完全实现了Pod中所有容器都和infra容器共享同一个Network Namespace。如下图:
所以当我们部署完k8s集群的时候,首先需要保证在所有的节点上都可以拉取到默认的infra镜像,默认情况下infra镜像地址为k8s.gcr.io/pause:3.1,这个容器占用的资源非常少,但是这个镜像默认需要科学上网,所以很多时候我们在部署应用的时候一直处于Pending状态,因为所有Pod最先启动的容器镜像都拉不下来:
$ kubelet --help |grep infra--pod-infra-container-image string The image whose network/ipc namespaces containers in each pod will use. This docker-specific flag only works when container-runtime is set to docker. (default "k8s.gcr.io/pause:3.1")
从上图可以看出普通的容器加入到了infra容器的Network Namespace中,所以这个Pod下面的所有容器就是共享同一个Network Namespace了,普通容器不会创建自己的网卡,配置自己的IP,而是和infra容器共享IP、端口范围,而且容器之间的进程可以通过lo网卡设备进行通信:
- 也就是容器之间可以直接使用localhost进行通信;
- 看到的网络设备信息都是和infra容器完全一样的;
- 也就意味同一个Pod下面的容器运行的多个进程不能绑定相同的端口;
- 而且Pod的生命周期和infra一直,而与容器A和B无关;
对于文件系统k8s是怎么实现让一个Pod中的容器共享的?默认情况下容器的文件系统是互相隔离的,要实现共享只需要在Pod的顶层声明一个Volume,然后在需要共享这个Volume的容器中声明挂载即可。
比如下面这个示例:
apiVersion: v1
kind: Pod
metadata:
name: counter
spec:
volumes:
- name: varlog
hostPath:
path: /var/log/counter
containers:
- name: count
image: busybox
args:
- /bin/sh
- -c
- >
i=0;
while true;
do
echo "$i: $(date)" >> /var/log/1.log;
i=$((i+1));
sleep 1;
done
volumeMounts:
- name: varlog
mountPath: /var/log
- name: count-log
image: busybox
args: [/bin/sh, -c, 'tail -n+1 -f /var/log/1.log']
volumeMounts:
- name: varlog
mountPath: /var/log
示例中我们在Pod的顶层声明了一个名为varlog的Volume,而这个Volume的类型是hostPath,就意味这个宿主机的/var/log/counter目录将被这个Pod共享,只需要在用到这个数据目录的容器上声明挂载即可,也就是通过volumeMounts声明挂载的部分,这样这个Pod就实现了共享容器/var/log目录,而且数据被持久化到了宿主机目录上。
这个方式也是k8s中一个非常重要的设计模式:sidecar模式。典型的场景就是容器日志收集,比如上面的这个应用,其中应用的日志是被输出到容器的/var/log目录上的,这个时候可以把Pod声明的Volume挂载到容器的/var/log目录上,然后在这个Pod里面同时运行一个sidecar容器,他也声明挂载相同的Volume到自己的容器的/var/log目录上,这样我们这个sidecar容器就只需要从/var/log目录下面不断消费日志发送到Elasticsearch中存储起来就能完成最基本的应用日志收集。
除了这个应用场景之外使用更多的还是利用Pod中的所有容器共享同一个Network Namespace这个特性,这样我们就可以把Pod网络相关的配置和管理交给一个sidecar容器来完成,完全不需要去干涉用户容器,这个特性在现在非常火热的Service Mesh(服务网格)中应用非常光蛋,典型的应用就是istio。
