书接上文,在之前的学习中,我们了解到类似Docker项目的Linux容器,实际上是基于linux内核,并由NameSpace、Cgroups和Rootfs三种技术构建出来的进程的隔离环境。而在单机上Docker容器这种技术注定只能成为小打小闹,而更多用户的需求是:现在我有了容器镜像,请帮我在给定集群上把这个应用运行起来。最好还给我提供一下路由网关、水平拓展、监控、备份、灾难恢复等运维能力。

K8s设计


k8s架构

  我们都知道,K8s做到了这些要求,那么他是怎么做到的呢?先来看看Kubernetes 项目的架构,可以看出是,由 Master 和 Node两种节点组成,对应着控制节点和计算节点。这里就一笔带过了,Master节点上面主要由四个模块组成:APIServer、scheduler、controller manager、etcd。每个Node节点主要由三个模块组成:kubelet、kube-proxy、runtime。然后每个节点上跑着各种k8s组件,最后提供了完整的服务。

master

  控制节点,即 Master 节点,由三个紧密协作的独立组件组合而成,它们分别是负责 API 服务的 kube-apiserver、负责调度的 kube-scheduler,以及负责容器编排的 kube-controllermanager。整个集群的持久化数据,则由 kube-apiserver 处理后保存在 Ectd 中。

node

  而计算节点上最核心的部分,则是一个叫作 kubelet 的组件。在 Kubernetes 项目中,kubelet 主要负责同容器运行时(比如 Docker 项目)打交道。而这个交互所依赖的,是一个称作 CRI(Container Runtime Interface)的远程调用接口,这个接口定义了容器运行时的各项核心操作,比如:启动一个容器需要的所有参数。这也是为何,Kubernetes 项目并不关心你部署的是什么容器运行时、使用的什么技术实现,只要你的这个容器运行时能够运行标准的容器镜像,它就可以通过实现 CRI 接入到 Kubernetes 项目当中。

  而具体的容器运行时,比如 Docker 项目,则一般通过 OCI 这个容器运行时规范同底层的 Linux 操作系统进行交互,即:把 CRI 请求翻译成对 Linux 操作系统的调用(操作 Linux Namespace 和Cgroups 等)。

  此外,kubelet 还通过 gRPC 协议同一个叫作 Device Plugin 的插件进行交互。这个插件,是Kubernetes 项目用来管理 GPU 等宿主机物理设备的主要组件,也是基于 Kubernetes 项目进行机器学习训练、高性能作业支持等工作必须关注的功能。
  而kubelet 的另一个重要功能,则是调用网络插件和存储插件为容器配置网络和持久化存储。这两个插件与 kubelet 进行交互的接口,分别是 CNI(Container Networking Interface)和CSI(Container Storage Interface)。

k8s架构

  然后我们来看Kubernetes中的一些基本概念。
k8s基本概念
  很多是吧,容器我知道,pod是容器组我也知道,那deployment是啥啊,Job是啥啊,DaemonSet是啥啊,为啥K8s要搞这么多组件啊?答案是谷歌的研究人员发现,大规模的编排、调度、管理系统最困难的地方其实是各种任务调用关系的处理。

  要说明这个就得从Docker说起,Docker Compose项目中,你想让一个运行Web的Docker容器连接运行数据库的Docker容器,你可以Docker在 Web 容器中,将 数据库 容器的 IP 地址、端口等信息以环境变量的方式注入进去,供应用进程使用。并且当数据库容器发生变化时(比如,镜像更新,被迁移到其他宿主机上等等),这些环境变量的值会由 Docker 项目自动更新。这就是平台项目自动地处理容器间关系的典型例子。

  打个比方,我们每个人是容器,Docker link就相当于你在大学同班40个人,有什么事微信上说一声就ok了,每次写一个环境变量也还可以接受。但现在你来浦发了,4万人怎么一起工作,4万个人的微信群那怎么办事呢?K8s就说,4万人是吧,那分组件啊,我们先分五处五中心,每个人再一层层分下去。我们每个人是容器的话,我们这个Cass小组就是Pod,同一个Pod会共享网络、存储等,就相当于现在我们一起培训,在一起办公。再往上就是Deployment,可以说是多个Pod实例的管理器,相当于Cass西安小组、合肥小组、上海小组的管理者。有了一组组Pod,我们需要一个固定的IP地址和端口以负载均衡的方式访问它,于是就有了Service,相当于练总,我们pod可以滚蛋,但领导要结果找的是连总,pod的IP动态会变,但service的ip不会变。Serviec的Service就是ingress,就是余处了。

  后面为了满足更多场景,还出现了更多基于pod改进的对象,比如job用来描述一次性运行的pod,就是栋杰哥外派,干完活就回去了,比如daemonset用来描述每个宿主机上必须且只能运行一个副本的守护进程服务,就像每个团队配个保安;比如 CronJob用于描述定时任务,就相当于给个团队就干到4年然后就结束了。

  有人说,啊,这解决方案不是很简单吗,一想就该这样。但实际上在K8s出来之前,人们是没有这种按照角色划分进行管理的解决方案。他们做的事是,为每一个管理功能创建一个指令,然后在项目中实现其中的逻辑。比如定时任务,没有job,而是自己实现一套逻辑。这种做法,的确可以解决当前的问题,但是在更多的问题来临之后,往往会力不从心。所以回过头看K8s的组件,确实是有种智慧在里面。

  在 Kubernetes 项目中,比较推崇的使用方法是:

  首先,通过一个“编排对象”,比如 Pod、Job、CronJob 等,来描述你试图管理的应用;
  然后,再为它定义一些“服务对象”,比如 Service、Secret、Horizontal Pod Autoscaler(自动水平扩展器)等。这些对象,会负责具体的平台级功能。

  并且我们也可以自定义我们自己的编排对象来实现所需的功能,在第四阶段我会介绍一下蚂蚁金服的CafeDeployment。

