蚂蚁大规模 Kubernetes 集群无损升级实践指南【探索篇】

e7bb673570cbe543bdb29fd40245367b.png

 为什么要持续迭代升级 /strong>

Kubernetes 社区的活跃度非常高,众多的云原生爱好者为社区贡献智慧,推动社区版本不断更新。升级是为了紧跟社区的步伐,及时享用社区沉淀下来的优秀特性,进而给公司带来更大利益。

 为什么升级那么难 /strong>

按照蚂蚁 Sigma 的规模,升级对我们来讲是一件非常不容易的事情,主要体现在:

– 在升级准备阶段,要全量推动客户端进行升级,业务方要安排专门的人投入进来,耗时耗力;

– 在升级过程中,为了规避版本滚动时对 Kubernetes 资源操作可能带的来不可预期后果,升级过程中一般会关停流量,业务体感不好;

– 对于升级时间窗口选择,为了给用户更好的服务体验,升级要放到业务量少的时间进行,这对平台运维人员不太友好。

因此,升级过程中如何提升用户、研发、SRE 的幸福感是我们想要达成的目标。我们期望实现无损升级来降低升级风险,解耦用户来提升幸福感,高效迭代来提供更强大的平台能力,最终实现无人值守。

本文将结合蚂蚁 Sigma 系统升级实践,从 Kubernetes 系统升级的目标、挑战开始,逐步剖析相关的 Kubernetes 知识,针对这些挑战给出蚂蚁 Sigma 的一些原则和思考。

【两种不同的升级思路】

在介绍挑战和收益前,我们先了解下当前集群升级的方式。Kubernetes 升级与普通软件升级类似,主要有以下两种常见的升级方式:替换升级和原地升级。

– 替换升级:将应用运行的环境切换到新版本,将旧版本服务下线,即完成替换。在 Kubernetes 升级中,即升级前创建新版本的 Kubernetes 集群,将应用迁移到新的 Kubernetes 集群中,然后将旧版本集群下线。当然,这种替换升级可以从不同粒度替换,从集群为度则是切换集群;从节点维度,则管控节点组件单独升级后,kubelet 节点升级时迁移节点上的 Pod 到新版本节点,下线旧版本节点。

– 原地升级:将升级的软件包原地替换,旧服务进程停止,用新的软件包重新运行服务。在 Kubernetes 升级中,apiserver 和 kubelet 采用原地软件包更新,然后重启服务,这种方式与替换升级最大的区别在于节点上的 workload 不用迁移,应用不用中断,保持业务的连续性。

上述两种方式各有优缺点,蚂蚁 Sigma 采用的是原地升级。

【方法论-庖丁解牛】

采用原地升级时也必然会遇到原地升级的问题,其中最主要问题就是兼容性问题,主要包含两个方面:Kubernetes API 和组件内部的控制逻辑兼容性。

Kubernetes API 层面包含 API 接口、resource 结构和 feature 三方面变化,而组件内部控制逻辑变化主要是 resource 在 Kubernetes 内部流转行为的变化。

前者是影响用户及集群稳定性最重要的因素,也是我们重点解决的问题。

eefbc8119f22358f8e151727818933e0.png

还有一个重要的环节,整个过程我们要做到自动化、可视化,在升级过程中流量的充分灰度是很有必要的,升级节奏的自动化推进和应急场景下的人工可控性也是非常重要的,这些将在另一篇文章中详细介绍。

整体来看,我们通过客户端最小化升级和滚动自动化升级能力、提升升级的效率,通过精细化流量控制、灰度可回滚能力以及长效的字段管控能力,提升整个升级过程的可靠性、稳定性。

PART. 2

升级前

集群升级必然会有 API 的更新和迭代,主要体现在 API 新增、演进和移除,在 Kubernetes 中 API 的演进一般是 Alpha、beta、GA,一个 resouce 的 API version 会按照上述版本进行迭代,当一个 API 新增时,最开始是 Alpha 阶段,例如”cert-manager.io/v1alpha3″,经过若干次迭代,新特性进入 beta 版本,最后进入稳定的 GA 版本,这个过程可能跨若干个大的社区版本,一些版本会在 GA 版本稳定运行一定时间后被 deprached 掉,并且被 deprached 的 API 版本在一段时间后会被直接移除,这就对我们的客户端有了升级的刚性需求。

