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

本文主要是介绍【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 操作是如何计算配置差异并合并变更的?

这篇关于【k8s】kubernetes更新操作详解--kubectl apply(patch、replace)的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/994760

相关文章

MySQL数据库双机热备的配置方法详解

《MySQL数据库双机热备的配置方法详解》在企业级应用中,数据库的高可用性和数据的安全性是至关重要的,MySQL作为最流行的开源关系型数据库管理系统之一,提供了多种方式来实现高可用性,其中双机热备(M... 目录1. 环境准备1.1 安装mysql1.2 配置MySQL1.2.1 主服务器配置1.2.2 从

Linux kill正在执行的后台任务 kill进程组使用详解

《Linuxkill正在执行的后台任务kill进程组使用详解》文章介绍了两个脚本的功能和区别,以及执行这些脚本时遇到的进程管理问题,通过查看进程树、使用`kill`命令和`lsof`命令,分析了子... 目录零. 用到的命令一. 待执行的脚本二. 执行含子进程的脚本,并kill2.1 进程查看2.2 遇到的

MyBatis常用XML语法详解

《MyBatis常用XML语法详解》文章介绍了MyBatis常用XML语法,包括结果映射、查询语句、插入语句、更新语句、删除语句、动态SQL标签以及ehcache.xml文件的使用,感兴趣的朋友跟随小... 目录1、定义结果映射2、查询语句3、插入语句4、更新语句5、删除语句6、动态 SQL 标签7、ehc

详解SpringBoot+Ehcache使用示例

《详解SpringBoot+Ehcache使用示例》本文介绍了SpringBoot中配置Ehcache、自定义get/set方式,并实际使用缓存的过程,文中通过示例代码介绍的非常详细,对大家的学习或者... 目录摘要概念内存与磁盘持久化存储:配置灵活性:编码示例引入依赖:配置ehcache.XML文件:配置

从基础到高级详解Go语言中错误处理的实践指南

《从基础到高级详解Go语言中错误处理的实践指南》Go语言采用了一种独特而明确的错误处理哲学,与其他主流编程语言形成鲜明对比,本文将为大家详细介绍Go语言中错误处理详细方法,希望对大家有所帮助... 目录1 Go 错误处理哲学与核心机制1.1 错误接口设计1.2 错误与异常的区别2 错误创建与检查2.1 基础

k8s按需创建PV和使用PVC详解

《k8s按需创建PV和使用PVC详解》Kubernetes中,PV和PVC用于管理持久存储,StorageClass实现动态PV分配,PVC声明存储需求并绑定PV,通过kubectl验证状态,注意回收... 目录1.按需创建 PV(使用 StorageClass)创建 StorageClass2.创建 PV

Python版本信息获取方法详解与实战

《Python版本信息获取方法详解与实战》在Python开发中,获取Python版本号是调试、兼容性检查和版本控制的重要基础操作,本文详细介绍了如何使用sys和platform模块获取Python的主... 目录1. python版本号获取基础2. 使用sys模块获取版本信息2.1 sys模块概述2.1.1

一文详解Python如何开发游戏

《一文详解Python如何开发游戏》Python是一种非常流行的编程语言,也可以用来开发游戏模组,:本文主要介绍Python如何开发游戏的相关资料,文中通过代码介绍的非常详细,需要的朋友可以参考下... 目录一、python简介二、Python 开发 2D 游戏的优劣势优势缺点三、Python 开发 3D

Redis 基本数据类型和使用详解

《Redis基本数据类型和使用详解》String是Redis最基本的数据类型,一个键对应一个值,它的功能十分强大,可以存储字符串、整数、浮点数等多种数据格式,本文给大家介绍Redis基本数据类型和... 目录一、Redis 入门介绍二、Redis 的五大基本数据类型2.1 String 类型2.2 Hash

Java中的.close()举例详解

《Java中的.close()举例详解》.close()方法只适用于通过window.open()打开的弹出窗口,对于浏览器的主窗口,如果没有得到用户允许是不能关闭的,:本文主要介绍Java中的.... 目录当你遇到以下三种情况时,一定要记得使用 .close():用法作用举例如何判断代码中的 input