Pod、Replicaset、Deployment

  当然上面的内容大家都明白,我就是下面就讲些组件实现方面的东西。

Pod的本质

  容器是进程、k8s是操作系统,那pod是什么,Pod是进程组。

  还是那句Docker的本质是基于linux内核,并由NameSpace、Cgroups和Rootfs三种技术构建出来的进程的隔离环境。而实际上Pod也是如此,它只是一个概念逻辑。Kubernetes 真正处理的,还是宿主机操作系统上 Linux 容器的 Namespace 和Cgroups,而并不存在一个所谓的 Pod 的边界或者隔离环境。Pod,其实是一组共享了某些资源的容器。具体的说:Pod 里的所有容器,共享的是同一个 Network Namespace,并且可以声明共享同一个Volume。

  有人说,那这就简单了,我把一个容器(容器 A)共享另外一个容器(容器 B)的网络和 Volume不就完事了吗?

$ docker run --net=B --volumes-from=B --name=A image-A ...

  但是,如果真这样做的话,那容器 B 就必须比容器 A 先启动,这样一个 Pod 里的多个容器就不是对等关系,而是拓扑关系了。所以,在 Kubernetes 项目里,Pod 的实现需要使用一个中间容器,这个容器叫作 Infra 容器。它使用的是一个非常特殊的镜像,叫作:k8s.gcr.io/pause 。在 Pod 中,Infra 容器永远都是第一个被创建的容器,而其他用户定义的容器,则通过 JoinNetwork Namespace 的方式,与 Infra 容器关联在一起。这样的组织关系,可以用下面这样一个示意图来表达:

Infra容器

  并且CNI(容器网络方案)的设计思想就是K8s创建pod时,在启动infra容器后可以直接调用CNI插件,为其配置符合预期的网络栈。

  配好网络后,共享 Volume 就简单多了:Kubernetes 只要把所有 Volume 的定义都设计在 Pod 层级即可。大家都挂同一个目录就好了。

  这也就意味着,对于 Pod 里的容器 A 和容器 B 来说:
    它们可以直接使用 localhost 进行通信;
    它们看到的网络设备跟 Infra 容器看到的完全一样;
    一个 Pod 只有一个 IP 地址,也就是这个 Pod 的 Network Namespace 对应的 IP 地址;
  当然,其他的所有网络资源,都是一个 Pod 一份,并且被该 Pod 中的所有容器共享;Pod 的生命周期只跟 Infra 容器一致,而与容器 A 和 B 无关。

  Pod 这种“超亲密关系”容器的设计思想,实际上就是希望,当用户想在一个容器里跑多个功能并不相关的应用时,应该优先考虑它们是不是更应该被描述成一个 Pod 里的多个容器。

  Pod这样设计的原因还有为了容器耦合,考虑了sidecar的设计思想。就如同那个经典的例子,我们现在有一个 Java Web 应用的 WAR 包,它需要被放在 Tomcat 的 webapps 目录下运行起来。假如,你现在只能用 Docker 来做这件事情,那该如何处理这个组合关系呢?

  一种方法是,把 WAR 包直接放在 Tomcat 镜像的 webapps 目录下,做成一个新的镜像运行起来。可是,这时候,如果你要更新 WAR 包的内容,或者要升级 Tomcat 镜像,就要重新制作一个新的发布镜像,非常麻烦。

  另一种方法是,你压根儿不管 WAR 包,永远只发布一个 Tomcat 容器。不过,这个容器的webapps 目录,就必须声明一个 hostPath 类型的 Volume,从而把宿主机上的 WAR 包挂载进Tomcat 容器当中运行起来。不过,这样你就必须要解决一个问题,即:如何让每一台宿主机,都预先准备好这个存储有 WAR 包的目录呢?这样来看,你只能独立维护一套分布式存储系统了。

  实际上,有了 Pod 之后,这样的问题就很容易解决了。我们可以把 WAR 包和 Tomcat 分别做成镜像,然后把它们作为一个 Pod 里的两个容器“组合”在一起。每次war包镜像先启动,并把war包复制到一个目录下,tomcat再启动,从目录下读取war包。然后每次更新我更新war包镜像就可以了。所以可以看出Pod这种容器组合我们必要的。

  当然再往下还有Pod的字段属性、各种命令用法、生命周期、健康检查、恢复机制等,这些我都不会。

