揭秘:如何为 Kubernetes 实现原地升级

头图.png

做者 | 王思宇(酒祝)  阿里云技术专家nginx

参与阿里巴巴云原生文末留言互动,即有机会得到赠书福利及做者答疑!git

概念介绍

原地升级一词中,“升级”不难理解,是将应用实例的版本由旧版替换为新版。那么如何结合 Kubernetes 环境来理解“原地”呢?github

咱们先来看看 K8s 原生 workload 的发布方式。这里假设咱们须要部署一个应用,包括 foo、bar 两个容器在 Pod 中。其中,foo 容器第一次部署时用的镜像版本是 v1,咱们须要将其升级为 v2 版本镜像,该怎么作呢?docker

  • 若是这个应用使用 Deployment 部署,那么升级过程当中 Deployment 会触发新版本 ReplicaSet 建立 Pod,并删除旧版本 Pod。以下图所示:

1.png

在本次升级过程当中,原 Pod 对象被删除,一个新 Pod 对象被建立。新 Pod 被调度到另外一个 Node 上,分配到一个新的 IP,并把 foo、bar 两个容器在这个 Node 上从新拉取镜像、启动容器。api

  • 若是这个应该使用 StatefulSet 部署,那么升级过程当中 StatefulSet 会先删除旧 Pod 对象,等删除完成后用一样的名字在建立一个新的 Pod 对象。以下图所示:

2.png

值得注意的是,尽管新旧两个 Pod 名字都叫 pod-0,但实际上是两个彻底不一样的 Pod 对象(uid也变了)。StatefulSet 等到原先的 pod-0 对象彻底从 Kubernetes 集群中被删除后,才会提交建立一个新的 pod-0 对象。而这个新的 Pod 也会被从新调度、分配IP、拉镜像、启动容器。网络

  • 而所谓原地升级模式,就是在应用升级过程当中避免将整个 Pod 对象删除、新建,而是基于原有的 Pod 对象升级其中某一个或多个容器的镜像版本:

3.png

在原地升级的过程当中,咱们仅仅更新了原 Pod 对象中 foo 容器的 image 字段来触发 foo 容器升级到新版本。而不论是 Pod 对象,仍是 Node、IP 都没有发生变化,甚至 foo 容器升级的过程当中 bar 容器还一直处于运行状态。less

总结:这种只更新 Pod 中某一个或多个容器版本、而不影响整个 Pod 对象、其他容器的升级方式,被咱们称为 Kubernetes 中的原地升级。异步

收益分析

那么,咱们为何要在 Kubernetes 中引入这种原地升级的理念和设计呢?ide

首先,这种原地升级的模式极大地提高了应用发布的效率,根据非彻底统计数据,在阿里环境下原地升级至少比彻底重建升级提高了 80% 以上的发布速度。这其实很容易理解,原地升级为发布效率带来了如下优化点:微服务

  1. 节省了调度的耗时,Pod 的位置、资源都不发生变化;
  2. 节省了分配网络的耗时,Pod 还使用原有的 IP;
  3. 节省了分配、挂载远程盘的耗时,Pod 还使用原有的 PV(且都是已经在 Node 上挂载好的);
  4. 节省了大部分拉取镜像的耗时,由于 Node 上已经存在了应用的旧镜像,当拉取新版本镜像时只须要下载不多的几层 layer。

其次,当咱们升级 Pod 中一些 sidecar 容器(如采集日志、监控等)时,其实并不但愿干扰到业务容器的运行。但面对这种场景,Deployment 或 StatefulSet 的升级都会将整个 Pod 重建,势必会对业务形成必定的影响。而容器级别的原地升级变更的范围很是可控,只会将须要升级的容器作重建,其他容器包括网络、挂载盘都不会受到影响。

最后,原地升级也为咱们带来了集群的稳定性和肯定性。当一个 Kubernetes 集群中大量应用触发重建 Pod 升级时,可能形成大规模的 Pod 飘移,以及对 Node 上一些低优先级的任务 Pod 形成反复的抢占迁移。这些大规模的 Pod 重建,自己会对 apiserver、scheduler、网络/磁盘分配等中心组件形成较大的压力,而这些组件的延迟也会给 Pod 重建带来恶性循环。而采用原地升级后,整个升级过程只会涉及到 controller 对 Pod 对象的更新操做和 kubelet 重建对应的容器。

技术背景

在阿里巴巴内部,绝大部分电商应用在云原生环境都统一用原地升级的方式作发布,而这套支持原地升级的控制器就位于 OpenKruise 开源项目中。

也就是说,阿里内部的云原生应用都是统一使用 OpenKruise 中的扩展 workload 作部署管理的,而并无采用原生 Deployment/StatefulSet 等。

