背景
调用put 更新
1 |
|
调用 K8s api 接口做更新。结果收到报错如下:
1 |
|
因为 YAML 文件中没有包含 resourceVersion 字段。对于 update 请求而言,应该取出当前 K8s 中的对象做修改后提交
正确应该
1 |
|
原理
对一个 Kubernetes 资源对象做更新操作,简单来说就是通知 kube-apiserver 组件我们希望如何修改这个对象。
而 K8s 为这类需求定义了两种“通知”方式,分别是 update 和 patch。
- 在 update 请求中,我们需要将整个修改后的对象提交给 K8s;K8s 对 update 请求的版本控制机制所限制的。
- 而对于 patch 请求,我们只需要将对象中某些字段的修改提交给 K8s。
Update 机制
Kubernetes 中的所有资源对象,都有一个全局唯一的版本号(metadata.resourceVersion)。每个资源对象从创建开始就会有一个版本号,而后每次被修改(不管是 update 还是 patch 修改),版本号都会发生变化。
唯一能做的就是通过比较版本号相等来确定对象是否是同一个版本(即是否发生了变化),resourceVersion 一个重要的用处,就是来做 update 请求的版本控制。
K8s 要求用户 update 请求中提交的对象必须带有 resourceVersion,也就是说我们提交 update 的数据必须先来源于 K8s 中已经存在的对象。因此,一次完整的 update 操作流程是:
- 首先,从 K8s 中拿到一个已经存在的对象(可以选择直接从 K8s 中查询;如果在客户端做了 list watch,推荐从本地 informer 中获取);
- 然后,基于这个取出来的对象做一些修改,比如将 Deployment 中的 replicas 做增减,或是将 image 字段修改为一个新版本的镜像;
- 最后,将修改后的对象通过 update 请求提交给 K8s;
- 此时,kube-apiserver 会校验用户 update 请求提交对象中的 resourceVersion 一定要和当前 K8s 中这个对象最新的 resourceVersion 一致,才能接受本次 update。否则,K8s 会拒绝请求,并告诉用户发生了版本冲突(Conflict)。
Patch 机制
当用户对某个资源对象提交一个 patch 请求时,kube-apiserver 不会考虑版本问题,而是直接接受用户的合法请求,也就是将 patch 打到对象上、同时更新版本号。
有不同的更新策略
1 |
|
Example
如果针对一个已有的 Deployment 对象,假设 template 中已经有了一个名为 app 的容器
-
如果要在其中新增一个 nginx 容器,如何 patch 更新?
-
如果要修改 app 容器的镜像,如何 patch 更新?
json patch
新增容器:
1 |
|
修改已有容器 image:
1 |
|
可以看到,在 json patch 中我们要指定操作类型,比如 add 新增还是 replace 替换,另外在修改 containers 列表时要通过元素序号来指定容器。
merge patch
无法单独更新一个列表中的某个元素,因此不管我们是要在 containers 里新增容器、还是修改已有容器的 image、env 等字段,都要用整个 containers 列表来提交 patch
1 |
|
显然,这个策略并不适合我们对一些列表深层的字段做更新,更适用于大片段的覆盖更新。
不过对于 labels/annotations 这些 map 类型的元素更新,merge patch 是可以单独指定 key-value 操作的,相比于 json patch 方便一些,写起来也更加直观:
1 |
|
strategic merge patch
在我们 patch 更新 containers 不再需要指定下标序号了,而是指定 name 来修改,K8s 会把 name 作为 key 来计算 merge。比如针对以下的 patch 操作
1 |
|
如果 K8s 发现当前 containers 中已经有名字为 nginx 的容器,则只会把 image 更新上去;而如果当前 containers 中没有 nginx 容器,K8s 会把这个容器插入 containers 列表。
目前 strategic 策略只能用于原生 K8s 资源以及 Aggregated API 方式的自定义资源,对于 CRD 定义的资源对象,是无法使用的。这很好理解,因为 kube-apiserver 无法得知 CRD 资源的结构和 merge 策略。如果用 kubectl patch 命令更新一个 CR,则默认会采用 merge patch 的策略来操作
kubectl的封装
https://github.com/kubernetes/kubectl
其实 kubectl 为了给命令行用户提供良好的交互体感,设计了较为复杂的内部执行逻辑,诸如 apply、edit 这些常用操作其实背后并非对应一次简单的 update 请求。
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:
- kubectl 尝试从对象 B 的 annotation 中取出 kubectl.kubernetes.io/last-applied-configuration 的值(对应了上一次 apply 提交的内容)
- kubectl 根据前一次 apply 的内容和本次 apply 的内容计算出 diff(默认为 strategic merge patch 格式,如果非原生资源则采用 merge patch)
- 将 diff 中添加本次的 kubectl.kubernetes.io/last-applied-configuration annotation,最后用 patch 请求提交给 K8s 做更新。
edit
当用户修改完成、保存退出时,kubectl 并非直接把修改后的对象提交 update(避免 Conflict,如果用户修改的过程中资源对象又被更新)
而是会把修改后的对象和初始拿到的对象计算 diff,最后将 diff 内容用 patch 请求提交给 K8s。