3. 理解Pod和容器设计模式

本文由 CNCF + Alibaba 云原生技术公开课 整理而来

理解Pod

  • 容器基本概念:

容器的本质实际上是一个进程,是一个视图隔离、资源受限的进程。

容器里面 PID=1 的进程就是应用本身,这意味着管理虚拟机等于管理基础设施,管理容器等于管理应用本身,这就是不可变基础设施的一个最佳体现。

在这个前提下,Kubernetes 可以称作云时代的操作系统,以此类推,容器镜像就是这个操作系统的软件安装包。

  • 真实操作系统:

在真实的操作系统中,一个程序实际上是由一组进程组成的,这里的进程实际上等同于 Linux 中的线程,这组进程共同协作并共享程序的资源。

一个程序往往是根据进程组来进行管理的,进程组在 Linux 中就等同于线程组。容器本质是一个进程,所以在 Linux 中等同于线程,如果把 Kubernetes 比作操作系统,那么 Pod就是进程组,也就是 Linux 中的线程组。

  • 进程组概念:

如何将一个一组进程组成的程序在容器中运行起来?最自然的方法就是启动一个 Docker 容器,里面运行这组进程。可这样会有一个问题,容器里面 PID=1 的进程该是哪个进程?如果它是 main 进程,那么“谁”负责管理剩下的进程?

这个问题的核心在于,容器的设计本身是一种“单进程”模型,不是说容器里只能启动一个进程,由于容器的应用等于进程,所以只能管理 PID=1 的进程,其它的进程其实是一个托管状态,所以说服务应用进程本身就具有“进程管理”的能力。

因此这个程序需要具有“进程管理”的能力,或者直接把容器里面 PID=1 的进程直接改成 systemd,否则这个容器或者应用是没办法去管理很多个进程的。因为 PID=1 的进程就是应用本身,如果把 PID=1 的进程给 kill 掉,或者运行过程中死掉,那么剩下的进程的资源就无法回收了,这是一个非常严重的问题。

而如果真的把这个程序本身改成 systemd,或者在容器中运行一个 systemd ,将会导致另一个问题:管理容器就不再是管理应用本身,而是管理 systemd,那想要运行的程序的运行状态是无法得知的,这个应用是否退出、fail、或者异常等一概不知。

注意:Linux 容器的“单进程”模型,指的是容器的生命周期等同于 PID=1 的进程(容器应用进程)的生命周期,而不是说容器里不能创建多进程。一般情况下,容器应用进程并不具备进程管理能力,所以通过 exec 或者 ssh 在容器里创建的其他进程,一旦异常退出(比如 ssh 终止)是很容易变成无法管理的进程的。

  • Pod

在 Kubernetes 里面,Pod实际上正是 Kubernetes 抽象出来的一个可以类比为进程组的概念。

一个由一组进程组成程序,在 Kubernetes 里面,实际上会被定义为一个拥有多个容器的 Pod,每个容器中会运行一个进程。

当 Kubernetes 把程序运行起来时,它会把多个进程分别用多个容器启动起来,然后把它们定义在一个 Pod 里面,这多个容器会共享 Pod 的资源,真正在物理上存在的东西就是这多个容器,Pod 只是一个抽象概念,或者说这多个容器的组合就叫做 PodPod 是 Kubernetes 分配资源的一个单位,因为里面的容器要共享这些资源,所以 Pod 也是 Kubernetes 最小的调度单位。

  • 为什么 Pod 必须是最小调度单位?

举个例子:

假如现在有两个容器,它们是紧密协作的,所以它们应该被部署在一个 Pod 里面。具体来说,第一个容器叫做 App,就是业务容器,它会写日志文件;
第二个容器叫做 LogCollector,它会把刚刚 App 容器写的日志文件转发到后端的 ElasticSearch 中。

两个容器的资源需求是这样的:App 容器需要 1G 内存,LogCollector 需要 0.5G 内存,而当前集群环境的可用内存是这样一个情况:
Node_A:1.25G 内存,Node_B:2G 内存。

假如说现在没有 Pod 概念,就只有两个容器,这两个容器要紧密协作、运行在一台机器上。可是,如果调度器先把 App 调度到了 Node_A 上面,接下来会怎么样呢?
这时你会发现:LogCollector 实际上是没办法调度到 Node_A 上的,因为资源不够。其实此时整个应用本身就已经出问题了,调度已经失败了,必须去重新调度。

以上就是一个非常典型的成组调度失败的例子。英文叫做:Task co-scheduling 问题,这个问题在很多项目里面都有解决方法。

比如说在 Mesos 里面,它会做一个事情,叫做资源囤积(resource hoarding):即当所有设置了 Affinity 约束的任务都达到时,才开始统一调度,这是一个非常典型的成组调度的解法。

所以上面提到的 App 和 LogCollector 这两个容器,在 Mesos 里面,他们不会说立刻调度,而是等两个容器都提交完成,才开始统一调度。这样也会带来新的问题,首先调度效率会损失,因为需要等待。由于需要等还会有外一个情况会出现,就是产生死锁,就是互相等待的一个情况。这些机制在 Mesos 里都是需要解决的,也带来了额外的复杂度。

另一种解法是 Google 的解法。它在 Omega 系统(就是 Borg 下一代)里面,做了一个非常复杂且非常厉害的解法,叫做乐观调度。比如说:不管这些冲突的异常情况,先调度,同时设置一个非常精妙的回滚机制,这样经过冲突后,通过回滚来解决问题。这个方式相对来说要更加优雅,也更加高效,但是它的实现机制是非常复杂的。这个有很多人也能理解,就是悲观锁的设置一定比乐观锁要简单。