Deployment

  书接上文,我们可以看到Pod就是对容器的进一步抽象和封装。现在K8s需要控制这个Pod那么就需要控制器了。控制器也有很多种,每种控制器以独有的方式负责某种编排功能,常用的还有job、cornjob、volume、replicaset。

  控制器是什么,kubernetes官方解释是:一个永不终止的控制循环,它持续管理着集群的状态,通过apiserver获取系统的状态,并且不断尝试以达到预期状态。那控制循环又是什么呢?回答这个问题之前先看看什么是编排功能?

Deployment就是最基本的控制器,
Deployment.yaml

  这个 Deployment 定义的编排动作非常简单,即:确保携带了 app=nginx 标签的 Pod 的个数,永远等于 spec.replicas 指定的个数,即 2 个。这就意味着,如果在这个集群中,携带 app=nginx 标签的 Pod 的个数大于 2 的时候,就会有旧的Pod 被删除;反之,就会有新的 Pod 被创建。

  好,我现在知道什么是编排功能了,那究竟是 Kubernetes 项目中的哪个组件,在执行这些操作呢?其实就是前面介绍 Kubernetes 架构的时候,曾经提到过一个叫作 kube-controller-manager 的组件。实际上,这个组件,就是一系列控制器的集合。那么这些控制器通过控制循环来控制Pod。而所谓控制循环,可以先用下面的伪码做个理解。

  除了controller-manager还有master的kube-controller-manager和node的kubelet。kubelet 通过心跳汇报的容器状态和节点状态,或者监控系统中保存的应用监控数据。而期望状态就是我们在yaml文件写的状态,会被存在etcd中。

  1. Deployment 控制器从 Etcd 中获取到所有携带了“app: nginx”标签的 Pod,然后统计它们的数量,这就是实际状态;
  2. Deployment 对象的 Replicas 字段的值就是期望状态;
  3. Deployment 控制器将两个状态做比较,然后根据比较结果,确定是创建 Pod,还是删除已有的 Pod。

  StatefulSet给Pod编了号并将 Pod 的拓扑状态(比如:哪个节点先启动,哪个节点后启动),按照 Pod 的“名字 + 编号”的方式固定了下来。并且用Pod 对应的 DNS 记录来访问Pod

ReplicaSet

  Deployment 看似简单,但实际上,它实现了 Kubernetes 项目中一个非常重要的功能:Pod的“水平扩展 / 收缩”(horizontal scaling out/in)。这个功能,是从 PaaS 时代开始,一个平台级项目就必须具备的编排能力。比如我更新了yaml文件中的Pod模板,修改了容器镜像,修改了Pod个数,那么重新加载了Deployment就必须升级了。而这个能力的实现,依赖的是 Kubernetes 项目中的一个非常重要的概念(API 对象):ReplicaSet。

ReplicaSet

  看这个 YAML 文件,是不是觉得和和Deployment很像,其实ReplicaSet的定义就是 Deployment 的一个子集。更重要的是,Deployment 控制器实际操纵的,正是这样的 ReplicaSet 对象,而不是 Pod 对象。
