Cheyne's Blog


  • Home
  • Archive
  • Categories
  •    

© 2026 John Doe

Theme Typography by Makito

Proudly published with Hexo

为什么我们需要Pod?

Posted at 2025-02-02 Kubernetes 

在前面的文章中,详细介绍了 Kubernetes 中部署一个应用的过程。在这些讲解中,提到了这样一个知识点:Pod,是 Kubernetes 项目中最小的 API 对象。如果换一个更专业的说法,我们可以这样描述:Pod,是 Kubernetes 项目的原子调度单位。
不过,我相信你在学习和使用 Kubernetes 项目的过程中,已经不止一次地想要问这样的一个问题:为什么我们会需要 Pod?

是啊,我们花了那么多时间去解读 Linux 容器的原理、分析 Docker 容器的本质,终于,“Namespace 做隔离,Cgroups 做限制,rootfs 做文件系统”这样的“三句箴言”已经朗朗上口了,为什么 Kubernetes 项目又搞出一个 Pod 来呢?
要回答这个问题,我们还是要回忆一下一个问题:容器的本质到底是什么?显然,容器的本质是进程。
没错,容器就是未来云计算系统中的进程;容器镜像就是这个系统里的“.exe”安装包。那么 Kubernetes 就可以被看成是“操作系统”这样的角色。

现在,让我们登录到一台 Linux 机器里,执行一条如下所示的命令:

1
$ pstree -g

这条命令的作用,是展示当前系统中正在运行的进程的树状结构。它的返回结果如下所示:

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

不难发现,在一个真正的操作系统里,进程并不是“孤苦伶仃”地独自运行的,而是以进程组的方式,“有规则地”组织在一起。比如这里的 rsyslogd 程序,它负责的是 Linux 操作系统里的日志处理。可以看到,它的主程序 main,和它要用到的内核日志模块 imklog 等,同属于 1632 进程组。这些进程相互协作,共同完成 rsyslogd 程序的职责。
而 Kubernetes 项目所做的,其实就是将“进程组”的概念映射到了容器技术中,并使其成为了这个云计算“操作系统”里的“一等公民”。
Kubernetes 项目之所以要这么做的原因,就是因为 Google 的工程师们发现,他们部署的应用往往都存在着类似于“进程和进程组”的关系。更具体地说,就是这些应用之间有着密切的协作关系,使得他们必须部署在同一台机器上。
而如果事先没有“组”的概念,像这样的运维关系就会非常难以处理。
还是以 rsyslogd 为例。它由三个进程组成:一个 imklog 模块,一个 imuxsock 模块,一个 rsyslogd 自己的 main 函数主进程。这三个进程一定要运行在同一台机器上,否则,它们之间基于 Socket 的通信和文件交换,都会出现问题。

现在,如果我要把 rsyslogd 这个应用给容器化,由于受限于容器的“单进程模型”,这三个模块必须被分别制作成三个不同的容器。而在这三个容器运行的时候,它们设置的内存配额都是 1 GB。

容器的“单进程模型”并不是指容器只能运行“一个”进程,而是指容器没有管理多个进程的能力。这是因为容器里 PID=1 的进程就是应用本身,其他的进程都是这个 PID=1 进程的子进程。可是,用户编写的应用,并不能够像正常操作系统里的 init 进程或者 systemd 那样拥有进程管理的功能。比如你的应用是一个 Java Web 程序(PID=1),然后你执行 docker exec 在后台启动了一个 Nginx 进程(PID=3)。但是,当这个 Nginx 进程异常退出的时候,你怎么知道呢?这个进程退出后的垃圾收集工作,又由谁去做呢?

假设我们的 Kubernetes 集群上有两个节点:node-1 上有 3GB 可用内存,node-2 上有 2.5GB 可用内存。
这时,假设我要用 Docker Swarm 来运行这个 rsyslogd 程序,为了能够让这三个容器都运行在同一台机器上,我就必须在另外两个容器上设置一个 affinity=main(与 main 容器有亲密性)的约束,即:它们俩必须和 main 容器运行在同一台机器上。

然后,我顺序执行:docker run main,docker run imklog 和 docker run imuxsock,来创建这三个容器。
这样,这三个容器都会进入 Swarm 的待调度队列。然后,main 容器和 imklog 容器先后出队并被调度到了 node-2 上。
可是,当 imuxsock 容器出队开始被调度时,Swarm 就有点懵了:node-2 上的可用资源只有 0.5GB 了,不足以运行 imuxsock 容器;可是根据约束,它又只能运行在 node-2 上。
这就是一个典型的成组调度没有被妥善处理的例子。

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

除此之外,Pod 在 Kubernetes 项目中还有更重要的意义,那就是:容器设计模式。
首先,关于 Pod 最重要的一个事实是:它只是一个逻辑概念。
也就是说,Kubernetes 真正处理的,还是宿主机操作系统上 Linux 容器的 Namespace 和 Cgroups,而并不存在一个所谓的 Pod 的边界或者隔离环境。
那么,Pod 又是怎么被“创建”出来的呢?
答案是:Pod,其实是一组共享了某些资源的容器。
具体来说:Pod 里的所有容器,共享的是同一个 Network Namespace,并且可以声明共享同一个 Volume。

那这么来看的话,一个有 A、B 两个容器的 Pod,不就等同于一个容器共享另外一个容器的网络和 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 容器关联在一起。很容易理解,这个 Infra 容器一定只能占有极少的资源,所以它使用的是一个非常特殊的镜像,叫做:k8s.gcr.io/pause。这个镜像是用汇编语言写的、永远处于“暂停”的状态,解压后的大小也只有 100~200KB 左右。

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

有了这个设计之后,共享 Volume 就简单多了:Kubernetes 项目只需要把所有 Volume 的定义都设计在 Pod 层级即可。
这样,一个 Volume 对应的宿主机目录对于 Pod 来说就只有一个,Pod 里的容器只要声明挂载这个 Volume,就一定可以共享这个 Volume 对应的宿主机目录。比如下面这个例子:

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

明白了 Pod 的实现原理后,我们再来讨论“容器设计模式”,就容易多了。
Pod 这种“超亲密关系”容器的设计思想,实际上就是希望,当用户想在一个容器里跑多个功能并不相关的应用时,应该优先考虑它们是不是更应该被描述成一个 Pod 里的多个容器。

总结

所以,你现在可以这么理解 Pod 的本质:

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

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

Share 

 Previous post: 深入解析Pod对象(一):基本概念 Next post: Kubernetes一件部署利器:kubeadm 

© 2026 John Doe

Theme Typography by Makito

Proudly published with Hexo