网站内容更新机制( 从一个阿里云上客户对接OpenKruise的疑问开始(图))

优采云 发布时间: 2021-09-03 11:09

  网站内容更新机制(

从一个阿里云上客户对接OpenKruise的疑问开始(图))

  

  作者 | Wine Toast,阿里云技术专家

  背景

  OpenKruise 是阿里云开源的*敏*感*词*应用自动化管理引擎。它与 Kubernetes 原生的 Deployment / StatefulSet 等控制器在功能上进行了基准测试,但 OpenKruise 提供了更多增强功能,例如:优雅的就地升级、发布优先级/中断策略、多区域工作负载的抽象管理、统一的 sidecar 容器注入管理等.,都是通过阿里巴巴超*敏*感*词*应用场景打磨出来的核心能力。这些特性帮助我们应对更加多样化的部署环境和需求,为集群维护者和应用开发者带来更加灵活的部署和发布组合策略。

  目前,在阿里巴巴内部的云原生环境中,大部分应用使用OpenKruise的Pod部署和发布管理能力。但是,由于K8s原生部署等负载,阿里云上的很多行业公司和客户无法完全满足他们的需求。 , 也改用 OpenKruise 作为应用部署载体。

  今天的分享文章从一个关于连接阿里云OpenKruise的问题开始。这里还原一下这位同学的用法(以下YAML数据只是一个demo):

  准备一个Advanced StatefulSet的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接口进行更新。结果收到如下错误:

  metadata.resourceVersion: Invalid value: 0x0: must be specified for an update

  如果使用kubectl apply命令更新,会返回成功:

  statefulset.apps.kruise.io/sample configured

  问题是,为什么在同一个修改过的YAML文件中,调用api接口更新失败,而使用kubectl apply更新成功?这其实不是OpenKruise的特殊检查,而是由K8s自身的更新机制决定的。

  从我们的接触来看,大多数用户都有通过 kubectl 命令或 sdk 更新 K8s 资源的经验,但真正了解这些更新操作背后原理的人并不多。本文将重点介绍K8s的资源更新机制以及我们常用的一些更新方法是如何实现的。

  更新原则

  不知道大家有没有想过一个问题:对于一个像Deployment这样的K8s资源对象,当我们尝试修改里面的图片图像时,如果其他人也在修改这个Deployment时会发生什么?同一时间?

  当然,这里可以提出两个问题:

  如果双方修改同一个字段,比如图片字段,会发生什么?如果两方修改不同的字段,比如一个修改image,另一个修改replicas怎么办?

  实际上,“更新”一个 Kubernetes 资源对象只是通知 kube-apiserver 组件我们想要如何修改这个对象。 K8s针对这种需求定义了两种“通知”方式,分别是update和patch。在更新请求中,我们需要将整个修改过的对象提交给K8s;对于补丁请求,我们只需要将对象中某些字段的修改提交给K8s即可。

  那么回到后台问题,为什么用户修改后的YAML文件更新失败?这实际上受限于 K8s 对更新请求的版本控制机制。

  更新机制

  Kubernetes 中的所有资源对象都有一个全球唯一的版本号(metadata.resourceVersion)。每个资源对象从创建开始就有一个版本号,每次修改(无论是更新还是补丁修改),版本号都会改变。

  告诉我们这个版本号是K8s的内部机制。用户不应假设它是一个数字或通过比较两个版本号的大小来确定新旧资源对象。唯一可以做的就是比较版本号是否相等。判断对象是否是同一个版本(即是否发生了变化)。 resourceVersion 的一个重要用途是对更新请求进行版本控制。

  K8s 要求用户更新请求中提交的对象必须有一个resourceVersion,这意味着我们提交更新的数据必须首先来自K8s中已经存在的对象。因此,一个完整的更新操作流程是:

  首先从K8s获取一个已有的对象(可以选择直接从K8s查询;如果在客户端做list watch,建议从本地informer获取);然后,根据检索到的对象做一些修改,比如在Deployment中增加或者减少replica,或者修改image字段为新版本的image;最后,通过更新请求将修改后的对象提交给K8s;这时候kube-apiserver会验证用户更新请求提交对象中的resourceVersion必须与当前K8s中该对象的最新resourceVersion一致,才能接受这次更新。否则,K8s 会拒绝请求并告诉用户发生了版本冲突(Conflict)。

  

  上图展示了多个用户同时更新某个资源对象时的情况。如果出现Conflict冲突,用户A应该重试,再次获取最新版本的对象,修改后重新提交更新。

  所以,我们上面的两个问题也得到了解答:

  用户修改 YAML 后提交更新失败,因为 YAML 文件不收录 resourceVersion 字段。对于更新请求,需要取出当前K8s中的对象修改后提交;如果两个用户同时更新一个资源对象,无论操作是对象中的相同字段还是不同字段,都有版本控制机制来保证两个用户的更新请求不会被覆盖。补丁机制

  相比于update的版本控制,K8s的补丁机制要简单得多。

  当用户向某个资源对象提交补丁请求时,kube-apiserver不考虑版本问题,而是“无脑”接受用户的请求(只要请求发送的补丁内容合法),即补丁命中对象的同时更新版本号。

  但是补丁的复杂性在于K8s目前提供了4种补丁策略:json补丁、合并补丁、策略合并补丁、应用补丁(从K8s开始1.14支持服务端应用)。通过kubectl patch -h命令,我们也可以看到这个策略选项(默认是strategy):

  $ kubectl patch -h

