22
33字面上,“容器”这个词难以让人形象地理解其真正含义,Kubernetes 中最核心的概念“Pod”也是如此。
44
5- 仅靠几句简单的解释并不足以让人充分理解这些概念,甚至可能引发误解。例如,业内常常将容器与轻量级虚拟机混为一谈,如果容器类似于虚拟机,那么应该存在一种通用的方法 ,能够无缝地将虚拟机内的应用迁移至容器中,但现实中并不存在这种方法。
5+ 仅靠几句简单的解释并不足以让人充分理解这些概念,甚至可能引发误解,如业内常常将容器与轻量级虚拟机混为一谈。如果容器类似虚拟机,那么应该存在一种普适的方法 ,能够无缝地将虚拟机内的应用迁移至容器中,但现实中并不存在这种方法。
66
7- 本节,笔者将从最初的文件系统隔离开始,逐步介绍容器在不同历史阶段的作用,深入理解容器技术的演进,以及 Kubernetes 中最核心的概念 Pod 的设计背景与应用 。
7+ 本节,笔者将从最初的文件系统隔离开始,逐步介绍容器在不同历史阶段的作用,深入理解容器技术的演进,以及 Kubernetes 中最核心的概念 Pod 的设计背景和应用 。
88
99## 7.2.1 文件系统隔离
1010
11- 容器的起源可以追溯到 1979 年 UNIX 系统中引入的 chroot 命令[ ^ 1 ] 。chroot 是“change root”的缩写,它允许管理员将进程的根目录锁定在特定位置,从而限制进程对文件系统的访问范围。
11+ 容器的起源可以追溯到 1979 年 UNIX 系统中引入的 chroot 命令[ ^ 1 ] 。
1212
13- chroot 的隔离功能对安全性至关重要,例如可以创建一个 “蜜罐”,用来安全地运行和监控可疑代码或程序。因此,chroot 环境也被形象地称为“jail”(监狱),而突破 chroot 的过程则被称为“越狱”。
13+ chroot 是“change root”的缩写,它允许管理员将进程的根目录锁定在特定位置,从而限制进程对文件系统的访问范围。chroot 的隔离功能对安全性至关重要。例如,可以创建一个 “蜜罐”,用来安全地运行和监控可疑代码或程序。因此,chroot 环境也被形象地称为“jail”(监狱),而突破 chroot 的过程则被称为“越狱”。
1414
1515时至今日,chroot 命令仍然活跃于主流的 Linux 系统中。在绝大部分 Linux 系统中,只需几步操作,就可以为进程创建一个文件隔离环境。
1616
@@ -41,7 +41,9 @@ root@028f46a5b7db:/# cd bin
4141root@028f46a5b7db:/bin# pwd
4242/bin
4343```
44- 虽然 chroot 看起来与容器相似,都是创建与宿主机隔离的文件系统环境,但这并不意味着 chroot 就是容器。chroot 只是改变了进程的根目录,并未创建真正独立、安全的隔离环境。在 Linux 系统中,从低层次的资源(如网络、磁盘、内存、处理器)到操作系统控制的高层次资源(如 UNIX 分时、进程 ID、用户 ID、进程间通信),都存在大量非文件暴露的操作入口。
44+ 虽然 chroot 看起来与容器相似,都是创建与宿主机隔离的文件系统环境,但这并不意味着 chroot 就是容器。
45+
46+ chroot 只是改变了进程的根目录,并未创建真正独立、安全的隔离环境。在 Linux 系统中,从低层次的资源(如网络、磁盘、内存、处理器)到操作系统控制的高层次资源(如 UNIX 分时、进程 ID、用户 ID、进程间通信),都存在大量非文件暴露的操作入口。
4547
4648因此,无论是 chroot,还是针对 chroot 安全问题改进后的 pivot_root,都无法实现对资源的完美隔离。
4749
@@ -90,7 +92,7 @@ int pid = clone(main_function, stack_size, flags | SIGCHLD, NULL);
9092
9193cgroups 是 Linux 内核中用于隔离、分配并限制进程组使用资源配额的机制。例如用来控制进程 CPU 占用时间、内存的大小、磁盘 I/O 速度等。该项目由 Google 工程师(主要是 Paul Menage 和 Rohit Seth)在 2000 年发起,当时取名字叫“进程容器”(Process container)。不过,在 Linux 内核中,容器(container)这个名词有许多不同的意义。为了避免与其他“容器”相关概念混淆,于是被重命名为 cgroups 。
9294
93- 2008 年,cgroups 合并到 Linux 内核 2.6.24 版本 后正式对外发布,这一阶段的 cgroups 被称为第一代 cgroups。2016 年 3 月发布的 Linux 内核 4.5 中引入了由 Facebook 工程师 Tejun Heo 重新编写的“第二代 cgroups”。相较于 v1 版本,第二代 cgroups 提供了更加统一的资源控制接口,使得对 CPU、内存、I/O 等资源的限制更加一致和统一。不过由于兼容性和稳定性原因 ,目前多数容器运行时(container runtime)默认使用的仍然是第一代 cgroups。
95+ 2008 年,cgroups 合并到 Linux 内核 2.6.24 版本 后正式对外发布,这一阶段的 cgroups 被称为第一代 cgroups。2016 年 3 月发布的 Linux 内核 4.5 中引入了由 Facebook 工程师 Tejun Heo 重新编写的“第二代 cgroups”。相较于 v1 版本,第二代 cgroups 提供了更加统一的资源控制接口,使得对 CPU、内存、I/O 等资源的限制更加一致和统一。不过,由于兼容性和稳定性原因 ,目前多数容器运行时(container runtime)默认使用的仍然是第一代 cgroups。
9496
9597Linux 系统通过文件系统向用户暴露 cgroups 的操作接口,这些接口以文件和目录的形式组织在 /sys/fs/cgroup 路径下。在 Linux 中执行 ls /sys/fs/cgroup 命令,可以看到在该路径下有许多子目录,如 blkio、cpu、memory 等。
9698
@@ -113,7 +115,7 @@ cgroup.sane_behavior memory.memsw.usage_in_bytes
113115```
114116这些文件各自具有不同的作用。例如,memory.kmem.limit_in_bytes 文件用于限制应用的总内存使用;memory.stat 用于统计内存使用情况;memory.failcnt 文件报告内存使用达到 memory.limit_in_bytes 设定的限制值的次数,等等。
115117
116- 目前,主流的 Linux 系统支持的控制组群子系统如表 7-2 所示。
118+ 目前,主流的 Linux 系统支持的控制组子系统如表 7-2 所示。
117119
118120::: center
119121表 7-2 cgroups 控制组群子系统
@@ -133,7 +135,7 @@ cgroup.sane_behavior memory.memsw.usage_in_bytes
133135
134136Linux cgroups 的设计简单易用。对于 Docker 等容器系统,它们只需在每个子系统下为每个容器创建一个控制组(通过新建目录的方式),然后在容器进程启动后,将进程的 PID 写入对应控制组的 tasks 文件即可。
135137
136- 如下代码所示,我们创建了一个新的控制组 (目录名为 $hostname),将进程(PID 为 3892)的内存限制为 1 GB,并限制其 CPU 使用时间为 1/4。
138+ 如下代码所示,我们创建了一个内存控制组子系统 (目录名为 $hostname),将进程(PID 为 3892)的内存限制为 1 GB,并限制其 CPU 使用时间为 1/4。
137139
138140``` bash
139141/sys/fs/cgroup/memory/$hostname /memory.limit_in_bytes=1GB // 容器进程及其子进程使用的总内存不超过 1GB
@@ -142,11 +144,13 @@ Linux cgroups 的设计简单易用。对于 Docker 等容器系统,它们只
142144echo 3892 > /sys/fs/cgroup/cpu/$hostname /tasks
143145```
144146
145- 最后,笔者需要补充一点,实际上 cgroups 对资源的限制也存在不完善之处,最常提到的问题是 /proc 文件系统的问题。/proc 文件系统记录了 Linux 系统中一些特殊状态,如 CPU 使用情况和内存占用情况,这些数据也是 top 命令查看系统信息的主要来源。
147+ 最后,笔者需要补充一点,实际上 cgroups 对资源的限制也存在不完善之处。最常提到的问题是 /proc 文件系统的问题,/proc 文件系统记录了 Linux 系统中一些特殊状态,如 CPU 使用情况和内存占用情况,这些数据也是 top 命令查看系统信息的主要来源。
148+
149+ 问题在于,/proc 文件系统并不反映通过 cgroups 对进程施加的限制。因此,在容器内部执行 top 命令时,显示的信息是宿主机的数据,而不是容器内部的数据。现在,业内一般使用 LXCFS(FUSE filesystem for LXC)技术维护一套专用于容器的 /proc 文件系统,解决这个问题。
146150
147- 问题在于,/proc 文件系统并不反映通过 cgroups 对进程施加的限制。因此,在容器内部执行 top 命令时,显示的信息是宿主机的数据,而不是容器内部的数据。在生产环境中,这个问题必须得到解决,不然会给系统带来很大的问题。现在,业内一般使用 LXCFS(FUSE filesystem for LXC)来维护一套专用于容器的 /proc 文件系统,解决这个问题 。
151+ 至此,相信读者们一定理解容器是什么 。
148152
149- 至此,相信读者们一定理解容器是什么。 容器并不是轻量化的虚拟机,也没有创建出真正的沙盒(容器之间共享系统内核,这也是为什么出现了如 kata 和 gVisor 等内核隔离的沙盒容器,7.4.5 节详细介绍)。容器只是利用命名空间、cgroups 等技术进行资源隔离和限制,并拥有独立的根目录(rootfs)的特殊进程。
153+ 容器并不是轻量化的虚拟机,也没有创建出真正的沙盒(容器之间共享系统内核,这也是为什么出现了如 kata 和 gVisor 等内核隔离的沙盒容器,7.4.5 节详细介绍)。说白了, 容器只是利用命名空间、cgroups 等技术进行资源隔离和限制,并拥有独立的根目录(rootfs)的特殊进程。
150154
151155
152156## 7.2.4 设计容器协作的方式
@@ -165,19 +169,25 @@ $ pstree -g
165169
166170对于操作系统而言,这种进程组管理更加方便。比如,Linux 操作系统可以通过向一个进程组发送信号(如 SIGKILL),使该进程组中的所有进程同时终止运行。
167171
168- 那么,现在思考一个问题:“如果把上面的进程用容器改造,该如何设计?”。如果是使用 Docker,自然会想到在 Docker 容器内运行两个进程:
172+ 那么,现在思考一个问题:“如果把上面的进程用容器改造,该如何设计?”。
173+
174+ 如果是使用 Docker,自然会想到在 Docker 容器内运行两个进程:
169175- rsyslogd 进程执行具体的业务;
170176- imklog 进程处理业务日志。
171177
172- 但这种设计会遇到一个问题:“容器中的 PID=1 进程应该是谁?”。在 Linux 系统中,PID 为 1 的进程是 init,它作为所有其他进程的祖先进程,负责监控进程状态,并处理孤儿进程。因此,容器中的第一个进程也需要具备类似的功能,能够处理 SIGTERM、SIGINT 等信号,优雅地终止容器内的其他进程。
178+ 但这种设计会遇到一个问题:“容器中的 PID=1 进程应该是谁?”。
179+
180+ 在 Linux 系统中,PID 为 1 的进程是 init,它作为所有其他进程的祖先进程,负责监控进程状态,并处理孤儿进程。因此,容器中的第一个进程也需要具备类似的功能,能够处理 SIGTERM、SIGINT 等信号,优雅地终止容器内的其他进程。
173181
174182Docker 的设计核心在于 Docker 容器采用的是“单进程”模型。Docker 通过监控 PID 为 1 的进程的状态来判断容器的健康状态(在 Dockerfile 中用 ENTRYPOINT 指定启动的进程)。如果确实需要在一个 Docker 容器中运行多个进程,首个启动的进程应该具备资源监控和管理能力,例如使用专为容器开发的 tinit 程序。
175183
176184通过 Docker,虽然可以勉强实现容器内运行多个进程,但进程间的协作远不止于资源回收那么简单。要让容器像操作系统中的进程组一样进行协作,下一步的演进是找到一个类似于“进程组”的概念。这是实现容器从隔离到协作的第一步。
177185
178186# # 7.2.5 超亲密容器组 Pod
179187
180- 在 Kubernetes 中,与“进程组”对应的设计概念是 Pod。Pod 是一组紧密关联的容器集合,它们共享 IPC、Network 和 UTS 等命名空间,是 Kubernetes 的最基本单位。
188+ 在 Kubernetes 中,与“进程组”对应的设计概念是 Pod。
189+
190+ Pod 是一组紧密关联的容器集合,它们共享 IPC、Network 和 UTS 等命名空间,是 Kubernetes 的最基本单位。
181191
182192容器之间原本是通过命名空间和 cgroups 进行隔离的。Pod 首要解决的问题是如何打破这种隔离,使 Pod 内的容器能够像进程组一样自然地共享资源和数据。为了解决这个问题,Kubernetes 引入了一个特殊的容器 —— Infra Container。
183193
@@ -250,13 +260,13 @@ Pod 承担的另一个重要职责是作为调度的原子单位。
250260- Node1:1.25G 可用内存;
251261- Node2:2G 可用内存。
252262
253- 如果这两个 Pod 需要协作并运行在同一台机器上,调度器可能首先将 Nginx 调度到 Node1。但由于 Node1 上只剩下 1.25G 的内存,而 Nginx 占用 1G 内存,LogCollector 将无法在 Node1 上运行,因为资源不足, 从而导致调度流程被阻塞。尽管可以通过重新调度来解决这个问题,但考虑到如果需要解决数以万计的容器协同调度问题呢?
263+ 如果这两个 Pod 需要协作并运行在同一台机器上,调度器可能首先将 Nginx 调度到 Node1。但由于 Node1 上只剩下 1.25G 的内存,而 Nginx 占用 1G 内存,LogCollector 将无法在 Node1 上运行,从而导致调度流程被阻塞。
254264
255- 以下为业内两种典型的解决方案:
265+ 尽管可以通过重新调度来解决这个问题,但考虑到如果需要解决数以万计的容器协同调度问题呢? 以下为业内两种典型的解决方案:
256266
257267- ** 成组调度** :可以在集群中等待足够的空余资源以满足亲和性约束的容器需求后,再进行统一调度。这是一种典型的成组调度方式,但会导致调度效率降低、资源利用不足,并可能出现互相等待而导致死锁的问题;
258268- ** 提高单个调度效率** :
259- 通过提高单任务调度的效率来解决这一问题。例如, Google 的 Omega 系统引入了一种基于共享状态的乐观绑定(Optimistic Binding)方式,以提高大规模系统调度的效率,但这种方案无疑非常复杂。笔者将在本章 7.7.3 节“调度器及扩展设计”中详细介绍该方案。 。
269+ 通过提高单任务调度的效率来解决这一问题。如 Google 的 Omega 系统引入了一种基于共享状态的乐观绑定(Optimistic Binding)方式,以提高大规模系统调度的效率,但这种方案无疑非常复杂。笔者将在本章 7.7.3 节“调度器及扩展设计”中详细介绍该方案。
260270
261271
262272将资源需求声明直接定义在 Pod 上,并以 Pod 作为最小的原子单位来实现调度。Pod 与 Pod 之间不存在超亲密的关系,如果有关系,就通过网络通信实现关联。复杂的协同调度问题在 Kubernetes 中直接消失了。
0 commit comments