目录
为什么需要 Pod
真实操作系统里的例子
如果说 Kubernetes 就是操作系统的话,那么我们不妨看一下真实的操作系统的例子。
例如,一个程序叫做 Helloworld,这个 Helloworld 程序实际上是由一组进程组成的,需要注意一下,这里说的进程实际上等同于 Linux 中的线程。
因为 Linux 中的线程是轻量级进程,所以如果从 Linux 系统中去查看 Helloworld 中的 pstree,将会看到这个 Helloworld 实际上是由四个线程组成的,分别是 {api、main、log、compute}。也就是说,四个这样的线程共同协作,共享 Helloworld 程序的资源,组成了 Helloworld 程序的真实工作情况。
这是操作系统里面进程组或者线程组中一个非常真实的例子,以上就是进程组的一个概念。
在真实的操作系统里面,一个程序往往是根据进程组来进行管理的。Kubernetes 把它类比为一个操作系统,比如说 Linux。针对于容器我们可以类比为进程,就是前面的 Linux 线程。那么 Pod 又是什么呢?实际上 Pod 就是我们刚刚提到的进程组,也就是 Linux 里的线程组。
进程组概念
还是前面那个例子:Helloworld 程序由四个进程组成,这些进程之间会共享一些资源和文件。那么现在有一个问题:假如说现在把 Helloworld 程序用容器跑起来,你会怎么去做?
当然,最自然的一个解法就是,我现在就启动一个 Docker 容器,里面运行四个进程。可是这样会有一个问题,这种情况下容器里面 PID=1 的进程该是谁? 比如说,它应该是我的 main 进程,那么问题来了,谁又负责去管理剩余的 3 个进程呢?
这个核心问题在于,容器的设计本身是一种“单进程”模型,不是说容器里只能起一个进程,由于容器的应用等于进程,所以只能去管理 PID=1 的这个进程,其他再起来的进程其实是一个托管状态。 所以说服务应用进程本身就具有“进程管理”的能力。
比如说 Helloworld 的程序有 system 的能力,或者直接把容器里 PID=1 的进程直接改成 systemd,否则这个应用,或者是容器是没有办法去管理很多个进程的。因为 PID=1 进程是应用本身,如果现在把这个 PID=1 的进程给 kill 了,或者它自己运行过程中死掉了,那么剩下三个进程的资源就没有人回收了,这个是非常非常严重的一个问题。
而反过来真的把这个应用本身改成了 systemd,或者在容器里面运行了一个 systemd,将会导致另外一个问题:使得管理容器,不再是管理应用本身了,而等于是管理 systemd,这里的问题就非常明显了。比如说我这个容器里面 run 的程序或者进程是 systemd,那么接下来,这个应用是不是退出了?是不是 fail 了?是不是出现异常失败了?实际上是没办法直接知道的,因为容器管理的是 systemd。这就是为什么在容器里面运行一个复杂程序往往比较困难的一个原因。
由于容器实际上是一个“单进程”模型,所以如果你在容器里启动多个进程,只有一个可以作为 PID=1 的进程,而这时候,如果这个 PID=1 的进程挂了,或者说失败退出了,那么其他三个进程就会自然而然的成为孤儿,没有人能够管理它们,没有人能够回收它们的资源,这是一个非常不好的情况。
注意:Linux 容器的“单进程”模型,指的是容器的生命周期等同于 PID=1 的进程(容器应用进程)的生命周期,而不是说容器里不能创建多进程。当然,一般情况下,容器应用进程并不具备进程管理能力,所以你通过 exec 或者 ssh 在容器里创建的其他进程,一旦异常退出(比如 ssh 终止)是很容易变成孤儿进程的。
反过来,其实可以在容器里面 run 一个 systemd,用它来管理其他所有的进程。这样会产生第二个问题:实际上没办法直接管理我的应用了,因为我的应用被 systemd 给接管了,那么这个时候应用状态的生命周期就不等于容器生命周期。这个管理模型实际上是非常非常复杂的。
Pod = “进程组”
在 kubernetes 里面,Pod 实际上正是 kubernetes 项目为你抽象出来的一个可以类比为进程组的概念。
前面提到的,由四个进程共同组成的一个应用 Helloworld,在 Kubernetes 里面,实际上会被定义为一个拥有四个容器的 Pod,这个概念大家一定要非常仔细的理解。
就是说现在有四个职责不同、相互协作的进程,需要放在容器里去运行,在 Kubernetes 里面并不会把它们放到一个容器里,因为这里会遇到两个问题。那么在 Kubernetes 里会怎么去做呢?它会把四个独立的进程分别用四个独立的容器启动起来,然后把它们定义在一个 Pod 里面。
所以当 Kubernetes 把 Helloworld 给拉起来的时候,你实际上会看到四个容器,它们共享了某些资源,这些资源都属于 Pod,所以我们说 Pod 在 Kubernetes 里面只有一个逻辑单位,没有一个真实的东西对应说这个就是 Pod,不会有的。真正起来在物理上存在的东西,就是四个容器。这四个容器,或者说是多个容器的组合就叫做 Pod。并且还有一个概念一定要非常明确,Pod 是 Kubernetes 分配资源的一个单位,因为里面的容器要共享某些资源,所以 Pod 也是 Kubernetes 的原子调度单位。
上面提到的 Pod 设计,也不是 Kubernetes 项目自己想出来的, 而是早在 Google 研发 Borg 的时候,就已经发现了这样一个问题。这个在 Borg paper 里面有非常非常明确的描述。简单来说 Google 工程师发现在 Borg 下面部署应用时,很多场景下都存在着类似于“进程与进程组”的关系。更具体的是,这些应用之前往往有着密切的协作关系,使得它们必须部署在同一台机器上并且共享某些信息。
为什么 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 下一代)里面,做了一个非常复杂且非常厉害的解法,叫做乐观调度。比如说:不管这些冲突的异常情况,先调度,同时设置一个非常精妙的回滚机制,这样经过冲突后,通过回滚来解决问题。这个方式相对来说要更加优雅,也更加高效,但是它的实现机制是非常复杂的。这个有很多人也能理解,就是悲观锁的设置一定比乐观锁要简单。
而像这样的一个 Task co-scheduling 问题,在 Kubernetes 里,就直接通过 Pod 这样一个概念去解决了。因为在 Kubernetes 里,这样的一个 App 容器和 LogCollector 容器一定是属于一个 Pod 的,它们在调度时必然是以一个 Pod 为单位进行调度,所以这个问题是根本不存在的。
Pod要解决的问题
因为容器之间原本是被 Linux Namespace 和 cgroups 隔开的,所以现在实际要解决的是怎么去打破这个隔离,然后共享某些事情和某些信息。这就是 Pod 的设计要解决的核心问题所在。
所以说具体的解法分为两个部分:网络和存储。
共享网络
在一个Pod中,一个容器 A 和一个容器 B需要共享Network Namespace,Kubernetes会在Pod中额外起一个Infra container小容器来共享整个Pod的Network Namespace。
Infra container 是一个非常小的镜像,大概 100~200KB 左右,是一个汇编语言写的、永远处于“暂停”状态的容器。由于有了这样一个 Infra container 之后,其他所有容器都会通过 Join Namespace 的方式加入到 Infra container 的 Network Namespace 中。
在 Pod 里面,一定有一个 IP 地址,是这个 Pod 的 Network Namespace 对应的地址,也是这个 Infra container 的 IP 地址。所以大家看到的都是一份,而其他所有网络资源,都是一个 Pod 一份,并且被 Pod 中的所有容器共享。这就是 Pod 的网络实现方式。
共享存储
一个 Pod 可以设置一组共享的存储卷。 Pod 中的所有容器都可以访问该共享卷,从而允许这些容器共享数据。卷的存在时间会超出 Pod 中运行的所有容器,并且在容器重新启动时数据也会得到保留。 当 Pod 不再存在时,卷也将不再存在。