-
Notifications
You must be signed in to change notification settings - Fork 5
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
8785e73
commit 665db53
Showing
4 changed files
with
159 additions
and
0 deletions.
There are no files selected for viewing
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
158 changes: 158 additions & 0 deletions
158
...ng-apimachinery/5.6-li-jie-k8s-zhong-de-clientside-apply-he-serverside-apply.md
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,158 @@ | ||
# 5.6 理解 K8s 中的 Client-Side Apply 和 Server-Side Apply | ||
|
||
### 前言 | ||
|
||
如果你经常与`kubectl`打交道,那相信你一定见过 `kubectl.kubernetes.io/last-applied-configuration` annotation,以及那神烦的`managedFields`,像这样: | ||
|
||
```bash | ||
$ kubectl get pods hello -oyaml apiVersion: v1 kind: Pod metadata: annotations: kubectl.kubernetes.io/last-applied-configuration: | {"apiVersion":"v1","kind":"Pod","metadata":{"annotations":{},"creationTimestamp":null,"labels":{"run":"hello"},"name":"hello","namespace":"default"},"spec":{"containers":[{"image":"nginx","name":"hello","resources":{}}],"dnsPolicy":"ClusterFirst","restartPolicy":"Always"},"status":{}} creationTimestamp: "2022-05-28T07:28:51Z" labels: run: hello managedFields: - apiVersion: v1 fieldsType: FieldsV1 fieldsV1: f:metadata: f:annotations: .: {} f:kubectl.kubernetes.io/last-applied-configuration: {} f:labels: .: {} f:run: {} .... manager: kubectl operation: Update time: "2022-05-28T07:28:51Z" .... | ||
``` | ||
|
||
由这两个字段,引出本文的两位主角,Client-Side Apply(以下简称**CSA**)和Server-Side Apply(以下简称**SSA**) | ||
|
||
* `kubectl.kubernetes.io/last-applied-configuration`是使用`kubectl apply`进行Client-Side Apply时,由`kubectl`自行填充的。 | ||
* `managedFields` 则是由`kubectl apply`的增强功能—— Server-Side Apply 的引入而添加。 | ||
|
||
本文将介绍以下内容: | ||
|
||
* `last-applied-configuration`和`managedFields`的作用。 | ||
* Client-Side Apply 和 Server-Side Apply的基本工作方式。 | ||
* Server-Side Apply的优点。 | ||
|
||
在开始之前,有必要澄清一下`kubectl apply`的预期工作方式。`kubectl apply`是一种声明示的K8S对象管理方式,是我们最常用的应用部署,升级方式之一。 | ||
|
||
需要特别指出的是,`kubectl apply`声明的仅仅是它关心的字段的状态,而不是整个对象的真实状态。apply表达的意思是:“我”管理的字段应该和我apply的配置文件一致(但我不关心其他字段)。 | ||
|
||
什么是“我”管理的字段,什么又是其他的字段呢?举个例子,当我们希望使用HPA管理应用副本数时,[Kubernetes推荐的做法](https://link.juejin.cn/?target=https%3A%2F%2Fkubernetes.io%2Fdocs%2Ftasks%2Frun-application%2Fhorizontal-pod-autoscale%2F%23migrating-deployments-and-statefulsets-to-horizontal-autoscaling)是在apply的配置文中不指定具体`replicas`副本数。首次部署时,K8S会将`replicas`值设置为默认1,随后由HPA控制器扩容到合适的副本数。 | ||
|
||
```yaml | ||
apiVersion: apps/v1 kind: Deployment metadata: creationTimestamp: null labels: app: nginx name: nginx spec: # replicas: 1 不要设置replicas selector: matchLabels: app: nginx strategy: {} template: metadata: creationTimestamp: null labels: app: nginx spec: containers: - image: nginx:latest name: nginx resources: {} | ||
``` | ||
当升级应用时(修改镜像版本),修改配置文件中的`image`字段,再次执行`kubectl apply`。此时`kubectl apply`只会影响镜像版本(因为他是“我”管理的字段),而不会影响HPA控制器设置的副本数。在这个例子中,`replicas`字段不是`kubectl apply`管理的字段,因此更新镜像时不会被删除,避免了每次应用升级时,副本数都会被重置。 | ||
|
||
在上述例子中,为了能识别出`replicas`不是`kubectl`管理的字段,`kubectl`需要一个标识,用来追踪对象中哪些字段是由`kubectl apply`管理的,而这个标识就是`last-applied-configuration`。 该annotation是在`kubectl apply`时,由`kubectl`客户端自行填充——每次执行`kubectl apply`时(未启用**SSA**),`kubectl`会将本次`apply`的配置文件全量的记录在`last-applied-configuration`annotation中,用于追踪哪些字段由`kubectl apply`管理。 | ||
|
||
**CSA**的工作工作机制大致如下:当apply一个对象,如果该对象不存在,则创建它(同时写入`last-applied-configuration`)。如果对象已经存在,则`kubectl`需要根据以下三个状态: | ||
|
||
* 当前配置文件所表示的对象在集群中的真实状态。(修改对象前先Get一次) | ||
* 当前apply的配置。 | ||
* 以及上次apply的配置。 (在`last-applied-configuration`里) | ||
|
||
计算出patch报文,通过patch方式进行更新(而不是将配置文件全量的发送到服务端)。 patch报文的计算方法如下: | ||
|
||
1. 计算需要被删除的字段。如果字段存在在`last-applied-configuration`中,但配置文件中没有,将删除它们。 | ||
2. 计算需要修改或添加的字段。如果配置文件中的字段与真实状态不一致,则添加或修改它们。 | ||
3. 特别的,对于那些`last-applied-configuration`中不存在的字段,不要修改它们(例如上述示例中的`replicas`字段) | ||
|
||
> 详细的patch计算示例可参考[K8S文档中给出的详细示例](https://link.juejin.cn/?target=https%3A%2F%2Fkubernetes.io%2Fzh-cn%2Fdocs%2Ftasks%2Fmanage-kubernetes-objects%2Fdeclarative-config%2F%23apply-%25E6%2593%258D%25E4%25BD%259C%25E6%2598%25AF%25E5%25A6%2582%25E4%25BD%2595%25E8%25AE%25A1%25E7%25AE%2597%25E9%2585%258D%25E7%25BD%25AE%25E5%25B7%25AE%25E5%25BC%2582%25E5%25B9%25B6%25E5%2590%2588%25E5%25B9%25B6%25E5%258F%2598%25E6%259B%25B4%25E7%259A%2584)。 | ||
|
||
由此可见,`last-applied-configuration`体现的是一种ownership的关系,表示哪些字段是由`kubectl`管理,它是`kubectl apply`时,计算patch报文的依据。 | ||
|
||
### `kubectl apply`升级版——Server-Side Apply | ||
|
||
**SSA**是另一种声明式的对象管理方式,和**CSA**的作用是基本一致的。**SSA**始于从1.14开始发布alpha版本,到1.16beta,到1.18beta2,终于在1.22升级为GA。 | ||
|
||
> Server-Side Apply 协助用户、控制器通过声明式配置的方式管理他们的资源。 客户端可以发送完整描述的目标(A fully specified intent), 声明式地创建和修改对象。 | ||
> | ||
> [kubernetes.io/zh-cn/docs/…](https://link.juejin.cn/?target=https%3A%2F%2Fkubernetes.io%2Fzh-cn%2Fdocs%2Freference%2Fusing-api%2Fserver-side-apply%2F) | ||
|
||
顾名思义,**SSA**将对象合并的逻辑转移到了服务端(APIServer),客户端只需提交完整的配置文件,剩下的工作交给服务端处理。 在`kubectl`中使用**SSA**,只需在`kubectl apply`时加上`--server-side`参数即可,例如这样: | ||
|
||
```bash | ||
$ kubectl apply --server-side=true -f - <<EOF apiVersion: v1 kind: ConfigMap metadata: name: test-server-side-apply data: a: "a" b: "b" EOF | ||
``` | ||
|
||
部署成功后,查看对象会发现该对象中不再存在`last-applied-configuration`。 | ||
|
||
```bash | ||
$ kubectl get cm test-server-side-apply -oyaml apiVersion: v1 data: a: a b: b kind: ConfigMap metadata: creationTimestamp: "2022-12-04T07:59:24Z" managedFields: - apiVersion: v1 fieldsType: FieldsV1 fieldsV1: f:data: f:a: {} f:b: {} manager: kubectl operation: Apply time: "2022-12-04T07:59:24Z" name: test-server-side-apply namespace: default resourceVersion: "1304750" uid: d265df3d-b9e9-4d0f-91c2-e654f850d25a # 没有 last-applied-configuration annotation啦 | ||
``` | ||
|
||
> TIPS: 如果你没能看到`managedFields`字段,可以加上 --show-managed-fields 参数: kubectl get cm test-server-side-apply -oyaml --show-managed-fields | ||
> | ||
> `managedFields`的出现导致`kubectl get xxx -oyaml | json`的输出变得非常冗长,难以阅读。 这个问题在v1.20版本中得到优化,使用v1.20+版本的kubectl 将默认不显示`managedFields` | ||
|
||
失去`last-applied-configuration`后,表达ownership的任务就落入了新引入的字段管理机制(field management)手中。根据以上输出的yaml的`metadata.managedFields`字段,我们不难得出它想表达的含义:该`configmap`中 `data.a`和 `data.b`字段都是由`kubectl`来管理的。 | ||
|
||
> “[字段管理(field management)](https://link.juejin.cn/?target=https%3A%2F%2Fkubernetes.io%2Fzh-cn%2Fdocs%2Freference%2Fusing-api%2Fserver-side-apply%2F%23field-management)”机制追踪对象字段的变化。 当一个字段值改变时,其所有权从当前管理器(manager)转移到施加变更的管理器。 当尝试将新配置应用到一个对象时,如果字段有不同的值,且由其他管理器管理, 将会引发[冲突](https://link.juejin.cn/?target=https%3A%2F%2Fkubernetes.io%2Fzh-cn%2Fdocs%2Freference%2Fusing-api%2Fserver-side-apply%2F%23conflicts)。 冲突引发警告信号:此操作可能抹掉其他协作者的修改。 冲突可以被刻意忽略,这种情况下,值将会被改写,所有权也会发生转移。 [kubernetes.io/zh-cn/docs/…](https://link.juejin.cn/?target=https%3A%2F%2Fkubernetes.io%2Fzh-cn%2Fdocs%2Freference%2Fusing-api%2Fserver-side-apply%2F) | ||
|
||
### managedFields冲突机制 | ||
|
||
**SSA**中使用了字段管理机制来追踪对象的变化,当apply改变一个字段时,而恰巧该字段被其他用户声明了ownership,此时会发生冲突。 这可以防止一个管理者不小心覆盖掉其他用户设置的值。 举个例子: 如果修改我们刚刚通过**SSA**创建的`test-server-side-apply`configmap,并且手动设置管理者为`test`(通过--field-manager字段),此时`kubectl`会拒绝我们的提交,提示冲突: | ||
|
||
```bash | ||
$ kubectl apply --server-side=true --field-manager="test" -f - <<EOF apiVersion: v1 kind: ConfigMap metadata: name: test-server-side-apply data: a: "a" # 把b,改成c了。 b: "c" EOF error: Apply failed with 1 conflict: conflict with "kubectl": .data.b Please review the fields above--they currently have other managers. Here are the ways you can resolve this warning: * If you intend to manage all of these fields, please re-run the apply command with the `--force-conflicts` flag. * If you do not intend to manage all of the fields, please edit your manifest to remove references to the fields that should keep their current managers. * You may co-own fields by updating your manifest to match the existing value; in this case, you'll become the manager if the other manager(s) stop managing the field (remove it from their configuration). See https://kubernetes.io/docs/reference/using-api/server-side-apply/#conflicts | ||
``` | ||
|
||
从`kubectl`返回的提示,我们可以得知当冲突发生的时我们有三种选择: | ||
|
||
* 覆盖前值,成为当前字段的唯一管理者——通过增加`--force-conflicts` flag | ||
* 不覆盖前值,放弃管理权——在本次配置中,把修改的字段删掉(本例中是`data.b`) | ||
* 不覆盖前值,成为共享管理者——把冲突值改成和服务器对象一致 | ||
|
||
> 参考文档: [kubernetes.io/zh-cn/docs/…](https://link.juejin.cn/?target=https%3A%2F%2Fkubernetes.io%2Fzh-cn%2Fdocs%2Freference%2Fusing-api%2Fserver-side-apply%2F%23conflicts) | ||
### Server-Side Apply的合并策略 | ||
|
||
在介绍**SSA**的合并策略前,我们先了解一下**CSA**的合并策略。**CSA**的合并规则是基于Kubernetes的`strategic merge patch`方式,不同的字段类型分别有各自不同的合并策略,规则比较复杂。我们光从[文档描述](https://link.juejin.cn/?target=https%3A%2F%2Fkubernetes.io%2Fzh-cn%2Fdocs%2Ftasks%2Fmanage-kubernetes-objects%2Fdeclarative-config%2F%23%25E4%25B8%258D%25E5%2590%258C%25E7%25B1%25BB%25E5%259E%258B%25E5%25AD%2597%25E6%25AE%25B5%25E7%259A%2584%25E5%2590%2588%25E5%25B9%25B6%25E6%2596%25B9%25E5%25BC%258F) 就能感受到该过程的复杂程度: ![image.png](https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/41510d85bded4e2580ec6ef951675293\~tplv-k3u1fbpfcp-zoom-in-crop-mark:1512:0:0:0.awebp?) | ||
|
||
这也导致了**CSA**容易产生[更多的Bug](https://link.juejin.cn/?target=https%3A%2F%2Fgithub.com%2Fkubernetes%2Fkubernetes%2Fissues%2F35234)。 | ||
|
||
**SSA**针对这个问题做了优化,相较于**CSA**,**SSA**定义了更加规范和准确的合并规则。 这里抄录[文档](https://link.juejin.cn/?target=https%3A%2F%2Fkubernetes.io%2Fdocs%2Freference%2Fusing-api%2Fserver-side-apply%2F%23merge-strategy)中的一段表格加以说明: | ||
|
||
| Golang 标记 | OpenAPI extension | 可接受的值 | 描述 | | ||
| ------------- | -------------------------- | -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | ||
| //+listType | x-kubernetes-list-type | atomic/set/map | 适用于 list。set 适用于仅包含标量元素的列表。这些元素必须是不重复的。map 仅适用于包含嵌套类型的列表。列表中的键(参见 listMapKey)不可以重复。atomic 适用于任何类型的列表。如果配置为 atomic,则合并时整个列表会被替换掉。任何时候,只有一个管理器负责管理指定列表。如果配置为 set 或 map,不同的管理器也可以分开管理条目。 | | ||
| //+listMapKey | x-kubernetes-list-map-keys | 字段名称的列表,例如,\["port", "protocol"] | 仅当 +listType=map 时适用。取值为字段名称的列表,这些字段值的组合能够唯一标识列表中的条目。尽管可以存在多个键,listMapKey 是单数的,这是因为键名需要在 Go 类型中各自独立指定。键字段必须是标量。 | | ||
| //+mapType | x-kubernetes-map-type | atomic/granular | 适用于 map。 atomic 指 map 只能被单个的管理器整个的替换。 granular 指 map 支持多个管理器各自更新自己的字段。 | | ||
| //+structType | x-kubernetes-map-type | atomic/granular | 适用于 structs;否则就像 //+mapType 有相同的用法和 openapi 注释. | | ||
|
||
表格中的“Golang标记“在代码中对应的API结构体中定义,举`Service`为例,在`ServiceSpec`的定义中`Ports`字段的注释中有如下标记: ![image.png](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/f5afc6e0962a45f399080f3bcec2b2a5\~tplv-k3u1fbpfcp-zoom-in-crop-mark:1512:0:0:0.awebp?) | ||
|
||
这表明`service.spec.ports`这个数组由`ports.port`和`ports.protocol`的组合值来确定唯一性。例如我们通过**SSA** apply这样一个`service`: | ||
|
||
```yaml | ||
kubectl apply --server-side=true -f - <<EOF apiVersion: v1 kind: Service metadata: name: my-cs spec: ports: - name: 5678-8080 port: 5678 protocol: TCP targetPort: 8080 type: ClusterIP EOF | ||
``` | ||
这表示“5768”+“TCP”组成了唯一标识,当我们继续使用**SSA** apply对这个`service`进行修改时,如果在`ports`中有相同的`port` + `protocol`组合,那会被认定为是同一条记录。 | ||
|
||
这意味着如果另一个管理者尝试apply具有相同`port` + `protocol`组合的`ports`,会抛出冲突: | ||
|
||
```yaml | ||
$ kubectl apply --server-side=true --field-manager="test" -f - <<EOF apiVersion: v1 kind: Service metadata: name: my-cs spec: ports: - name: 5679-9999 # 这里的port和protocol还是5679和TCP的组合 port: 5679 protocol: TCP targetPort: 9999 type: ClusterIP EOF error: Apply failed with 2 conflicts: conflicts with "kubectl": - .spec.ports[port=5679,protocol="TCP"].targetPort - .spec.ports[port=5679,protocol="TCP"].targetPort ..... | ||
``` | ||
|
||
如果该管理者修改了`port`或`protocol`再次apply,`ports`字段中会出现两条记录,分属不同的管理者: | ||
|
||
```yaml | ||
$ kubectl get svc my-cs -oyaml apiVersion: v1 kind: Service metadata: creationTimestamp: "2022-12-04T14:32:24Z" managedFields: - apiVersion: v1 fieldsType: FieldsV1 fieldsV1: f:spec: f:ports: k:{"port":5679,"protocol":"TCP"}: .... manager: kubectl <-第一次apply operation: Apply time: "2022-12-04T14:32:24Z" - apiVersion: v1 fieldsType: FieldsV1 fieldsV1: f:spec: f:ports: k:{"port":5679,"protocol":"UDP"}: .... f:type: {} manager: test <-第二次apply operation: Apply time: "2022-12-04T14:35:11Z" name: my-cs namespace: default resourceVersion: "1340102" uid: 6f7e23ab-165f-4498-8354-d3b83924faba spec: clusterIP: 10.96.155.168 clusterIPs: - 10.96.155.168 ipFamilies: - IPv4 ipFamilyPolicy: SingleStack ports: # 有两条记录 - name: 5679-8080 port: 5679 protocol: TCP targetPort: 8080 - name: 5679-9999 port: 5679 protocol: UDP targetPort: 9999 sessionAffinity: None type: ClusterIP status: loadBalancer: {} | ||
``` | ||
|
||
显然,这种合并策略更好的解决了多管理者之间的协作问题。 | ||
|
||
### Server-Side Apply的优点 | ||
|
||
#### 简化客户端逻辑 | ||
|
||
**CSA**是一个很重的客户端逻辑,里面有复杂的对象合并操作,这意味着apply这项操作和`kubectl`是深度绑定的,使用其他客户端或者在控制器(Controller)中难以使用apply方式来配置对象。 而**SSA**将这些合并的逻辑转移到了服务端,提供单一的API,客户端实现方式得以简化。这让apply的能力得以整合到`client-go`中,让应用可以[通过client-go](https://link.juejin.cn/?target=https%3A%2F%2Fkubernetes.io%2Fblog%2F2021%2F08%2F06%2Fserver-side-apply-ga%2F%23server-side-apply-support-in-client-go)来使用apply的能力。 | ||
|
||
#### 更细粒度的字段所有权管理,减少错误覆盖配置的可能性 | ||
|
||
相比于`last-applied-configuration`,**SSA**使用`managedFields`来管理每个字段的ownership,这是一种更细粒度的字段管理方式。这使得多个管理者之间能更好的协作,且其自带冲突检测,能很大程度避免错误覆盖配置的发生。 | ||
|
||
#### 更好的dry-run效果 | ||
|
||
当使用**SSA**时,`dry-run`的逻辑也放在服务端执行。相比**CSA**,服务端`dry-run`可以真实的经过validating/mutating admission webhooks的校验,从而获取最准确的返回结果。这是**CSA**无法实现的。 | ||
|
||
### 总结 | ||
|
||
简而言之,**CSA**和**SSA**是两种不同实现的声明示管理Kubernetes对象的方式。**SSA**的出现是为了解决了**CSA**中存在的一些挑战与问题,如apply逻辑和`kubectl`深度绑定、`strategic merge patch`复杂多bug等等。**SSA**发展至今已是Kubernetes中的一个关键特性,相信其[最终的目标](https://link.juejin.cn/?target=https%3A%2F%2Fgithub.com%2Fkubernetes%2Fenhancements%2Fpull%2F3518%23discussion\_r984102619)将会是完全取代**CSA**,成为Kubernetes中唯一的apply方式。 | ||
|
||
### 参考 | ||
|
||
1. [Break Down Kubernetes Server-Side Apply](https://link.juejin.cn/?target=https%3A%2F%2Fmedium.com%2Fswlh%2Fbreak-down-kubernetes-server-side-apply-5d59f6a14e26) | ||
2. [Kubernetes 1.22: Server Side Apply moves to GA](https://link.juejin.cn/?target=https%3A%2F%2Fkubernetes.io%2Fblog%2F2021%2F08%2F06%2Fserver-side-apply-ga%2F%23server-side-apply-support-in-client-go) | ||
3. [Server Side Apply Is Great And You Should Be Using It](https://link.juejin.cn/?target=https%3A%2F%2Fkubernetes.io%2Fblog%2F2022%2F10%2F20%2Fadvanced-server-side-apply%2F) | ||
4. [github.com/kubernetes-…](https://link.juejin.cn/?target=https%3A%2F%2Fgithub.com%2Fkubernetes-sigs%2Fstructured-merge-diff) |