Pod 是 Kubernetes项目中最小的 API 对象。
Pod 是 Kubernetes 项目的原子调度单位。
Namespace 做隔离,Cgroups 做限制,rootfs 做文件系统
未来云计算系统中,容器就是进程,容器镜像就是安装包,Kubernetes 就是操作系统。

进程组映射至容器技术中

展示当前系统中正在运行的进程的树状结构。

  1. $ pstree -g
  2. systemd(1)-+-accounts-daemon(1984)-+-{gdbus}(1984)
  3. | `-{gmain}(1984)
  4. |-acpid(2044)
  5. ...
  6. |-lxcfs(1936)-+-{lxcfs}(1936)
  7. | `-{lxcfs}(1936)
  8. |-mdadm(2135)
  9. |-ntpd(2358)
  10. |-polkitd(2128)-+-{gdbus}(2128)
  11. | `-{gmain}(2128)
  12. |-rsyslogd(1632)-+-{in:imklog}(1632)
  13. | |-{in:imuxsock) S 1(1632)
  14. | `-{rs:main Q:Reg}(1632)
  15. |-snapd(1942)-+-{snapd}(1942)
  16. | |-{snapd}(1942)
  17. | |-{snapd}(1942)
  18. | |-{snapd}(1942)
  19. | |-{snapd}(1942)

在一个真正的操作系统中,进程是以进程组的方式,“有原则的”组织在一起。

注:此处的“进程”,如 rsyslogd 对应的 imklog,imuxsock 和 main,严格说是 Linux 操作系统语境下的“线程”。线程或者说轻量级进程之间,可以共享文件、信号、数据内存、甚至部分代码,从而紧密协作共同完成一个程序的职责。所以,“进程组”对应 Linux 的“线程组”。

Kubernetes 项目所做的,就是将“进程组”的概念映射到容器技术中。因为在 Borg 项目的开发实践中,部署的应用,往往存在着类似于“进程和进程组”的关系。更具体,这些应用之间有着密切的协作关系,使它们必须部署在同一台机器上。

rsyslogd 例子

rsyslogd 的例子,三个进程要运行在同一台机器上,否则它们之间的 Socket 通信和文件交换,都会出现问题。
现将 rsyslogd 容器化,受限于容器的“单进程模型”,三个模块要分别制作成三个容器。在运行的时候,内存配额都为 1 GB。

容器的“单进程模型”是指容器没有管理多个进程的能力。

假设 Kubernetes 集群上有两个节点,node-1 有 3 GB 可用内存,node-2 有 2.5 GB 可用内存。
假设使用 Docker Swarm 运行 rsyslogd 程序,为了使这三个容器都运行在同一机器上,就必须在另外两个容器上设置一个 affinity=main(与 main 容器有亲密性)的约束,即它俩必须和 main 容器运行在同一台机器上。
然后顺序执行 docker run main docker run imklog docker run imuxsock
这样三个容器都会进入 Swarm 的待调度队列。然后,main 和 imklog 都先后出队并被调度到 node-2 上(这种情况是可能的)。
这是,当 imuxsock 出队被调度时:node-2 可用资源只有 0.5 GB了,但根据 affinity=main 约束,imuxsock 只能运行在 node-2 上。
这是一个典型的 成组调度(gang scheduling) 没有被妥善处理的例子。

Mesos 中有资源屯集(resource hoarding)机制,会在所有设置了 Affinity 约束的任务都达到时,才开始进行统一调度。
Google Omega 论文中,提出使用乐观调度处理冲突,即:先不管冲突,通过精心设计的回滚机制在出现了冲突之后解决问题。
上两种方法都不完美,资源囤积带来了不可避免的调度效率损失和死锁的可能性;乐观调度有过高的复杂程度。

