网站内容更新机制(从一个阿里云上客户对接OpenKruise的疑问开始,还原一下)
优采云 发布时间: 2021-09-26 09:21网站内容更新机制(从一个阿里云上客户对接OpenKruise的疑问开始,还原一下)
在这里你可以找到来自不同行业的第一手云资讯,还等什么,快来吧!
背景
OpenKruise 是阿里云开源的*敏*感*词*应用自动化管理引擎。它与Kubernetes原生的Deployment/StatefulSet等控制器进行了功能对标,但OpenKruise提供了更多的增强,例如:优雅的就地升级、发布优先级/开放分布式策略、多区域工作负载的抽象管理、统一的sidecar容器注入管理等.,都是通过阿里巴巴超*敏*感*词*应用场景打磨出来的核心能力。这些特性帮助我们应对更加多样化的部署环境和需求,为集群维护者和应用开发者带来更加灵活的部署和发布组合策略。
目前,在阿里巴巴内部的云原生环境中,大部分应用使用OpenKruise的Pod部署和发布管理能力。但是,由于K8s原生部署等负载,很多行业公司和阿里云客户无法完全满足他们的需求。相反,OpenKruise 被用作应用程序部署载体。
今天的分享文章从一个关于OpenKruise与阿里云客户对接的问题开始。这里还原一下这位同学的用法(以下YAML数据只是一个demo):
准备一个 Advanced StatefulSet 的 YAML 文件并提交创建。喜欢:
yaml apiVersion: apps.kruise.io/v1alpha1
kind: StatefulSet
metadata:
name: sample
spec:
# ...
template:
# ...
spec:
containers:
- name: main
image: nginx:alpine
updateStrategy:
type: RollingUpdate
rollingUpdate:
podUpdatePolicy: InPlaceIfPossible
然后,在YAML中修改镜像镜像版本,然后调用K8s api接口进行更新。结果,收到的错误如下:
shell metadata.resourceVersion:无效值:0x0:必须为更新指定
如果使用 kubectl apply 命令更新,则返回成功:
shell statefulset.apps.kruise.io/sample 配置
问题是,为什么在同一个修改后的YAML文件中,调用api接口更新失败,而使用kubectl apply更新成功?这其实并不是OpenKruise的特殊检查,而是由K8s自身的更新机制决定的。
从我们的接触来看,大多数用户都有通过 kubectl 命令或 sdk 更新 K8s 资源的经验,但真正了解这些更新操作背后原理的人并不多。本文将重点介绍K8s的资源更新机制以及我们常用的一些更新方法是如何实现的。
更新原则
不知道大家有没有想过一个问题:对于一个像Deployment这样的K8s资源对象,当我们尝试修改里面的图片图像时,如果其他人也在同时修改Deployment会怎样?
当然,这里可以推导出两个问题:
事实上,“更新”一个 Kubernetes 资源对象只是通知 kube-apiserver 组件我们想要如何修改这个对象。K8s针对这种需求定义了两种“通知”方式,分别是update和patch。在更新请求中,我们需要将整个修改过的对象提交给K8s;对于补丁请求,我们只需要将对象中某些字段的修改提交给K8s。
那么回到背景问题,为什么用户没有更新修改后的YAML文件呢?这实际上受限于 K8s 对更新请求的版本控制机制。
更新机制
Kubernetes 中的所有资源对象都有一个全局唯一的版本号(metadata.resourceVersion)。每个资源对象从创建之初就有一个版本号,每次修改(无论是更新还是补丁修改),版本号都会改变。
官方文档告诉我们,这个版本号是K8s的内部机制。用户不应假设它是一个数字或通过比较两个版本号的大小来确定新旧资源对象。唯一可以做的就是通过比较版本号来确定相同。对象是否是同一个版本(即是否发生了变化)。resourceVersion 的一个重要用途是对更新请求进行版本控制。
K8s要求用户更新请求中提交的对象必须有resourceVersion,这意味着我们提交更新的数据必须首先来自K8s中已经存在的对象。因此,一个完整的更新操作流程是:
上图显示了当多个用户同时更新某个资源对象时会发生什么。如果有Conflict冲突,User A应该做的是重试,重新获取最新版本的对象,修改后重新提交更新。
因此,我们上面的两个问题也得到了解答:
补丁机制
相比更新版本控制,K8s的补丁机制要简单得多。
当用户向资源对象提交补丁请求时,kube-apiserver 不考虑版本问题,而是“无脑”接受用户的请求(只要请求发送的补丁内容合法),即,补丁打到对象的同时更新版本号。
但是补丁的复杂性在于K8s目前提供了4种补丁策略:json补丁、合并补丁、策略合并补丁、应用补丁(从K8s开始1.14支持服务端应用)。我们也可以通过 kubectl patch -h 命令看到这个策略选项(默认使用的是strategic):
限于篇幅,这里就不详细介绍每种策略了,我们举个简单的例子来看看它们之间的区别。如果对于现有的 Deployment 对象,假设模板中已经有一个名为 app 的容器:
json 补丁 (RFC 6902)
新容器:
bash kubectl patch deployment/foo --type='json' -p \
'[{"op":"add","path":"/spec/template/spec/containers/1","value":{"name":"nginx","image":"nginx:alpine"}}]
修改现有容器:
bash kubectl patch deployment/foo --type='json' -p \
'[{"op":"replace","path":"/spec/template/spec/containers/0/image","value":"app-image:v2"}]
可以看到,在json补丁中,我们需要指定操作类型,比如添加或者替换。另外,在修改容器列表时,我们需要通过元素编号来指定容器。
这样,如果这个对象在我们打补丁之前就被其他人修改了,那么我们的补丁可能会产生意想不到的后果。例如,在执行app容器镜像更新时,我们指定的序号是0,但是此时另一个容器插入到容器列表的第一个位置,更新后的镜像错误地插入到了这个意外的容器中。
合并补丁 (RFC 7386)
合并补丁不能单独更新列表中的一个元素,所以无论我们是想给容器添加一个新的容器,还是修改现有容器的image、env等字段,都必须使用整个容器列表来提交补丁:
显然,这种策略不适合我们更新列表的一些深度字段,更适合大片段的覆盖更新。
但是对于labels/annotations,更新了这些map-type元素,merge patch可以单独指定key-value操作,比json patch方便,写起来也更直观:
战略合并补丁
这个补丁策略没有通用的 RFC 标准,而是 K8s 独有的,但是比前两个更强大。
我们先从 K8s 源码开始,在 K8s 原生资源的数据结构定义中定义一些额外的策略注解。比如下面截取podSpec中容器列表的定义,参考Github:
您可以看到有两个关键信息:
这意味着当使用策略合并补丁策略更新容器列表时,以下每个元素中的名称字段将被视为键。
简单的说,在我们的补丁更新容器中,我们不再需要指定下标序号,而是指定要修改的名称,K8s会以名称作为key来计算合并。例如,对于以下补丁操作:
如果K8s发现当前容器中已经存在一个名为nginx的容器,则只会更新镜像;如果当前容器中没有 nginx 容器,K8s 会将该容器插入到容器列表中。
另外需要注意的是,目前的战略策略只能用于Aggregated API模式下的原生K8s资源和自定义资源。不能使用 CRD 定义的资源对象。这个很容易理解,因为kube-apiserver无法知道CRD资源的结构和合并策略。如果使用 kubectl patch 命令更新 CR,则默认使用合并补丁策略进行操作。
kubectl 包
了解了K8s的基本更新机制后,我们又回到原来的问题。为什么用户修改YAML文件后不能直接调用更新接口进行更新,而可以通过kubectl apply命令进行更新?
事实上,kubectl 设计了更复杂的内部执行逻辑,以便为命令行用户提供良好的交互感受。诸如应用和编辑之类的常见操作实际上并不对应于简单的更新请求。毕竟更新是版本控制的,如果出现更新冲突,对普通用户来说是不友好的。下面简单介绍一下kubectl的几个更新操作的逻辑。有兴趣的可以看看kubectl打包的源码。
申请
当使用默认参数执行apply 时,会触发客户端apply。kubectl逻辑如下:
首先将用户提交的数据(YAML/JSON)解析成对象A;然后调用Get接口从K8s查询这个资源对象:
这里只是一个粗略的流程梳理,真正的逻辑会比较复杂,而且从K8s1.14开始也支持服务端apply,有兴趣的同学可以看源码实现。
编辑
kubectl edit 在逻辑上更简单。用户执行命令后,kubectl从K8s中找到当前资源对象,打开命令行编辑器(默认使用vi),为用户提供编辑界面。
当用户完成修改,保存退出后,kubectl不会直接提交修改后的对象进行更新(避免冲突,如果用户修改过程中更新了资源对象),而是会发送修改后的对象和最初获取的对象计算diff,最后通过patch请求将diff内容提交给K8s。
总结
看完上面的介绍,大家应该对K8s的更新机制有了初步的了解。接下来,想一想。既然K8s提供了两种更新方式,那么在不同的场景下我们如何选择更新或者补丁来使用呢?我们这里的建议是:
如果要更新的字段只是我们自己修改(比如我们有一些自定义标签,写操作符来管理),那么patch是最简单的方法;
如果要更新的字段可能被其他方修改(例如我们修改的replicas字段,其他一些组件如HPA也可能被修改),建议使用update来更新,避免相互覆盖。
最后我们的客户根据get得到的修改对象改为提交更新,最终成功触发了Advanced StatefulSet的原位升级。此外,我们也欢迎并鼓励更多学生加入OpenKruise社区,共同打造面向*敏*感*词*场景的高性能应用交付解决方案。
【云栖在线课堂】每天都有产品技术专家分享!
课程地址:
立即加入社区,与专家面对面,了解课程的最新动态!
【云栖在线课堂社区】