【k8s】kubernetes更新操作详解--kubectl apply(patch、replace)

1. 背景

OpenKruise 是阿里云开源的大规模应用自动化管理引擎,在功能上对标了 Kubernetes 原生的 Deployment / StatefulSet 等控制器,但 OpenKruise 提供了更多的增强功能如:优雅原地升级、发布优先级/打散策略、多可用区workload抽象管理、统一 sidecar 容器注入管理等,都是经历了阿里巴巴超大规模应用场景打磨出的核心能力。这些 feature 帮助我们应对更加多样化的部署环境和需求、为集群维护者和应用开发者带来更加灵活的部署发布组合策略。

目前在阿里巴巴内部云原生环境中,绝大部分应用都统一使用 OpenKruise 的能力做 Pod 部署、发布管理,而不少业界公司和阿里云上客户由于 K8s 原生 Deployment 等负载不能完全满足需求,也转而采用 OpenKruise 作为应用部署载体。

今天的分享文章就从一个阿里云上客户对接 OpenKruise 的疑问开始。这里还原一下这位同学的用法(以下 YAML 数据仅为 demo):

  1. 准备一份 Advanced StatefulSet 的 YAML 文件,并提交创建。如:
apiVersion: apps.kruise.io/v1alpha1
kind: StatefulSet
metadata:name: sample
spec:# ...template:# ...spec:containers:- name: mainimage: nginx:alpineupdateStrategy:type: RollingUpdaterollingUpdate:podUpdatePolicy: InPlaceIfPossible
  1. 然后,修改了 YAML 中的 image 镜像版本,然后调用 K8s api 接口做更新。结果收到报错如下:
metadata.resourceVersion: Invalid value: 0x0: must be specified for an update
  1. 而如果使用 kubectl apply 命令做更新,则返回成功:
statefulset.apps.kruise.io/sample configured

问题在于,为什么同一份修改后的 YAML 文件,调用 api 接口更新是失败的,而用 kubectl apply 更新是成功的呢?这其实并不是 OpenKruise 有什么特殊校验,而是由 K8s 自身的更新机制所决定的。

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

2. 更新原理

不知道你有没有想过一个问题:对于一个 K8s 资源对象比如 Deployment,我们尝试在修改其中 image 镜像时,如果有其他人同时也在对这个 Deployment 做修改,会发生什么?

当然,这里还可以引申出两个问题:

  • 如果双方修改的是同一个字段,比如 image 字段,结果会怎样?
  • 如果双方修改的是不同字段,比如一个修改 image,另一个修改 replicas,又会怎么样?

其实,对一个 Kubernetes 资源对象做“更新”操作,简单来说就是通知 kube-apiserver 组件我们希望如何修改这个对象。而 K8s 为这类需求定义了两种“通知”方式,分别是 updatepatch。在 update 请求中,我们需要将整个修改后的对象提交给 K8s;而对于 patch 请求,我们只需要将对象中某些字段的修改提交给 K8s。

那么回到背景问题,为什么用户提交修改后的 YAML 文件做 update 会失败呢?这其实是被 K8s 对 update 请求的版本控制机制所限制的。

2.1 Update 机制

Kubernetes 中的所有资源对象,都有一个全局唯一的版本号(metadata.resourceVersion)。每个资源对象从创建开始就会有一个版本号,而后每次被修改(不管是 update 还是 patch 修改),版本号都会发生变化。

官方文档告诉我们,这个版本号是一个 K8s 的内部机制,用户不应该假设它是一个数字或者通过比较两个版本号大小来确定资源对象的新旧,唯一能做的就是通过比较版本号相等来确定对象是否是同一个版本(即是否发生了变化)。而 resourceVersion 一个重要的用处,就是来做 update 请求的版本控制。