replicaset

  ReplicaSet 负责通过“控制器模式”,保证系统中 Pod 的个数永远等于指定的个数(比如,3 个)。这也正是 Deployment 只允许容器的 restartPolicy=Always 的主要原因:只有在容器能保证自己始终是 Running 状态的前提下,ReplicaSet 调整 Pod 的个数才有意义。

  而在此基础上,Deployment 同样通过“控制器模式”,来操作 ReplicaSet 的个数和属性,进而实现“水平扩展 / 收缩”和“滚动更新”这两个编排动作。其中,“水平扩展 / 收缩”非常容易实现,Deployment Controller 只需要修改它所控制的ReplicaSet 的 Pod 副本个数就可以了。比如,把这个值从 3 改成 4,那么 Deployment 所对应的 ReplicaSet,就会根据修改后的值自动创建一个新的 Pod。这就是“水平扩展”了;“水平收缩”则反之。

  滚动更新的道理类似

  Deployment 实际上是一个两层控制器。首先,它通过ReplicaSet 的个数来描述应用的版本;然后,它再通过ReplicaSet 的属性(比如 replicas 的值),来保证 Pod 的副本数量。

  不过,相信你也能够感受到,Kubernetes 项目对 Deployment 的设计,实际上是代替我们完成了对“应用”的抽象,使得我们可以使用这个 Deployment 对象来描述应用,使用 kubectl rollout 命令控制应用的版本。

  可是,在实际使用场景中,应用发布的流程往往千差万别,也可能有很多的定制化需求。比如,我的应用可能有会话黏连(session sticky),这就意味着“滚动更新”的时候,哪个 Pod 能下线,是不能随便选择的。这种场景,光靠 Deployment 自己就很难应对了。对于这种需求,我们可以选择“自定义控制器” CRD(CustomResourceDefinition),来帮我们实现一个功能更加强大的 Deployment Controller。

新的方案

  这里的两个解决方案都是出自阿里七月份发布的《阿里巴巴云原生实践 15 讲》电子书。

CafeDeployment

  书接上文,实际上我们是需要根据不同的场景来自定义Deployment Controller的。K8s自带的扩容和回滚是用区 Deployment 和 StatefulSet实现的,但是蚂蚁金服金融分布式架构 - 云应用引擎组觉得还是有问题,于是开发了 CafeDeployment ,它致力于解决以下问题:

  原地升级(InplaceSet):升级过程中 Pod 的 IP 保持不变,可和经典的运维监控体系做无缝集成。
  替换升级(ReplicaSet):和社区版本的 ReplicaSet 能力保持一致。
  有状态应用(StatefulSet):和社区版本的 StatefulSet 能力保持一致。
除此之外,相比社区的 deployment,还具备 beta 验证,自定义分组策略,分组暂停,引流验证(配合 ServiceMesh)的能力。

  1. IP 不可变
      对于很多运维体系建设较为早期的用户,使用的服务框架、监控、安全策略,大量依赖 IP 作为唯一标识而被广泛使用。迁移到 Kubernetes 最大的改变就是 IP 会飘,而这对于他们来说,无异于运维、服务框架的推倒重来。
  2. 金融体系下的高可用
      Deployment/StatefulSet 无法根据特定属性进行差异化部署。而在以同城双活为建设基础的金融领域,为了强管控 Pod 的部署结构(即保证每个机房 / 部署单元都有副本运行,若通过原生组件进行部署,我们不得不维护多个几乎一模一样的Deployment/StatefulSet),来保证 Pod 一定会飘到指定机房 / 部署单元的 node上。在规模达到一定程度后,这疑加大了运维管控的复杂度和成本。
  3. 灵活的部署策略
      Deployment 无法控制发布步长,StatefulSet 虽然可以控制步长(StaetfulSet可以控制Pod副本的启停顺序,后一个pod一定要前一个running并且ready了才能使用),但是每次都需要人工计算最新版本需要的副本数并修改 Partition,在多机房 / 部署单元的情况下,光想想发布要做的操作都脑袋炸裂。

  在面对以上这些问题的时候,我们思考:能不能有一个类似 Deployment的东西,不仅可以实现副本保持,而且还能协助用户管控应用节点部署结构、做Beta 验证、分批发布,减少用户干预流程,实现最大限度减少发布风险的目标,做到快速止损,并进行修正干预。这就是我们为什么选择定义了自己的 CRD——CafeDeployment。

  CafeDeployment主要提供跨部署单元的管理功能,其下管理多个InPlaceSet。每个InPlaceSet对应一个部署单元。部署单元是逻辑概念,它通过Node上的label来划分集群中的节点,而InPlaceSet则通过NodeAffinity能力,将其下的Pod部署到同一个部署单元的机器上,由此实现CafeDeployment跨部署单元的管理。

  可以将大部分的控制逻辑都抽取到上层CafeDeployment中,并且设计了InPlaceSet,将它做得足够简单,只关注于“ InPlace ”相关的功能,即副本保持和原地升级,保持IP不变的能力,