那么 OpenKruise 是如何实现原地升级能力的呢?在介绍原地升级实现原理以前,咱们先来看一些原地升级功能所依赖的原生 Kubernetes 功能:

背景 1:Kubelet 针对 Pod 容器的版本管理

每一个 Node 上的 Kubelet,会针对本机上全部 Pod.spec.containers 中的每一个 container 计算一个 hash 值,并记录到实际建立的容器中。

若是咱们修改了 Pod 中某个 container 的 image 字段,kubelet 会发现 container 的 hash 发生了变化、与机器上过去建立的容器 hash 不一致,然后 kubelet 就会把旧容器停掉,而后根据最新 Pod spec 中的 container 来建立新的容器。

这个功能,其实就是针对单个 Pod 的原地升级的核心原理。

背景 2:Pod 更新限制

在原生 kube-apiserver 中,对 Pod 对象的更新请求有严格的 validation 校验逻辑

// validate updateable fields:
// 1.  spec.containers[*].image
// 2.  spec.initContainers[*].image
// 3.  spec.activeDeadlineSeconds

简单来讲,对于一个已经建立出来的 Pod,在 Pod Spec 中只容许修改 containers/initContainers 中的 image 字段,以及 activeDeadlineSeconds 字段。对 Pod Spec 中全部其余字段的更新,都会被 kube-apiserver 拒绝。

背景 3:containerStatuses 上报

kubelet 会在 pod.status 中上报 containerStatuses,对应 Pod 中全部容器的实际运行状态:

apiVersion: v1
kind: Pod
spec:
  containers:
  - name: nginx
    image: nginx:latest
status:
  containerStatuses:
  - name: nginx
    image: nginx:mainline
    imageID: docker-pullable://nginx@sha256:2f68b99bc0d6d25d0c56876b924ec20418544ff28e1fb89a4c27679a40da811b

绝大多数状况下,spec.containers[x].image 与 status.containerStatuses[x].image 两个镜像是一致的。

可是也有上述这种状况,kubelet 上报的与 spec 中的 image 不一致(spec 中是 nginx:latest,但 status 中上报的是 nginx:mainline)。

这是由于,kubelet 所上报的 image 实际上是从 CRI 接口中拿到的容器对应的镜像名。而若是 Node 机器上存在多个镜像对应了一个 imageID,那么上报的多是其中任意一个:

$ docker images | grep nginx
nginx            latest              2622e6cca7eb        2 days ago          132MB
nginx            mainline            2622e6cca7eb        2 days ago

所以,一个 Pod 中 spec 和 status 的 image 字段不一致,并不意味着宿主机上这个容器运行的镜像版本和指望的不一致。

背景 4:ReadinessGate 控制 Pod 是否 Ready

在 Kubernetes 1.12 版本以前,一个 Pod 是否处于 Ready 状态只是由 kubelet 根据容器状态来断定:若是 Pod 中容器所有 ready,那么 Pod 就处于 Ready 状态。

但事实上,不少时候上层 operator 或用户都须要能控制 Pod 是否 Ready 的能力。所以,Kubernetes 1.12 版本以后提供了一个 readinessGates 功能来知足这个场景。以下:

apiVersion: v1
kind: Pod
spec:
  readinessGates:
  - conditionType: MyDemo
status:
  conditions:
  - type: MyDemo
    status: "True"
  - type: ContainersReady
    status: "True"
  - type: Ready
    status: "True"

目前 kubelet 断定一个 Pod 是否 Ready 的两个前提条件:

  1. Pod 中容器所有 Ready(其实对应了 ContainersReady condition 为 True);
  2. 若是 pod.spec.readinessGates 中定义了一个或多个 conditionType,那么须要这些 conditionType 在 pod.status.conditions 中都有对应的 status: "true" 的状态。

只有知足上述两个前提,kubelet 才会上报 Ready condition 为 True。

实现原理

了解了上面的四个背景以后,接下来分析一下 OpenKruise 是如何在 Kubernetes 中实现原地升级的原理。

1. 单个 Pod 如何原地升级?

由“背景 1”可知,其实咱们对一个存量 Pod 的 spec.containers[x] 中字段作修改,kubelet 会感知到这个 container 的 hash 发生了变化,随即就会停掉对应的旧容器,并用新的 container 来拉镜像、建立和启动新容器。

由“背景 2”可知,当前咱们对一个存量 Pod 的 spec.containers[x] 中的修改,仅限于 image 字段。

所以,得出第一个实现原理:**对于一个现有的 Pod 对象,咱们能且只能修改其中的 spec.containers[x].image 字段,来触发 Pod 中对应容器升级到一个新的 image。

2. 如何判断 Pod 原地升级成功?

接下来的问题是,当咱们修改了 Pod 中的 spec.containers[x].image 字段后,如何判断 kubelet 已经将容器重建成功了呢?