K8s 要求用户 update 请求中提交的对象必须带有 resourceVersion,也就是说我们提交 update 的数据必须先来源于 K8s 中已经存在的对象。因此,一次完整的 update 操作流程是:

  1. 首先,从 K8s 中拿到一个已经存在的对象(可以选择直接从 K8s 中查询;如果在客户端做了 list watch,推荐从本地 informer 中获取);
  2. 然后,基于这个取出来的对象做一些修改,比如将 Deployment 中的 replicas 做增减,或是将 image 字段修改为一个新版本的镜像;
  3. 最后,将修改后的对象通过 update 请求提交给 K8s;
  4. 此时,kube-apiserver 会校验用户 update 请求提交对象中的 resourceVersion 一定要和当前 K8s 中这个对象最新的 resourceVersion 一致,才能接受本次 update。否则,K8s 会拒绝请求,并告诉用户发生了版本冲突(Conflict)。

在这里插入图片描述
上图展示了多个用户同时 update 某一个资源对象时会发生的事情。而如果如果发生了 Conflict 冲突,对于 User A 而言应该做的就是做一次重试,再次获取到最新版本的对象,修改后重新提交 update。

因此,我们上面的两个问题也都得到了解答:

  • 用户修改 YAML 后提交 update 失败,是因为 YAML 文件中没有包含 resourceVersion 字段。对于 update 请求而言,应该取出当前 K8s 中的对象做修改后提交;
  • 如果两个用户同时对一个资源对象做 update,不管操作的是对象中同一个字段还是不同字段,都存在版本控制的机制确保两个用户的 update 请求不会发生覆盖。
    回到顶部

2.2 Patch 机制

相比于 update 的版本控制,K8s 的 patch 机制则显得更加简单。

当用户对某个资源对象提交一个 patch 请求时,kube-apiserver 不会考虑版本问题,而是“无脑”地接受用户的请求(只要请求发送的 patch 内容合法),也就是将 patch 打到对象上、同时更新版本号。

不过,patch 的复杂点在于,目前 K8s 提供了 4 种 patch 策略:json patch、merge patch、strategic merge patch、apply patch(从 K8s 1.14 支持 server-side apply 开始)。通过 kubectl patch -h 命令我们也可以看到这个策略选项(默认采用 strategic):

$ kubectl patch -h
# ...--type='strategic': The type of patch being provided; one of [json merge strategic]

篇幅限制这里暂不对每个策略做详细的介绍了,我们就以一个简单的例子来看一下它们的差异性。如果针对一个已有的 Deployment 对象,假设 template 中已经有了一个名为 app 的容器:

  • 如果要在其中新增一个 nginx 容器,如何 patch 更新?
  • 如果要修改 app 容器的镜像,如何 patch 更新?

2.2.1 json patch(RFC 6902)

新增容器:

 kubectl patch  deployment.apps/kubia --type='json' -p \'[{"op":"add","path":"/spec/template/spec/containers/1","value":{"name":"nodejs8081","image":"kubia8081","imagePullPolicy": "IfNotPresent"}}]'

修改已有容器 image:

kubectl patch deployment/foo --type='json' -p \'[{"op":"replace","path":"/spec/template/spec/containers/0/image","value":"app-image:v2"}]'

可以看到,在 json patch 中我们要指定操作类型,比如 add 新增还是 replace 替换,另外在修改 containers 列表时要通过元素序号来指定容器。

这样一来,如果我们 patch 之前这个对象已经被其他人修改了,那么我们的 patch 有可能产生非预期的后果。比如在执行 app 容器镜像更新时,我们指定的序号是 0,但此时 containers 列表中第一个位置被插入了另一个容器,则更新的镜像就被错误地插入到这个非预期的容器中。

2.2.2 merge patch(RFC 7386)

merge patch 无法单独更新一个列表中的某个元素,因此不管我们是要在 containers 里新增容器、还是修改已有容器的 image、env 等字段,都要用整个 containers 列表来提交 patch:
merge patch 无法单独更新一个列表中的某个元素,因此不管我们是要在 containers 里新增容器、还是修改已有容器的 image、env 等字段,都要用整个 containers 列表来提交 patch:

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"}}}'

2.2.3 strategic merge patch(默认)

默认策略,不需要单独指定type

这种 patch 策略并没有一个通用的 RFC 标准,而是 K8s 独有的,不过相比前两种而言却更为强大的。

我们先从 K8s 源码看起,在 K8s 原生资源的数据结构定义中额外定义了一些的策略注解。比如以下这个截取了 podSpec 中针对 containers 列表的定义,参考 Github:

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

