Skip to content

Commit

Permalink
GITBOOK-107: No subject
Browse files Browse the repository at this point in the history
  • Loading branch information
mouuii authored and gitbook-bot committed Aug 31, 2024
1 parent 8785e73 commit 665db53
Show file tree
Hide file tree
Showing 4 changed files with 159 additions and 0 deletions.
Binary file added .gitbook/assets/截屏2024-08-31 10.35.42.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added .gitbook/assets/截屏2024-08-31 10.37.02.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
* [5.1 k8s核心数据结构分析](k8s-yuan-ma-ren-cai-fu-hua-xun-lian-ying/di-wu-zhang-apimachinery/5.1-k8s-he-xin-shu-ju-jie-gou-fen-xi.md)
* [5.2 apimachinery](k8s-yuan-ma-ren-cai-fu-hua-xun-lian-ying/di-wu-zhang-apimachinery/5.2-shen-me-shi-apimachinery.md)
* [5.3 api-conventions](k8s-yuan-ma-ren-cai-fu-hua-xun-lian-ying/di-wu-zhang-apimachinery/5.2-api-conventions.md)
* [5.6 理解 K8s 中的 Client-Side Apply 和 Server-Side Apply](k8s-yuan-ma-ren-cai-fu-hua-xun-lian-ying/di-wu-zhang-apimachinery/5.6-li-jie-k8s-zhong-de-clientside-apply-he-serverside-apply.md)
* [认证授权](k8s-yuan-ma-ren-cai-fu-hua-xun-lian-ying/ren-zheng-shou-quan/README.md)
* [01-认证](k8s-yuan-ma-ren-cai-fu-hua-xun-lian-ying/ren-zheng-shou-quan/01-ren-zheng.md)

Expand Down
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)

0 comments on commit 665db53

Please sign in to comment.