暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

理解 K8s 中的 Client-Side Apply 和 Server-Side Apply

k8s技术圈 2022-12-10
1116

如果你经常与 kubectl
打交道,那相信你一定见过 kubectl.kubernetes.io/last-applied-configuration
annotation,以及那神烦的 managedFields
,像这样:

$ 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
最初始的样子——Client-Side Apply

在开始之前,有必要澄清一下 kubectl apply
的预期工作方式。kubectl apply
是一种声明示的 K8S 对象管理方式,是我们最常用的应用部署,升级方式之一。

需要特别指出的是,kubectl apply
声明的仅仅是它关心的字段的状态,而不是整个对象的真实状态。apply 表达的意思是:“我”管理的字段应该和我 apply 的配置文件一致(但我不关心其他字段)。

什么是“我”管理的字段,什么又是其他的字段呢?举个例子,当我们希望使用 HPA 管理应用副本数时, Kubernetes 推荐的做法 是在apply的配置文中不指定具体replicas
副本数。首次部署时,K8S会将replicas
值设置为默认1,随后由HPA控制器扩容到合适的副本数。

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 文档中给出的详细示例 。

由此可见,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/…

顾名思义,SSA 将对象合并的逻辑转移到了服务端(APIServer),客户端只需提交完整的配置文件,剩下的工作交给服务端处理。在 kubectl
中使用
SSA
,只需在 kubectl apply
时加上 --server-side
参数即可,例如这样:

$ 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

$ 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) ”机制追踪对象字段的变化。当一个字段值改变时,其所有权从当前管理器(manager)转移到施加变更的管理器。当尝试将新配置应用到一个对象时,如果字段有不同的值,且由其他管理器管理, 将会引发 冲突 。冲突引发警告信号:此操作可能抹掉其他协作者的修改。冲突可以被刻意忽略,这种情况下,值将会被改写,所有权也会发生转移。kubernetes.io/zh-cn/docs/…

managedFields 冲突机制

**SSA 中使用了字段管理机制来追踪对象的变化,当 apply 改变一个字段时,而恰巧该字段被其他用户声明了 ownership,此时会发生冲突。这可以防止一个管理者不小心覆盖掉其他用户设置的值。举个例子:如果修改我们刚刚通过SSA **创建的 test-server-side-apply
configmap,并且手动设置管理者为 test
(通过--field-manager 字段),此时 kubectl
会拒绝我们的提交,提示冲突:

$ 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/…

Server-Side Apply 的合并策略

在介绍 SSA 的合并策略前,我们先了解一下CSA 的合并策略CSA 的合并规则是基于 Kubernetes 的 strategic merge patch
方式,不同的字段类型分别有各自不同的合并策略,规则比较复杂。我们光从 文档描述 就能感受到该过程的复杂程度:

这也导致了 CSA 容易产生 更多的 Bug 。

SSA 针对这个问题做了优化,相较于CSA,SSA 定义了更加规范和准确的合并规则。这里抄录 文档 中的一段表格加以说明:

Golang 标记OpenAPI extension可接受的值描述
//+listTypex-kubernetes-list-typeatomic/set/map适用于 list。set 适用于仅包含标量元素的列表。这些元素必须是不重复的。map 仅适用于包含嵌套类型的列表。列表中的键(参见 listMapKey)不可以重复。atomic 适用于任何类型的列表。如果配置为 atomic,则合并时整个列表会被替换掉。任何时候,只有一个管理器负责管理指定列表。如果配置为 set 或 map,不同的管理器也可以分开管理条目。
//+listMapKeyx-kubernetes-list-map-keys字段名称的列表,例如,["port", "protocol"]仅当 +listType=map 时适用。取值为字段名称的列表,这些字段值的组合能够唯一标识列表中的条目。尽管可以存在多个键,listMapKey 是单数的,这是因为键名需要在 Go 类型中各自独立指定。键字段必须是标量。
//+mapTypex-kubernetes-map-typeatomic/granular适用于 map。atomic 指 map 只能被单个的管理器整个的替换。granular 指 map 支持多个管理器各自更新自己的字段。
//+structTypex-kubernetes-map-typeatomic/granular适用于 structs;否则就像 +mapType 有相同的用法和 openapi 注释.

表格中的“Golang 标记“在代码中对应的 API 结构体中定义,举 Service
为例,在 ServiceSpec
的定义中 Ports
字段的注释中有如下标记:

这表明 service.spec.ports
这个数组由 ports.port
ports.protocol
的组合值来确定唯一性。例如我们通过SSA apply 这样一个 service
:

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
,会抛出冲突:

$ 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
字段中会出现两条记录,分属不同的管理者:

$ 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 来使用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 中的一个关键特性,相信其 最终的目标 将会是完全取代CSA
,成为Kubernetes中唯一的apply方式。

参考

  1. https://medium.com/swlh/break-down-kubernetes-server-side-apply-5d59f6a14e26
  2. https://kubernetes.io/blog/2021/08/06/server-side-apply-ga/#server-side-apply-support-in-client-go
  3. https://kubernetes.io/blog/2022/10/20/advanced-server-side-apply/
  4. https://github.com/kubernetes-sigs/structured-merge-diff

原文链接:https://juejin.cn/post/7173328614644006942

 点击上方卡片关注K8s技术圈,掌握前沿云原生技术

文章转载自k8s技术圈,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论