可以看到其中有两个关键信息:patchStrategy:"merge" patchMergeKey:"name" 。这就代表了,containers 列表使用 strategic merge patch 策略更新时,会把下面每个元素中的 name 字段看作 key

简单来说,在我们 patch 更新 containers 不再需要指定下标序号了,而是指定 name 来修改,K8s 会把 name 作为 key 来计算 merge。比如针对以下的 patch 操作:

kubectl patch deployment/foo -p \'{"spec":{"template":{"spec":{"containers":[{"name":"nginx","image":"nginx:mainline"}]}}}}'

如果 K8s 发现当前 containers 中已经有名字为 nginx 的容器,则只会把 image 更新上去;而如果当前 containers 中没有 nginx 容器,K8s 会把这个容器插入 containers 列表。

此外还要说明的是,目前 strategic 策略只能用于原生 K8s 资源以及 Aggregated API 方式的自定义资源,对于 CRD 定义的资源对象,是无法使用的。这很好理解,因为 kube-apiserver 无法得知 CRD 资源的结构和 merge 策略。如果用 kubectl patch 命令更新一个 CR,则默认会采用 merge patch 的策略来操作。

3. kubectl 封装

了解完了 K8s 的基础更新机制,我们再次回到最初的问题上。为什么用户修改 YAML 文件后无法直接调用 update 接口更新,却可以通过 kubectl apply 命令更新呢?

其实 kubectl 为了给命令行用户提供良好的交互体感,设计了较为复杂的内部执行逻辑,诸如 apply、edit 这些常用操作其实背后并非对应一次简单的 update 请求。毕竟 update 是有版本控制的,如果发生了更新冲突对于普通用户并不友好。以下简略介绍下 kubectl 几种更新操作的逻辑,有兴趣可以看一下 kubectl 封装的源码。

3.1 apply

在使用默认参数执行 apply 时,触发的是 client-side apply。kubectl 逻辑如下:

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

  • 如果查询结果不存在,kubectl 将本次用户提交的数据记录到对象 A 的 annotation 中(key 为 kubectl.kubernetes.io/last-applied-configuration),最后将对象 A提交给 K8s 创建;

  • 如果查询到 K8s 中已有这个资源,假设为对象 B:1. kubectl 尝试从对象 B 的 annotation 中取出 kubectl.kubernetes.io/last-applied-configuration 的值(对应了上一次 apply 提交的内容);2. kubectl 根据前一次 apply 的内容和本次 apply 的内容计算出 diff(默认为 strategic merge patch 格式,如果非原生资源则采用 merge patch);3. 将 diff 中添加本次的 kubectl.kubernetes.io/last-applied-configuration annotation,最后用 patch 请求提交给 K8s 做更新。详情参见 云原生 | Kubernetes - apply 操作是如何计算配置差异并合并变更的?

这里只是一个大致的流程梳理,真实的逻辑会更复杂一些,而从 K8s 1.14 之后也支持了 server-side apply,有兴趣的同学可以看一下源码实现。

3.2 edit

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

当用户修改完成、保存退出时,kubectl 并非直接把修改后的对象提交 update(避免 Conflict,如果用户修改的过程中资源对象又被更新),而是会把修改后的对象和初始拿到的对象计算 diff,最后将 diff 内容用 patch 请求提交给 K8s。

4. 总结

所有的kubectl 命令,例如 kubectl apply、kubectl edit、kubectl replace、kubectl patch都是围绕底层的 updatepatch 进行封装的。

看了上述的介绍,大家应该对 K8s 更新机制有了一个初步的了解了。接下来想一想,既然 K8s 提供了两种更新方式,我们在不同的场景下怎么选择 updatepatch 来使用呢?这里我们的建议是:

  • 如果要更新的字段只有我们自己会修改(比如我们有一些自定义标签,并写了 operator 来管理),则使用 patch 是最简单的方式;
  • 如果要更新的字段可能会被其他方修改(比如我们修改的 replicas 字段,可能有一些其他组件比如 HPA 也会做修改),则建议使用 update 来更新,避免出现互相覆盖。

最终我们的客户改为基于 get 到的对象做修改后提交 update,终于成功触发了 Advanced StatefulSet 的原地升级。

参考