在介绍客户端升级前,先介绍下一般 resource API 变化有哪些方面。

【Schema 变化】

不同版本的 Kubernetes 资源的 Schema 字段可能存在差异,主要表现在以下两个方面:

字段的增加/删除/修改

字段的默认值调整

字段增删改

Kubernetes 的 resource 如果对某个字段的调整,包括:增加、删除、修改三种。对于“增加”操作,可以在新 GV(GroupVersion)出现也可以在旧 GV 中出现。对于“删除”和“修改”,一般只会在新的 GV 中出现。

基于以上条件,对于新版本 APISever 引入的 resource 字段调整,可以得出以下结论:

8b1e19478338610c48c49d08a76bd79b.png

如上图所示,核心问题主要有以下三种:

1.低版本客户端访问已经被 depreached/removed 的 GroupVersionKind

2.低版本客户端操作的 resource 存在字段增加问题

3.低版本操作的 resource 存在字段默认值变化问题

针对第一个问题,访问已经被 depreached 特别是被 removed 的 GVK 时,服务器端直接返回类 404 错误,表示此 rest url 或者次 GVK 已经不存在。

针对第二个和第三个问题,都出现在低版本客户端的 Update 操作,为什么对于 patch 操作不会出现问题呢为 Update 操作是全量更新,patch 操作是局部更新,全量更新的情况下,如果客户端版本低没有新增字段或者没默认值变化的字段,此时去 Update 此 resource,提交的请求数据中不会出现此字段,但是此字段在 apiserver 内部会被补全和填充,如此此字段的值就完全依赖 apiserver 内部的逻辑了。

针对字段增加的情况,我们做了一个实验,如下:

1.18 版本中 Ingress 中多了一个 patchType 的字段,我们首先通过 1.18 的客户端进行创建,并设定 pathType=Prefix,然后通过 1.16 版本的客户端进行 Update,发现此职被修改为 pathType 的默认值。如下:

fc54b519a5b56c76dc05432b38f5bd17.png

针对此此问题,有以下结论:

(1)对于字段增加情况,当通过旧版本 apiserver 更新带有新字段的资源时存在字段默认值被删除的风险;对于字段删除和修改两种情况,无此风险;

(2)如果新增字段被用于计算 container hash,但由于 apiserver 升级时 kubelet 还处于 1.16 版本,所以依旧按照 1.16 版本计算 hash,apiserver 交叉变化不会导致容器重建。

– 字段默认值变化

字段默认值变化是指,在新旧 apiserver 中,对某个资源字段默认值填充不一致。如下图所示,1.16 版本的 Kubernetes 中 Pod 字段”FiledKey”默认值为”default_value_A”,到 1.18 版本时该字段默认值变为”default_value_B”,通过 1.18 apiserver 创建 PodA 后,再通过 1.16 版本 apiserver 更新会出现默认值被篡改的问题。这种情况的发生条件相对苛刻,一般 Update 之前会拉下集群中当前的 Pod 配置,更改关心的字段后重新 Update 回去,这种方式会保持默认值变化的字段值,但是如果用户不拉取集群 Pod 配置,直接 Update 就会出现问题。

96288810dc421ddca0b648a007b91eab.png

数据存储兼容性

本文将侧重讲解 Kubernetes 中 API resource 存储的数据在升级过程中如何保证兼容性的。主要回答以下两个问题:

问题一:

 Kubernetes 中存储在 etcd 中的 

 API resource 长什么样/strong>

Kubernetes 中的 resource 都会有一个 internal version,这个 internal version 只用作在 apiserver 内存中数据处理和流转,并且这个 Internal version 对应的数据是当前 apiserver 支持的多个 GV 的全集,例如 1.16 中支持 apps/v1beta1 和 apps/v1 版本的 deployments。

但是存储到 etcd 时,apiserver 要先将这个 internal 版本转换为 storage 版本 storage 版本怎么确定的呢/p>

如下,分为两种情况:

core resource