在 Kubernetes 中:Pod 是 Kubernetes 的原子调度单位。意味着, Kubernetes 项目的调度器,是统一按照 Pod 而非容器的资源需求进行计算的。
像 imklog、imuxsock 和 main 函数主进程,这三个容器,是一个由三个容器组成的 Pod。Kubernetes 项目在调度时,会去选择可用内存等于 3 GB 的 node-1 节点进行绑定,不会去考虑 node-2。
像这样容器间的紧密协作,可称为“超亲密关系”。特性包括但不限于:互相之间会发生直接的文件交换、使用 localhost 或 Socket 文件进行本地通信、会发生非常频繁的远程调用、需要共享某些 Linux Namespace(如一个容器要加入另一个容器的 Network Namespace)。
意味着,并不是所有“关系”容器都属于同一个 Pod。如 Golang 应用容器和 MySQL 虽会发生访问关系,但没必要也不应部署在同一机器上,它们更适合做成两个 Pod。

Pod 为何是“一等公民”

Pod 在 Kubernetes 项目中更重要的意义: 容器设计模式

  • Pod 实现原理

Pod 最重要的一个事实:它知识一个逻辑概念。
Kubernetes 真正处理的,还是宿主机操作系统上 Linux 容器的 Namespace 和 Cgroups,并不存在所谓的 Pod 的边界或隔离环境。
Pod 其实是一组共享了某些资源的容器。 具体说, Pod 里所有容器,共享的是同一个 Network Namespace,并可以声明共享同一个 Volume。
似乎通过 docker run --net --volumes-from 命令可以实现,如:

  1. $ docker run --net=B --volumes-from=B --name=A image-A ...

但这样,B 必须比 A 先启动,这样 Pod 中的多个容器就不是对等关系,而是拓扑关系了。
所以,在 Kubernetes,Pod 的实现需要使用一个中间容器:Infra 容器。在 Pod 中,Infra 容器永远都是第一个被创建,而其他用户定义的容器,则通过 Join Network Namespace 的方式,与 Infra 容器关联在一起。
image.png

如上,这个 Pod 里有两个用户容器 AB,还有 Infra 容器。在 Kubernetes 里,Infra 容器一定要占用极少的资源,所以它使用的是一个非常特殊的镜像:k8s.gcr.io/pause。这个镜像由汇编编写、永远处于“暂停”状态,解压后大小只有 100~200 KB 左右。
用户容器加入到 Infra 容器的 Network Namespace 中。
对于 Pod 里的 容器 A 和 B:

  • 它们可以直接使用 localhost 进行通信;
  • 它们看到的网络设备跟 Infra 容器看到的完全一样;
  • 一个 Pod 只有一个 IP 地址,就是这个 Pod 的 Network Namespace 对应的 IP 地址;
  • 其他的所有网络资源,都是一个 Pod 一份,并被该 Pod 中所有容器共享;
  • Pod 的生命周期只跟 Infra 容器一致,与容器 A 和 B 无关。

对于同一 Pod 中的所有用户容器来说,进出流量,可以认为都是通过 Infra 容器完成的。将来如果要为 Kubernetes 开发一个网络插件时,应该重点考虑如何配置这个 Pod 的 Network Namespace,而不是每个用户容器如何使用网络配置。
这意味着,如果网络插件需要在容器里安装某些包或者配置才能完成的话,是不可取的:Infra 容器镜像的 rootfs 里几乎什么都没有,没有随意发挥的空间。这同时也意味着网络插件完全不必关心用户容器的启动与否,只需要关注如何配置 Pod,Infra 容器的 Network Namespace 即可。

Volume
Kubernetes 项目只要把所有 Volume 的定义都设计在 Pod 层级即可共享 Volume。

  1. apiVersion: v1
  2. kind: Pod
  3. metadata:
  4. name: two-containers
  5. spec:
  6. restartPolicy: Never
  7. volumes:
  8. - name: shared-data
  9. hostPath:
  10. path: /data
  11. containers:
  12. - name: nginx-container
  13. image: nginx
  14. volumeMounts:
  15. - name: shared-data
  16. mountPath: /usr/share/nginx/html
  17. - name: debian-container
  18. image: debian
  19. volumeMounts:
  20. - name: shared-data
  21. mountPath: /pod-data
  22. command: ["/bin/sh"]
  23. args: ["-c", "echo Hello from the debian container > /pod-data/index.html"]

上例中,debian-container 和 nginx-container 都声明挂载了 shared-data 这个 Volume。而 shared-data 是 hostPath 类型。所以,它对应在宿主机上的目录就是:/data。而这个目录,其实就被同时绑定挂载进了上述两个容器当中。
所以,nginx-container 可从它的 /usr/share/nginx/html 目录中,读取到 debian-container 生成的 index.html 文件。

