理解Kubernetes的设计模式

为什么最小单元是Pod?

容器的本质是一个进程,是一个视图被隔离,资源收限制的进程。容器中的PID=1的进程就是应用本身,这就意味着管理虚拟机等于管理基础设施,因为这个时候我们是在管理机器,但是管理容器却等于直接管理应用本身;现在逐渐将kubernetes作为云时代的操作系统了,那么容器镜像充当着软件安装包的角色;那么容器中的程序在运行时, 实际上就是一组进程组组成,这里说的进程组相当于Linux中的线程,而进程组的角色就与Pod非常的类似。在kubernetes中,Pod实际上正是抽象出来的一个可以类比为进程组的概念。在Pod中,多个进程间存在着类似于“进程与进程组”的关系,也就是说这些应用之间有些密切的协作关系,适得他们必须在同一台机器中去共享这些信息。

为什么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的亲密关系?

两个Pod同时运行在同一台宿主机上,但是对于超亲密关系有一个问题需要解决,既它必须通过Pod来解决。如果超亲密关系都赋予不了,那么整个Pod或者说整个应用都无法启动。大概分为以下几类:

  1. 比如说两个进程之间发生文件交互,比如说一个是产生日志文件,一个是收集日志

  2. 两个进程之间需要通过localhost或者说本地的Socket去通信

  3. 两个容器或者微服务之间的调用需要发生非常频繁的RPC调用

  4. 两个容器或者应用,它们需要共享某些namespace,最常见的例子就是一个容器要加另外一个容器的Network Namespace

这样便说明了为什么需要Pod,它解决了两个问题:

  • 我们可以怎么样去描述亲密关系

  • 我们怎么去对亲密关系的容器或者说业务去做统一调度

Pod的实现机制是什么?

一个Pod里的多个容器之间最高效的共享着某些资源和数据,容器与容器之间原本是被namespace和Cgroup隔开的,所以现在实际要解决的是怎么去打破这个隔离,然后分享某些事情和某些信息。

共享网络

第一个问题是 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地址、Mac地址等等,跟网络相关的信息,其实全是一份,这一份都来自于 Pod 第一次创建的这个 Infra container。这就是 Pod 解决网络共享的一个解法。

在 Pod 里面,一定有一个 IP 地址,是这个 Pod 的 Network Namespace 对应的地址,也是这个 Infra container 的 IP 地址。所以大家看到的都是一份,而其他所有网络资源,都是一个 Pod 一份,并且被 Pod 中的所有容器共享。这就是 Pod 的网络实现方式。

由于需要有一个相当于说中间的容器存在,所以整个 Pod 里面,必然是 Infra container 第一个启动。并且整个 Pod 的生命周期是等同于 Infra container 的生命周期的,与容器 A 和 B 是无关的。这也是为什么在 Kubernetes 里面,它是允许去单独更新 Pod 里的某一个镜像的,即:做这个操作,整个 Pod 不会重建,也不会重启,这是非常重要的一个设计。

共享存储

比如说现在有两个容器,一个是 Nginx,另外一个是非常普通的容器,在 Nginx 里放一些文件,让我能通过 Nginx 访问到。所以它需要去 share 这个目录。我 share 文件或者是 share 目录在 Pod 里面是非常简单的,实际上就是把 volume 变成了 Pod level。然后所有容器,就是所有同属于一个 Pod 的容器,他们共享所有的 volume。

apiVersion: v1
kind: Pod
metadata:
  name: two-containers
spec:
  restartPolicy: Never
  volumes:
  - name: shard-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" ]

这个 volume 叫做 shared-data,它是属于 Pod level 的,所以在每一个容器里可以直接声明:要挂载 shared-data 这个 volume,只要你声明了你挂载这个 volume,你在容器里去看这个目录,实际上大家看到的就是同一份。这个就是 Kubernetes 通过 Pod 来给容器共享存储的一个做法。

为了能够在容器启动时,保证一些操作是必须按照顺序进行操作的,这个时候使用到了Init Container。Init Container中的操作会比spec.containers定义的用户容器先启动,并且严格按照定义顺序依次执行。

什么是Sidecar?

在Pod里面,可以定义一些专门的容器,来执行主业务容器所需要的一些辅助工作,比如容器的Init Container,它就干了一个事儿,它就是一个Sidercar,它只是负责把镜像里的文件拷贝到共享目录里面,以便其他能够使用。还有一下操作:

  • 原本需要在容器里面执行 SSH 需要干的一些事情,可以写脚本、一些前置的条件,其实都可以通过像 Init Container 或者另外像 Sidecar 的方式去解决;

  • 当然还有一个典型例子就是我的日志收集,日志收集本身是一个进程,是一个小容器,那么就可以把它打包进 Pod 里面去做这个收集工作;

  • 还有一个非常重要的东西就是 Debug 应用,实际上现在 Debug 整个应用都可以在应用 Pod 里面再次定义一个额外的小的 Container,它可以去 exec 应用 pod 的 namespace;

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

  • 代理容器,假如现在有个 Pod 需要访问一个外部系统,或者一些外部服务,但是这些外部系统是一个集群,那么这个时候如何通过一个统一的、简单的方式,用一个 IP 地址,就把这些集群都访问到?有一种方法就是:修改代码。因为代码里记录了这些集群的地址;另外还有一种解耦的方法,即通过 Sidecar 代理容器。

  • 适配器容器,现在业务暴露出来的 API,比如说有个 API 的一个格式是 A,但是现在有一个外部系统要去访问我的业务容器,它只知道的一种格式是 API B ,所以要做一个工作,就是把业务容器怎么想办法改掉,要去改业务代码。但实际上,你可以通过一个 Adapter 帮你来做这层转换。将A转换成B。

Last updated