灵活的分组定义
  CafeDeployment 支持跨部署单元的分组扩容、 Pod 调度、分组发布。分组策略主要分为两种,Beta 分组和 Batch 分组:

安全的分组扩容和发布能力
  分组扩容
  为预防不正确的配置造成大量错误 Pod 同时创建、占用大量资源等意外情况出现,CafeDeployment 支持分组扩容,以降低风险。

Pod 发布与外部的通信机制
  使 用 Readiness Gate 作 为 Pod 是 否 可 以 承 载 外 部 流 量 的 标 识。

自适应的 Pod 重调度
  在创建 Pod 的过程中,可能会遇到某个部署单元资源不足的情况,新的 Pod 会一直 Pending。这时如果打开自动重调度功能(如下所示),则 CafeDeploymentController 会尝试将 Pod 分配到其他未出现资源紧张的部署单元上。

可适配多种社区工作负载
  CafeDeploymentController 本身只提供了发布策略和跨部署单元管理的一个抽象实现,它对底层的 Pod 集合是通过 PodSetControlInterface 接口来控制。因此,通过对此接口的不同实现,可以保证对接多种 workload。目前已经实现了与InPlaceSet 和 ReplicaSet 的对接,对 StatefulSet 的对接也在进行中。

  为预防不正确的配置造成大量错误Pod同时创建、占用大量资源等意外情况出现,CafeDeployment支持分组扩容,以降低风险。

在 Web 级集群中动态调整 Pod 资源限制

  这个功能对标的是K8s里面的HPA,VPA。HPA是Pod水平伸缩的组件,VPA是Pod纵向伸缩的组件

  当我们拥有了一套Kubernetes集群,然后开始部署应用的时候,我们应该给容器分配多少资源呢?很难说。由于Kubernetes自己的机制,我们可以理解容器的资源实质上是一个静态的配置。如果发现资源不足,为了分配给容器更多资源,我们需要重建Pod。如果分配冗余的资源,那么我们的worker node节点似乎又部署不了多少容器。试问,我们能做到容器资源的按需分配吗?

  对于电商应用,对于采用了重量级 java 框架和相关技术栈的 web 应用,短时间内 HPA 或者 VPA 都不是件容易的事情。先说 HPA,我们或许可以秒级拉起了
Pod,创建新的容器,然而拉起的容器是否真的可用呢。从创建到可用,可能需要比较久的时间,对于大促和抢购秒杀 - 这种访问量“洪峰”可能仅维持几分钟或者十几分钟的实际场景,如果我们等到 HPA 的副本全部可用,可能市场活动早已经结束了。至于社区目前的 VPA 场景,删掉旧 Pod,创建新 Pod,这样的逻辑更难接受。所以综合考虑,我们需要一个更实际的解决方案弥补 HPA 和 VPA 的在这一单机资源调度的空缺。

  安全稳定:工具本身高可用。所用的算法和实施手段必须做到可控。
  ● 业务容器按需分配资源:可以及时根据业务实时资源消耗对不太久远的将来进行资源消耗预测,让用户明白业务接下来对于资源的真实需求。
  ● 工具本身资源开销小:工具本身资源的消耗要尽可能小,不要成为运维的负担。
操作方便,扩展性强:能做到无需接受培训即可玩转这个工具,当然工具还要具有良好扩展性,供用户 DIY。
  ● 快速发现 & 及时响应:实时性,也就是最重要的特质,这也是和 HPA 或者VPA 在解决资源调度问题方式不同的地方

  我们也在尝试和 HPA,VPA 进行打通,毕竟这些和 Policy engine 是存在着互补的关系。因此我们架构进一步演进成如下情形。当 Policy engine 在处理一些更多复杂场景搞到无力时,上报事件让中心端做出更全局的决策。水平扩容或是垂直增加资源。

  在未来,我们计划打通单机节点和中心端的资源调控链路,由中心端综合单机节点上报的性能信息和资源调整请求,统一进行资源的重新分配,或者容器的重新编排,或者触发 HPA,从而形成一个集群级别的闭环的智能资源调控链路,这将会大大提高整个集群维度的稳定性和综合资源利
用率

  策略智能化:我们现在的资源调整策略仍然比较粗粒度,可以调整的资源也比较有限;后续我们希望让资源调整策略更加智能化,并且考虑到更多的资源,比如对磁盘和网络 IO 带宽的调整,提高资源调整的有效性。

  SLO(服务等级目标)指定了服务所提供功能的一种期望状态。

  这段代码预计19年9月加入开源计划