由“背景 3”可知,比较 spec 和 status 中的 image 字段是不靠谱的,由于颇有可能 status 中上报的是 Node 上存在的另外一个镜像名(相同 imageID)。

所以,得出第二个实现原理:判断 Pod 原地升级是否成功,相对来讲比较靠谱的办法,是在原地升级前先将 status.containerStatuses[x].imageID 记录下来。在更新了 spec 镜像以后,若是观察到 Pod 的 status.containerStatuses[x].imageID 变化了,咱们就认为原地升级已经重建了容器。

但这样一来,咱们对原地升级的 image 也有了一个要求:不能用 image 名字(tag)不一样、但实际对应同一个 imageID 的镜像来作原地升级,不然可能一直都被判断为没有升级成功(由于 status 中 imageID 不会变化)。

固然,后续咱们还能够继续优化。OpenKruise 即将开源镜像预热的能力,会经过 DaemonSet 在每一个 Node 上部署一个 NodeImage Pod。经过 NodeImage 上报咱们能够得知 pod spec 中的 image 所对应的 imageID,而后和 pod status 中的 imageID 比较便可准确判断原地升级是否成功。

3. 如何确保原地升级过程当中流量无损?

在 Kubernetes 中,一个 Pod 是否 Ready 就表明了它是否能够提供服务。所以,像 Service 这类的流量入口都会经过判断 Pod Ready 来选择是否能将这个 Pod 加入 endpoints 端点中。

由“背景 4”可知,从 Kubernetes 1.12+ 以后,operator/controller 这些组件也能够经过设置 readinessGates 和更新 pod.status.conditions 中的自定义 type 状态,来控制 Pod 是否可用。

所以,得出第三个实现原理:能够在 pod.spec.readinessGates 中定义一个叫 InPlaceUpdateReady 的 conditionType。

在原地升级时:

  1. 先将 pod.status.conditions 中的 InPlaceUpdateReady condition 设为 "False",这样就会触发 kubelet 将 Pod 上报为 NotReady,从而使流量组件(如 endpoint controller)将这个 Pod 从服务端点摘除;
  2. 再更新 pod spec 中的 image 触发原地升级。

原地升级结束后,再将 InPlaceUpdateReady condition 设为 "True",使 Pod 从新回到 Ready 状态。

另外在原地升级的两个步骤中,第一步将 Pod 改成 NotReady 后,流量组件异步 watch 到变化并摘除端点多是须要必定时间的。所以咱们也提供优雅原地升级的能力,即经过 gracePeriodSeconds 配置在修改 NotReady 状态和真正更新 image 触发原地升级两个步骤之间的静默期时间。

4. 组合发布策略

原地升级和 Pod 重建升级同样,能够配合各类发布策略来执行:

  • partition:若是配置 partition 作灰度,那么只会将 replicas-partition 数量的 Pod 作原地升级;
  • maxUnavailable:若是配置 maxUnavailable,那么只会将知足 unavailable 数量的 Pod 作原地升级;
  • maxSurge:若是配置 maxSurge 作弹性,那么当先扩出来 maxSurge 数量的 Pod 以后,存量的 Pod 仍然使用原地升级;
  • priority/scatter:若是配置了发布优先级/打散策略,会按照策略顺序对 Pod 作原地升级。

总结

如上文所述,OpenKruise 结合 Kubernetes 原生提供的 kubelet 容器版本管理、readinessGates 等功能,实现了针对 Pod 的原地升级能力。

而原地升级也为应用发布带来大幅的效率、稳定性提高。值得关注的是,随着集群、应用规模的增大,这种提高的收益越加明显。正是这种原地升级能力,在近两年帮助了阿里巴巴超大规模的应用容器平稳迁移到了基于 Kubernetes 的云原生环境,而原生 Deployment/StatefulSet 是彻底没法在这种体量的环境下铺开使用的。(欢迎加入钉钉交流群:23330762)

- 赠书福利 -

尾图.png

6 月 19 日 12:00 前在【阿里巴巴云原生公众号】留言区提出你的疑问,精选留言点赞第 1 名将免费得到此书,届时咱们还会请本文做者针对留言点赞前 5 名的问题进行答疑!

课程推荐

为了更多开发者可以享受到 Serverless 带来的红利,这一次,咱们集结了 10+ 位阿里巴巴 Serverless 领域技术专家,打造出最适合开发者入门的 Serverless 公开课,让你即学即用,轻松拥抱云计算的新范式——Serverless。

点击便可免费观看课程:https://developer.aliyun.com/learning/roadmap/serverless

阿里巴巴云原生关注微服务、Serverless、容器、Service Mesh 等技术领域、聚焦云原生流行技术趋势、云原生大规模的落地实践,作最懂云原生开发者的公众号。”
相关文章
相关标签/搜索