# ...

--type='strategic': The type of patch being provided; one of [json merge strategic]

  限于篇幅,这里就不详细介绍每个策略了。让我们举一个简单的例子来看看它们之间的区别。如果对于现有的 Deployment 对象,假设模板中已经有一个名为 app 的容器:

  如果要给它添加一个nginx容器,如何打补丁?如果要修改app容器的镜像,如何打补丁更新? json 补丁 ([RFC 6902]())

  新容器:

  kubectl patch deployment/foo --type='json' -p \

'[{"op":"add","path":"/spec/template/spec/containers/1","value":{"name":"nginx","image":"nginx:alpine"}}]'

  修改现有容器镜像:

  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)

  merge patch不能单独更新list中的一个元素,所以无论是要给container添加一个新的container,还是修改已有container的image、env等字段,都必须使用整个container list来提交补丁:

  kubectl patch deployment/foo --type='merge' -p \

'{"spec":{"template":{"spec":{"containers":[{"name":"app","image":"app-image:v2"},{"name":"nginx","image":"nginx:alpline"}]}}}}'

  显然这种策略不适合我们更新链表的一些深度字段,更适合覆盖大片段的更新。

  但是对于labels/annotations,这些map类型的元素更新,merge patch可以单独指定key-value操作,比json patch方便,写起来也更直观:

  kubectl patch deployment/foo --type='merge' -p '{"metadata":{"labels":{"test-key":"foo"}}}'

  战略合并补丁

  这个补丁策略没有通用的 RFC 标准,而是 K8s 独有的,但是比前两个更强大。

  先从 K8s 源码开始,在 K8s 原生资源的数据结构定义中定义一些额外的策略注解。比如下面截取podSpec中容器列表的定义:

  // ...

// +patchMergeKey=name

// +patchStrategy=merge

Containers []Container `json:"containers" patchStrategy:"merge" patchMergeKey:"name" protobuf:"bytes,2,rep,name=containers"`

  可以看到有两个关键信息: patchStrategy: "merge" patchMergeKey: "name"。这意味着当使用策略合并补丁策略更新容器列表时,以下每个元素中的名称字段将被视为键。

  简而言之,我们不再需要在我们的补丁更新容器中指定下标序列号,而是指定要修改的名称,K8s将使用名称作为计算合并的键。例如,对于以下补丁操作:

  kubectl patch deployment/foo -p \

'{"spec":{"template":{"spec":{"containers":[{"name":"nginx","image":"nginx:mainline"}]}}}}'

  如果K8s发现当前容器中已经存在一个名为nginx的容器,则只会更新镜像;如果当前容器中没有 nginx 容器,K8s 会将该容器插入到容器列表中。

  另外需要说明的是,目前的战略策略只能用于Aggregated API模式下的原生K8s资源和自定义资源。不能使用 CRD 定义的资源对象。这个很容易理解,因为kube-apiserver无法知道CRD资源的结构和合并策略。如果使用 kubectl patch 命令更新 CR,则默认使用合并补丁策略进行操作。

  kubectl 包

  了解了K8s的基本更新机制后,我们又回到原来的问题。为什么用户修改了YAML文件后不能直接调用更新接口更新YAML文件,而可以通过kubectl apply命令更新?

  其实,为了给命令行用户提供良好的交互感,kubectl 设计了更复杂的内部执行逻辑。诸如应用和编辑之类的常见操作实际上并不对应于简单的更新请求。毕竟更新是版本控制的,如果出现更新冲突,对普通用户来说是不友好的。下面简单介绍一下kubectl的几个更新操作的逻辑。有兴趣的可以看看kubectl打包的源码。

  申请

  当使用默认参数执行apply时,会触发客户端apply。 kubectl逻辑如下:

  首先将用户提交的数据(YAML/JSON)解析成对象A;然后调用Get接口从K8s查询这个资源对象:

  这里只是一个粗略的流程梳理,真正的逻辑会比较复杂,K8s也支持服务端apply1.14,有兴趣的同学可以看源码实现。

  编辑

  kubectl edit 在逻辑上更简单。用户执行命令后,kubectl从K8s中找到当前资源对象,打开命令行编辑器(默认使用vi)为用户提供编辑界面。

  当用户完成修改,保存退出后,kubectl不会直接提交修改后的对象进行更新(避免冲突,如果用户修改过程中更新了资源对象),而是会取修改后的对象和原来的得到的对象计算diff,最后通过patch请求将diff内容提交给K8s。

  总结

  看完上面的介绍,大家应该对K8s的更新机制有了初步的了解。接下来,想一想。既然K8s提供了两种更新方式,那么在不同的场景下我们如何选择更新或者补丁来使用呢?我们的建议是:

  最后我们的客户根据get修改对象并提交更新,最终成功触发了Advanced StatefulSet的原位升级。此外,我们也欢迎并鼓励更多学生加入 OpenKruise 社区,共同打造面向*敏*感*词*场景的高性能应用交付解决方案。

  》聚焦微服务、Serverless、容器、Service Mesh等技术领域,关注云原生流行技术趋势,云原生*敏*感*词*落地实践,做最懂开发者的公众号关于云原生。”

0 个评论

要回复文章请先登录注册


官方客服QQ群

微信人工客服

QQ人工客服


线