存储版本在 apiserver 初始化时确定,针对某个 GroupVersion 分两步确定其存储版本:

(1)确定与本 GV 使用同一存储版本 group resource —> StorageFactory 中静态定义的 overrides

(2)在 group 中选择优先级最高的 version 作为存储版本—> Schema 注册时按照静态定义顺序获取

custom resource

自定义 CR 的存储版本确定在 CRD 的配置中

(详见:CRD 配置) https://kubernetes.io/zh/docs/tasks/extend-kubernetes/custom-resources/custom-resource-definition-versioning/

问题二:

 不同版本的 Kubernetes 中 

 如何做到存储数据兼容/strong>

storage version 在 Kubernetes 版本迭代更新中不是一成不变的,也会不断的更新。首先我们看一个 Kubernetes 中的存储版本的升级规则:给定 API 组的 “storage version(存储版本)”在既支持老版本也支持新版本的 Kubernetes 发布 版本出来以前不可以提升其版本号。

这条规则换句话说,当某个 Kubernetes 版本中某个 resource 的 storage version 有变化时,此版本的 Kubernetes 一定是同时支持新旧两个 storage version 的。如此加上 Schema 中多个 version 之间的转换能力,就能轻松做到版本的升级和降级了。

对于升级或者降级,apiserver 可以动态的识别当前 etcd 中存储的是什么版本的数据,并将其转换为 Internal 版本,然后写入到 etcd 时再转换为当前升级后的最新的 Storage Version 进行存入。

a3bf0bdcd2b0f407e2796bc760fa6504.png

讲到这里可能会有人问,为什么要跨版本升级呢/strong>

上述这个问题如果是从 1.16 到 1.17 再到 1.18 逐个版本升级就不会出现了,这个想法非常好,但对于蚂蚁 Sigma Kubernetes 这种体量来讲频繁的升级难度较大,这也是我们做此事的原生动力,将升级变得更自动化、效率更高,当这个目标实现后此问题也就不复存在了,在当前阶段回退存储版本不兼容问题仍然棘手。

思考与解决

升级本身就是一次引入众多变量的操作,我们尽量做到在变化中找到一条能把控的路子,最基本的方法论就是控制变量,所以对于 API 兼容性问题,我们核心的原则为:新特性没有必要开启的先进性压制,保证可回滚

压制的主要目标有两个:

高版本 apiserver 中新增的 GVK

保证它们在升级的这个版本中不会出现

etcd 中的数据的存储版本

存储版本对用户是透明的,我们也要保证压制调整对用户也是无感的,调整和压制的手段可以通过对 apiserver 代码做兼容性调整来实现。

对于其他兼容性问题,目前没有很好的方案解决,当前我们的主要通过升级回滚 e2e 测试暴露问题,针对不兼容的问题做相应兼容性修改。

兼容性压制的手段只存在于升级过程中,也是升级过程中临时现象。压制调整的时候我们需要充分的考量是否会引入其他不可控问题,这个要具体看 GVK 自身的变化来定。当然,一切还要从理论回到实践,充分的 e2e 测试也是需要的。有了理论和测试两把利刃的加持,我相信兼容性问题会迎刃而解。

以上是升级过程中遇到的三个棘手的问题,以及相关的解决思路,接下来介绍下升级后的保障工作。

PART. 4

升级后

大版本升级时无法保证 100% 的客户端都升级到对应的最新版本。虽然升级前我们会推动 Update 流量的客户端进行升级,但是可能做不到 100% 升级,更重要的,升级后可能也会出现某个用户用低版本的客户端进行访问。我们期望通过 webhook 能够避免升级后低版本客户端意外篡改 resource 字段,达到真正的升级无损的目的。

字段管控主要原则一句话总结:防止默认值变化的字段,被用户使用低版本客户端以 Update 的方式修改为新的 default 值。

【字段管控】

残留问题

字段管控的最大挑战是,如何准确的识别出用户是否是无意篡改字段。判断用户是否无意篡改需要拿到两个关键信息:

用户原始请求内容

用户原始请求内容是判断用户是否无意篡改的关键,如果原始请求中有某个字段的内容,说明用户是明确要修改,此时不需要管控。

