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

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

概念介绍

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

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

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

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

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

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

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

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

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

收益分析

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

首先,这种原地升级的模式极大地提升了应用发布的效率,根据非完全统计数据,在阿里环境下原地升级至少比完全重建升级提升了 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: nginximage: nginx:latest
status:containerStatuses:- name: nginximage: nginx:mainlineimageID: 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: MyDemostatus: "True"- type: ContainersReadystatus: "True"- type: Readystatus: "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)

- 赠书福利 -

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

课程推荐

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

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

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

原文链接
本文为云栖社区原创内容,未经允许不得转载。

如何为Kubernetes实现原地升级相关推荐

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

    作者 | 王思宇(酒祝)  阿里云技术专家 参与阿里巴巴云原生文末留言互动,即有机会获得赠书福利及作者答疑! 概念介绍 原地升级一词中,"升级"不难理解,是将应用实例的版本由旧版替 ...

  2. Kubernetes 集群升级指南:从理论到实践

    作者 | 高相林(禅鸣) **导读:**集群升级是 Kubernetes 集群生命周期中最为重要的一环,也是众多使用者最为谨慎对待的操作之一.为了更好地理解集群升级这件事情的内涵外延,我们首先会对集群 ...

  3. 深入解析阿里 PouchContainer 如何实现容器原地升级

    PouchContainer 是阿里巴巴集团开源的高效.轻量级企业级富容器引擎技术,拥有隔离性强.可移植性高.资源占用少等特性.可以帮助企业快速实现存量业务容器化,同时提高超大规模下数据中心的物理资源 ...

  4. Oracle 11g RAC 原地升级到 19c

    作者 | JiekeXu 来源 |公众号 JiekeXu DBA之路(ID: JiekeXu_IT) 如需转载请联系授权 | (个人微信 ID:JiekeXu_DBA) 大家好,我是 JiekeXu, ...

  5. 云原生|kubernetes|kubernetes集群升级+证书更新(Ubuntu-18.04+kubeadm)

    前言: kubernetes集群根据部署手法来分类,一般是两种,一种是基于kubeadm搭建的集群,一种是二进制方式搭建的集群.那么,二进制集群升级和证书更新就完全是手动处理了,而kubeadm的集群 ...

  6. 原地升级k8s集群docker和containerd版本

    微信公众号:运维开发故事,作者:小姜 前言 公司用的k8s集群是"多环境合一"的方式,集群流量入口也摒弃了常见的traefik和ingress-nginx,直接用了一个国内不常见的 ...

  7. 【云原生】Kubernetes集群升级

    [云原生]Kubernetes集群升级指南 前言 一.集群升级过程辅助命令 二.升级master节点 2.1.升级kubeadm. 2.2.验证升级计划 2.3.master节点升级 三.升级node ...

  8. 刚入职新公司做一些什么贡献_如果您有全职工作,如何为Kubernetes做贡献

    刚入职新公司做一些什么贡献 当我在IBM产品安全事件响应团队工作时,我于2018年10月开始为Kubernetes (K8s)做出贡献. 我被分布式系统所吸引,但在日常工作中无法与它们合作,因此我的导 ...

  9. 全职工作者如何为 Kubernetes 做贡献

    | 作者:Tara Gu | 编辑:王皓月 | 设计:朱亿钦 | 翻译:Rudy Steiner | 审稿:颖颖子,PuPaul_II_ 你甚至可以在业余时间参与最大的开源项目之一的内部工作. 我是从 ...

最新文章

  1. CImg库的一个简单例子
  2. /etc/rc.d 与 /etc/profile或者./.bash_profile的区别
  3. 带你了解加速度传感器的几种应用
  4. 计算机视觉编程——增强现实基础
  5. 概念模型让产品更简单
  6. linux fedora安装、运行mybase7.3.5报错:error while loading shared libraries: libpng12.so.0
  7. jvm_垃圾收集算法讲解(二)
  8. 为什么一定要用MQ中间件
  9. js中 json详解
  10. MYSQL 常用查询命令
  11. linux环境下调试嵌入式设备时出现Aborted、segmentation fault、卡死的问题以及关于指针使用的一点想法
  12. 手把手教你如何逐步安装OpenStack
  13. 目标检测——标注文件的格式设计
  14. mysql数据库管理手册_CentOS MySQL 用户及数据库管理手册
  15. 史上最强Dubbo面试26题和答案:核心组件+服务治理+架构设计等
  16. 20款开源搜索引擎介绍与比较
  17. Matlab:Matlab编程语言的简介、安装、学习路线(几十项代码编程案例分析)之详细攻略
  18. Python学习资料篇
  19. [读史思考] 魏王真的被张仪忽悠瘸了吗?
  20. dcos universe

热门文章

  1. java resources 目录_[Java] 在 jar 文件中读取 resources 目录下的文件
  2. go channel 缓冲区最大限制_Go语言11周年,泛型问题有望明年得到解决
  3. ip访问次数统计 nginx_Linux分析Nginx日志统计IP访问次数的shell脚本
  4. linux命令cp命令行参数,linux命令之cp命令参数及用法详解
  5. 小白零基础怎么学习Java?不要慌
  6. 六西格玛dfss_向六西格玛质量水平进攻!
  7. 技术交底软件_【干货分享】软件类产品如何进行专利挖掘与技术交底书撰写?...
  8. 【LeetCode笔记】剑指 Offer 21-. 调整数组顺序使奇数位于偶数前面(Java、双指针)
  9. python中newfile是干嘛用的_Python基础介绍 | File I\O 读写文件
  10. 中国工程院院士和科学院院士有什么区别,谁贡献很大却没有入院士?