Contain和Pod关系
pod是怎么来的?
字面上,“Container”这个词很难让人形象地理解其真正含义,Kubernetes 中的“Pod”也是如此。
仅凭几句解释并不能充分认识这些概念,甚至还会引起误解,譬如把容器与轻量化虚拟机混为一谈。
为了尽可能深入理解容器,我们从最原始的文件系统隔离讲起。
1 、文件系统隔离
1979年,Unix 系统引入了一个革命性的技术 —— chroot(chang root)。
chroot 允许管理员将进程的根目录锁定在指定的位置,从而限制该进程访问的文件系统范围。chroot 的隔离能力对安全性至关重要,比如创建一个隔离环境,用来安全地运行和监控可疑的代码或者程序,因此 chroot 之后的环境也被形象地称为jail(监狱)。
仅需几步,就能创建一个 chroot 环境。
这个 jail 用处不大,只有 bash 以及一些内置的函数,但也足以说明它的作用“运行在此 jail 下进程的文件系统与宿主机隔离了”。
我们再运行一个 docker,看看两者之间的区别。
看起来跟 chroot 差不多,也是一个与宿主机隔离的文件系统环境,那这是否意味着 chroot 就是容器了呢?
肯定不是,chroot 只是改变了根目录,而非创建了真正的独立隔离、安全的环境,chroot 内的进程通过几行代码就能从当前的 jail 中逃逸,而且文件系统、网络、设备等等都没有被充分隔离。
2、 资源全方位隔离
chroot 最初的目的是为了实现文件的隔离,并非为了容器而设计。
后来 Linux 吸收了 chroot 的理念,先是在 2.4.19 引入了 Mount 命名空间,这样就可以隔离挂载文件系统。又想到进程间通信也需要隔离,就有了 IPC 命名空间。同时,容器还需要一个独立的主机名以便在网络中标识自己,有了网络,自然还要有独立的 IP、端口、路由等...。
从 Linux 内核 2.6.19 起,陆陆续续添加了 UTS、IPC、PID、Network、User 等命名空间。至 Linux 内核 3.8 版本,Linux 已经完成容器所需的 6 项最基本资源隔离。
我们创建子进程通常使用 fork(),fork 背后调用的是 clone(),如果要为创建的子进程设置各类资源隔离,使用 clone 并指定 flags 参数即可。
如下代码所示,通过 clone 创建子进程,新建的子进程将会“看到”一个全新的系统环境。这个环境内,挂载的文件目录、进程 PID、进程通信资源、网络及网络设备、UTS 等,全部与宿主机隔离。
3 、资源全方位限制
进程的资源隔离已经完成,如果再对使用资源进行额度限制,就能对进程的运行环境实现一个进乎完美的隔离。
这就要用 Linux 内核的第二项技术:Linux Control Cgroup —— 简称 cgroups。
Linux 系统执行
ll /sys/fs/cgroup
,可以看到有很多 blkio、cpu 这样的目录。这也叫子系统,子系统显示了当前机器可被 cgroups 限制的资源种类。在对应的子系统内,创建目录,比如。
这样的目录被称为”控制群组“,控制群组创建后会自动生成一系列资源限制文件。目前,Linux 系统一般支持如下控制群组。
控制群组内的资源限制往文件内写入资源管理配置,最后则是将进程 PID 写入 tasks 文件里,然后配置生效。
如下所示,3892 这个进程内存被限制在 1 GB、只允许使用 1/4 CPU 时间。
至此,相信读者们也一定理解容器是什么,容器不是轻量化的虚拟机,也没有创造出真正的沙盒(容器之间共享系统内核,这也是为什么又出现了 kata、gVisor 等内核隔离的沙盒容器),只是使用了 Namespace、cgroups 等技术进行资源隔离、限制以及拥有独立 rootfs 的特殊进程。
4、 设计容器协作的方式
既然容器是个特殊的进程,那联想到真正的操作系统内大部分的进程也并非独自运行,而是以进程组有原则的组织在一起,共同协作完成某项工作。
登录到一台 Linux 机器,执行 pstree -g 命令展示当前系统中正在运行的进程树状结构。
如上输出,展示了 Linux 系统中负责处理日志的 rsyslogd 程序的进程树结构。可见 rsyslogd 的主程序 main 以及它要用到的内核日志模块 imklog 等同属 1089 进程组,这些进程相互协作,共享 rsyslogd 程序的资源,共同完成 rsyslogd 程序的职责。
对于操作系统而言,这种进程组也更方便管理,Linux 操作系统只需将信号(如 SIGKILL)发给一个进程组,该进程组中的所有进程就都会收到这个信号而终止运行。
那么,现在思考一个问题,如果把上面的进程用容器改造跑起来,该如何设计?
如果是使用 Docker,自然会想到在 Docker 容器内运行两个进程:
- rsyslogd 执行业务;
- imklog 处理业务日志。
但这样设计会有一个问题:容器里面 PID=1 的进程该是谁?这个问题的核心在于 Docker 容器的设计本身是一种“单进程”模型,Docker 只能通过监视 PID 为 1 的进程的运行状态来判断容器的工作状态是否正常(即由 ENTRYPOINT 启动的进程)。
如果容器想要实现类似操作系统进程组那般互相协作,容器下一步的演进就是要找到与“进程组”相对应的概念,这是实现容器从隔离到协作的第一步。
5 、超亲密容器组 Pod
Kubernetes 中这个设计叫做 Pod,Pod 是一组紧密关联的容器集合,它们共享 IPC、Network、UTS 等命名空间,是 Kubernetes 最基本单位。
容器之间原本是被 Linux Namespace 和 cgroups 隔开的,Pod 第一个要解决的问题是怎么去打破这个隔离,让 Pod 内的容器可以像进程组一样天然的共享资源和数据。
Kubernetes 使用了一个特殊的容器(Infra Container)解决这个了问题。
Infra Container 是整个 Pod 中第一个启动的容器,只有 300 KB 左右大小,它负责申请容器组的 UTS、IPC、网络等命名空间,Pod 内其他容器通过 setns(Linux 系统调用,把进程加入到某个命名空间中)方式共享 Infra Container 容器的命名空间,其次它还可作为 init 进程,用来管理子进程、回收资源等。
通过 Infra Container,同一 Pod 内的容器共享 UTS、Network、IPC、Time 命名空间。
注意,PID 命名空间和文件命名空间默认还是隔离的,这是因为:
- 容器之间也需要相互独立的文件系统以避免冲突。如果容器之间想要想要实现文件共享,Kubernetes 也提供了 Volume 支持(Volume 的概念将在本章 7.5 节介绍)。
- PID 隔离是因为如果某些容器进程不再具备 PID=1,容器可能会拒绝启动(例如使用 systemd 的容器)。
如果要共享 PID 命名空间,需要设置 PodSpec 中的 ShareProcessNamespace 为 true,如下 yaml 所示。
设置之后,Infra Container 将作为 PID 1 进程,由 Infra Container 负责信号处理、子进程的资源回收等。
6 、Pod 是 Kubernetes 的基本单位
解决了容器的协作问题,围绕容器和 Pod 不断向实际应用的场景扩展。
因为 Pod 不可能只有一个实例,于是有了 Deployment,实现 Pod 多个实例运行。因为 Pod 是动态变化的额,还得有一个唯一的访问入口,并在多个实例之间负载均衡,于是就有了 Service。Service 是基于四层 TCP 和 UDP 协议转发,还得有应用层协议 (HTTP/HTTPS)访问支持,并通过域名/路径做到更细粒度的划分,于是就有了 Ingress。
围绕 Pod,最终绘制出如图 7-5 所示 Kubernetes 核心功能全景图。
7 、Pod 是调度的原子单位
Pod 承担的另外一个重要职责是 —— 作为调度的原子单位。
协同调度是非常麻烦的事情。举个例子说明,有以下两个亲和性容器:
- 第一个容器 Nginx(资源需求 1G 内存) 接收请求,并将请求写入日志文件。
- 第二个容器 LogCollector(资源需求 0.5 G 内存),它会把 Nginx 容器写的日志文件转发到后端的 ElasticSearch 中。
当前集群环境的可用资源是这样一个情况:Node1 1.25G 内存,Node2 2G 内存。
假设这两个 Pod 因协作需要运行在一台机器上,如果调度器先把 Nginx 调度到 Node1,因为资源不够,LogCollector 实际上是没办法调度到 Node1 上的,得重新再发起新的调度。
虽然能通过新一轮的调度,最终解决,但你思考:假如有几千个 Node 节点、数以万计的容器呢?解决这种协同调度:
- 要么等待所有设置了亲和性约束的任务全部就绪,才开始统一调度。这是典型的成组调度的解法,但也带来新的问题,调度效率会损失、资源无法充分利用、互相等待还有可能产生死锁。
- 要么就想办法提高单任务调度的效率,Google Omega 系统介绍过一种基于共享状态,通过乐观锁解决因并发导致资源冲突的方式,但方案无疑非常复杂。
将运行资源的需求声明定义在 Pod 上,直接以 Pod 为最小的原子单位来实现调度的话,Pod 与 Pod 之间不存在什么超亲密关系,如果非要有什么关系,就通过网络联系。
复杂的协同调度设计在 Kubernetes 中直接消失了。
8、 容器的设计模式 Sidecar
通过组合两个不同角色的容器,共享资源,统一调度编排,在 Kubernetes 里面就是一个非常经典的容器设计模式 —— 即 Sidecar(边车)模式。
Sidecar 模式其实就是在 Pod 里面定义一些专门的容器,通过职责分离与容器的隔离特性,降低容器的复杂度。
通过图 7-6 所示的 Sidecar 容器(如日志记录、监控、安全性或数据同步 Sidecar 容器),能看到 Sidecar 模式通过增强或扩展主应用容器的功能,使开发一个高内聚、低耦合的软件变的更加容易。
Loading...