而在 Kubernetes 里,就直接通过 Pod 这样一个概念去解决了。因为在 Kubernetes 里,这样的一个 App 容器和 LogCollector 容器一定是属于一个 Pod ,它们在调度时必然是以一个 Pod 为单位进行调度,因此这个问题是根本不存在的。

  • 理解 Pod

Pod 里面的容器是“超亲密关系”。正常来说,有一种关系叫做亲密关系,这个亲密关系是一定可以通过调度来解决的。

比如说现在有两个 Pod,它们需要运行在同一台宿主机上,那这样就属于亲密关系,调度器一定是可以帮助去做的。但是对于超亲密关系来说,有一个问题,即它必须通过 Pod 来解决。因为如果超亲密关系赋予不了,那么整个 Pod 或者说是整个应用都无法启动。

什么叫做超亲密关系呢?大概分为以下几类:

1. 两个进程之间会发生文件交换;

2. 两个进程之间需要通过 localhost 或者说是本地的 Socket 去进行通信,这种本地通信也是超亲密关系;

3. 这两个容器或者是微服务之间,需要发生非常频繁的 RPC 调用,出于性能的考虑,也希望它们是超亲密关系;

4. 两个容器或应用,它们需要共享某些 Linux Namespace。

像以上几种关系都属于超亲密关系,它们都是在 Kubernetes 中会通过 Pod 的概念去解决的。

为什么需要 Pod,它解决了两个问题:

1. 怎么去描述超亲密关系

2. 怎么去对超亲密关系的容器或者业务去做统一调度,这是 Pod 最主要的一个诉求

Pod 的实现机制

Pod 既然是一个逻辑概念,那么在机器上它是怎么实现的呢?核心在于如何让一个 Pod 里的多个容器之间最高效的共享资源和数据。

因为容器之间原本是被 Linux namespace 和 cgroups 隔离开的,所以现在实际要解决的是怎么去打破这个隔离,然后共享资源和数据。

  • 共享网络:

在 Kubernetes 中,在每个 Pod 里,会额外启动一个 pause 容器来共享 Pod 的 network namespace。

pause 容器全称 Infrastucture ContainerInfra Container),其他所有容器都会加入到 pause 容器的 network namespace 中。这样一来,一个 Pod 里面的所有容器,它们看到的网络视图是完全一样的,都来自于pause 容器。

Pod 里面,有一个 ip 地址,是这个 Pod 的 network namespace对应的地址,也是这个 pause 容器的 ip 地址。而其他所有网络资源,都是一个 Pod 一份,并且被 Pod 中的所有容器共享,这就是 Pod 的网络实现方式。

由于有这样的一个中间容器存储,所以在 Pod中,pause 容器第一个启动,并且整个 Pod 的生命周期都等同于 pause 容器的生命周期,与 Pod 中的其他容器是无关的。这也是为什么 Kubernetes 允许单独更新 Pod 里的某一个镜像,这不会导致 Pod 重启或重建。

  • 共享存储:

Pod 共享存储更为简单。其实是把 volume 变成 Pod level,然后同属于一个 Pod 的所有容器共享所有的 volume。

实质上 Pod 的每个容器是共享 pause 容器的 volume,Pod 的每个容器都可以声明挂载 volume 到自己的指定路径下,这就是 Kubernetes 通过 Pod 给容器共享存储的方法。


容器设计模式

所有设计模式的本质都是:解耦和重用。

  • Sidecar

Sidecar,就是在 Pod 里面定义一些专门的容器,来执行主业务容器所需要的一些辅助工作。比如 Init Container,它会比 spec.containers 定义的用户容器先启动,并且严格按照定义的顺序依次执行。

这样一来,类似拷贝文件到主业务容器中的工作,就可以交给 Init Container 来完成,目的就是让 Pod 达到自包含的状态——无论放到哪个 Kubernetes 下都可以顺利启动起来。

通过组合两种不同角色的容器,并且按照一些像 Init Container 这样的编排方式,统一的去打包一个应用,最终把它通过 Pod 启动,这就是一种经典的容器设计模式——Sidecar

Sidecar 的优势在于将辅助功能从主业务容器解耦,所以能够独立发布 Sidecar 容器,并且这个能力是可以重用的。

具体的辅助工作可以有这些:

1. 原本需要在容器里面执行 SSH 需要干的一些事情,可以写脚本、一些前置的条件;
 

2. 当然还有日志收集,日志收集本身是一个进程,那么就可以把它打包进 Pod 里面去做这个收集工作;
 

3. 还有就是 Debug 应用,实际上现在 Debug 整个应用都可以在应用 Pod 里面定义一个额外的 Container,它可以去 exec 应用 pod 的 namespace;

4. 查看其他容器的工作状态。不再需要去 SSH 登陆到容器里去看,只要把监控组件装到额外的 Container 就可以了,然后把它作为一个 Sidecar 启动起来,
   跟主业务容器进行协作,所以同样业务监控也都可以通过 Sidecar 方式来去做。

当使用 Sidecar 模式进行日志收集时,业务容器将日志写在一个 volume 里面,而由于 volume 在 Pod 里面是被共享的,所以日志收集的容器——Sidecar 容器可以通过该 volume 直接将日志文件读取到,然后存到远端存储中。


猜你喜欢

转载自blog.csdn.net/miss1181248983/article/details/109725849