kubernetes更新操作详解–kubectl apply
云原生 | Kubernetes - apply 操作是如何计算配置差异并合并变更的?

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/706570.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

C#知识|上位机子窗体嵌入主窗体方法(实例)

哈喽,你好啊,我是雷工! 上位机开发中,经常会需要将子窗体嵌入到主窗体, 本节练习C#中在主窗体的某个容器中打开子窗体的方法。 01 需求说明 本节练习将【账号管理】子窗体在主窗体的panelMain容器中打开。 账号管理子窗体如下: 主窗体的panelMain容器位置如图: 02 实现…

AI试卷怎么解答问题?分享4款AI答题助手

AI试卷怎么解答问题?在数字化与智能化的双重推动下,AI试卷解答问题正逐步成为教育领域的新宠。这一技术不仅为学生们提供了全新的学习途径,更让他们在面对试卷难题时,能够迅速找到解题思路,减少时间浪费。AI的精准解答…

主流短视频评论采集python爬虫(含一二级评论内容)

声明 仅用于学习交流,不用于其他用途 正文 随着主流短视频评论采集更新需要登录,由于不懈的努力,攻破这一难点,不需要登录采集作品所有评论信息 话不多说上代码看效果: 输入作品id: 这样就拿到评论信息了&#xff…

标题:轻松掌握Python列表拼接:方法、示例与最佳实践

在Python编程中,列表(List)是一种非常常用的数据结构,用于存储一系列有序的元素。在日常编程中,我们经常需要将两个或多个列表拼接起来形成一个更长的列表。本文将详细介绍如何在Python中实现列表的拼接,包…

深入理解 Spring 循环依赖之三级缓存(附源码分析)

前言: 学过 Spring 的都知道 Spring 利用三级缓存解决了循环依赖问题,那你知道什么是循环依赖?什么又是三级缓存?本篇将从源码层面分析 Spring 是怎么去利用三级缓存帮我们解决循环依赖问题。 深入理解 Spring IOC 底层实现机制…

element 表格滚动条滑动,表格错位

解决办法(主要是根据滚动条的宽度决定的,可自行调整) 1、.el-table__header-wrapper { width: 99.3% !important; } 2、.el-table__header-wrapper { padding-right: 10px!important; }

Git—常用命令

官方 Git - Book (git-scm.com) 常用命令 命令作用git config --global user.name 用户名设置用户作者git config --global user.email 邮箱设置用户邮箱 git init初始化本地仓库git status查看本地仓库状态 git add 文件名 添加到暂存区git commit -m "提交日志…

【气象常用】时间序列的线性拟合

效果图: 主要步骤: 1. 数据准备:下载Hadley Centre observations datasets的HadSST数据 可参考【气象常用】时间序列图-CSDN博客 2. 数据处理:计算线性拟合 3. 图像绘制:绘制折线及拟合线,并添加文本 …

数据分析案例-印度美食数据可视化分析

🤵‍♂️ 个人主页:艾派森的个人主页 ✍🏻作者简介:Python学习者 🐋 希望大家多多支持,我们一起进步!😄 如果文章对你有帮助的话, 欢迎评论 💬点赞&#x1f4…

硕博电子 叉车电控 叉车控制器 叉车显示屏

硕博电子叉车电控系统以高效能、多功能和广泛的适应性为核心优势,它的设计与应用优化改变了传统的人工搬运模式,顺应了物流仓储行业对于节能、智能操控的迫切需求。该系统不仅大幅提升了叉车能源利用率,还改善了工作环境,减轻了工…

分割模型部件——将gt用边界在原始图像上展示,将pred用掩膜在原始图像上展示

目的 在论文可视化结果时,简介明了的展示我们自己模型的分割结果与gt的区别是十分重要的,因此这里记录一下自己的可视化方式。 踩坑: 因为大多数操作都是基于cv2进行的,因此你需要保持输入图像的h,w,c的顺…

Java入门——异常

异常的背景 初识异常 我们曾经的代码中已经接触了一些 "异常" 了. 例如: //除以 0 System.out.println(10 / 0); // 执行结果 Exception in thread "main" java.lang.ArithmeticException: / by zero //数组下标越界 int[] arr {1, 2, 3}; System.out.…