– 用户客户端版本信息

否则,要看用户客户端版本是否低于当前集群版本,如果不低于集群版本说明用户有此字段明确修改不需要管控,如果低于集群版本这个字段用户可能看不到就需要管控了。

那么问题来了,如何拿到这两个信息呢说用户原始请求内容,这个信息按照 Kubernetes 的能力,我们无法通过 webhook 或者其他插件机制很轻松的拿到请求内容,apiserver 调用 webhook 时的内容已经是经过版本转换后的内容。

再说用户客户端版本信息,这个信息虽然可以从 apiserver 的监控中拿到,当然我们为了与管控链路对接,并不是直接拉取的监控信息,而是在 apiserver 中做了信息补充。

思考与解决

解决此问题本质上是理解“用户原始意图”,能够识别出哪些动作是无意的篡改哪些是真正的需求,此动作需要依赖以下两个信息:

用户原始请求信息

用户客户端版本信息

上述两个信息存在的获取和准确性问题是我们后续的工作方向之一,当前我们还没很好的办法。理解用户的意图并且是实时理解是非常困难的,折中办法是定义一套规则,按照规则来识别用户是否在使用低版本的客户端篡改一些核心字段,我们宁可误杀一千也不放过一个坏操作,因为造成业务容器的不符合预期,意外行为很轻松就会带来一个 P 级故障。

c5bb12834d72641a7ce2712d8bec3d78.png

PART. 6

未来之路

整体来讲,做好升级核心就是要做好兼容性这件事,同时也要把整个过程做的更自动化一些,观测性做的更好一些,接下来有几个方向的工作要继续进行:

1.更精准

当前在管控的信息获取上还有缺失,在流量的管控上当前采用 namespace 的维度来处理,这些都存在精度不够的问题。前面也提到过,我们正在进行管控组件 Mesh 化能力建设,未来会有更灵活的细粒度流量管控和数据处理能力。同时,借助 Mesh 能力,实现管控组件升级过程中多版本流量灰度测试,对升级做到精确、可控。

2.平台化

本文介绍的这些挑战和技术方案,其实都是升级过程中的一部分,整个过程包含了前期的客户端最小化升级、核心组件滚动升级和后续的管控,这个过程繁琐且易出错,我们期望把这些过程规范化、平台化,把类似差异化比对、流量管控、流量监控等的工具集成到平台中,让升级更方便。

3.更高效

社区迭代速度非常快,当前的迭代速度是无法跟上社区,我们通过上述更智能、平台化的能力,提升基础设施的升级速度。当然,升级的速度也与集群的架构有非常大的关系,后续蚂蚁会走向联邦集群的架构,在联邦架构下可以对特定的用户 API 做向前兼容和转换,由此可以极大地解耦客户端与 apiserver 的升级关系。

对于蚂蚁 Sigma 规模级别的 Kubernetes 集群来讲升级不是一件容易的事,Sigma 作为蚂蚁最核心的运行底座,我们想做到通过技术手段让基础设施的迭代升级达到真正的无感、无损,让用户不再等待,让自己不再焦虑。面对 Kubernetes 这个庞然大物要实现上述目标颇有挑战性,但这并不能阻止我们探索的步伐。道长且阻,行则将至,作为全球 Kubernetes 规模化建设头部的蚂蚁集团将继续向社区输出更稳定、更易用的技术,助力云原生成为技术驱动发展的核心动力。

82cafe18cd5219d4e005b28a60b92978.png

攀登规模化的高峰 – 蚂蚁集团大规模 Sigma 集群 ApiServer 优化实践

5a7ac7639ced6b3cc32155b646420d7e.png

蚂蚁大规模 Sigma 集群 Etcd 拆分实践

439c4212c0fc3c8fb169c7c4a0688699.png

文章知识点与官方知识档案匹配,可进一步学习相关知识Java技能树首页概览92222 人正在系统学习中

来源:SOFAStack

声明:本站部分文章及图片转载于互联网,内容版权归原作者所有,如本站任何资料有侵权请您尽早请联系jinwei@zod.com.cn进行处理,非常感谢!

上一篇 2022年1月7日
下一篇 2022年1月7日

相关推荐