容器设计模式

Pod 这种“超亲密关系”容器的设计思想,就是希望,当用户想在一个容器里跑多个功能并不相关的应用时,应该优先考虑它们是不是更应该被描述成一个 Pod 里的多个容器。

例如:WAR 包与 Web 服务器。
现有 Java Web 应用的 WAR 包,需被放在 Tomcat 的 webapps 目录下运行。
只能使用 Docker 的情况下,处理组合关系的方式:

  • 把 WAR 包直接放在 Tomcat 镜像的 webapps 目录下,做成一个新镜像运行。但如果要升级 WAR 包或 Tomcat 镜像,又要重新制作新的发布镜像。
  • 只发布一个 Tomcat 容器,容器的 webapps 目录,需声明 hostPath 类型的 Volume,把宿主机上的 WAR 包挂载进 Tomcat 容器。但需要分布式存储系统解决 WAR 包。

使用 Pod,就可以把 WAR 包和 Tomcat 分别做成镜像“组合”在一起,配置文件如下:

apiVersion: v1
kind: Pod
metadata:
  name: javaweb-2
spec:
  initContainers:
  - image: geektime/sample:v2
    name: war
    command: ["cp", "/sample.war", "/app"]
    volumeMounts:
    - mountPath: /app
      name: app-volume
  containers:
  - image: geektime/tomcat:7.0
    name: tomcat
    command: ["sh","-c","/root/apache-tomcat-7.0.42-v2/bin/start.sh"]
    volumeMounts:
    - mountPath: /root/apache-tomcat-7.0.42-v2/webapps
      name: app-volume
    ports:
    - containerPort: 8080
      hostPort: 8001 
  volumes:
  - name: app-volume
    emptyDir: {}

在 Pod 中,所有 initContainers 定义的容器,都会比 sprc.containers 定义的容器先启动。并且,initContainers 容器会按顺序逐一启动,知道它们都启动并退出,用户容器才会启动。
这种所谓的“组合”操作,是容器设计模式最常用的模式,叫: sidecar 。指可以在一个 Pod中,启动一个辅助容器,来完成一些独立主进程(主容器)之外的工作。
上例中,Tomcat 就是主容器。WAR 包容器扮演了 sidecar 角色。

例如:容器的日志收集。
一个应用,不断把日志文件输出到容器的 /var/log 中。
把 Pod 里的 Volume 挂载到应用容器的 /var/log 上。
在 Pod 里运行一个 sidecar 容器,也声明挂载同一个 Volume 到自己的 /var/log 上。
这样,sidecar 容器就不断从自己的 /var/log 里读取日志,转发到 MongoDB 或 Elasticsearch 中存储。
使用共享的 Volume 来完成对文件的操作。

Pod 的另一个重要特性,它的所有容器都共享同一个 Network Namespace。使很多与 Pod 网络相关的配置和管理,都可交给 sidecar 完成,而完全无需干涉用户容器。
如 Istio:Istio 项目使用 sidecar 容器完成微服务治理。

Pod 的本质:实际上是在扮演传统基础设施里“虚拟机”的角色;而容器,则是这个虚拟机里运行的用户程序。

当需要把一个运行在虚拟机里的应用迁移到 Docker 容器中时,一定要仔细分析到底有哪些进程(组件)运行在这个虚拟机里。
然后,就可以把整个虚拟机想象成为一个 Pod,把这些进程分别做成容器镜像,把有顺序关系的容器,定义为 Init Container。
这是合理的、松耦合的容器编排诀窍,也是从传统应用架构,到“微服务架构”最自然的过渡方式。

注意:Pod 这个概念,提供的是一种编排思想,而不是具体的技术方案。所以,如果愿意,完全可以使用虚拟机来作为 Pod 的实现,然后把用户容器都运行在这个虚拟机里。比如,Mirantis 公司的 virtlet 项目。甚至,可以去实现一个带有 Init 进程的容器项目,来模拟传统应用的运行方式。这些工作,在 Kubernetes 中都非常容易,也和 CRI 会有关。