从 Kubernetes 1.18 开始,可以看到一个明显的变化就是资源的 YAML 在 metadata 部分多了很多信息:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
apiVersion: v1
kind: Namespace
metadata:
  creationTimestamp: "2021-02-18T03:03:40Z"
  managedFields:
  - apiVersion: v1
    fieldsType: FieldsV1
    fieldsV1:
      f:status:
        f:phase: {}
    manager: kube-apiserver
    operation: Update
    time: "2021-02-18T03:03:40Z"
  name: default
  resourceVersion: "199"
  uid: 2f4536b5-7302-4dc1-9620-052a567c917c
spec:
  finalizers:
  - kubernetes
status:
  phase: Active

比如这个 NS 的例子, 其中大部分都是 mangedFields部分.简单来讲,managedFields 字段是用来声明一个资源的各个字段的具体的管理者是谁.我们可以参考一个有点类似的案例, Node 的 conditions 字段:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
  conditions:
  - lastHeartbeatTime: "2021-02-18T03:05:25Z"
    lastTransitionTime: "2021-02-18T03:05:25Z"
    message: Flannel is running on this node
    reason: FlannelIsUp
    status: "False"
    type: NetworkUnavailable
  - lastHeartbeatTime: "2021-02-18T07:34:05Z"
    lastTransitionTime: "2021-02-18T03:03:36Z"
    message: kubelet has sufficient memory available
    reason: KubeletHasSufficientMemory
    status: "False"
    type: MemoryPressure

Contaions 是一个列表,不同的 Component 负责上报自己所观察到的信息,最终汇总到 Node 的 Status 下面.然后再合并汇总出一个整体的 Ready 状态. mangedFieds 也是有点类似的思路, 一个资源的不同部分是由不同组件/人负责维护的,显式的声明这种维护关系有助于各方协同地维护一个资源完整的状态.更直观的,我们可以看一个 Deployment 的例子. 首先,我们使用如下的 deployment 文件来创建

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-deployment
  labels:
    app: nginx
spec:
  replicas: 3
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx
        image: nginx:1.14.2

创建命令是:

1
2
# -n 后面是希望使用的 namespace, 非关键信息
kubectl apply -f d.yaml -n ssa

最终生成的 deployment 如下所示(部分信息):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
apiVersion: apps/v1
kind: Deployment
metadata:
  annotations:
    deployment.kubernetes.io/revision: "1"
    kubectl.kubernetes.io/last-applied-configuration: |
            {"apiVersion":"apps/v1","kind":"Deployment","metadata":{"annotations":{},"labels":{"app":"nginx"},"name":"nginx-deployment","namespace":"ssa"},"spec":{"replicas":3,"selector":{"matchLabels":{"app":"nginx"}},"template":{"metadata":{"labels":{"app":"nginx"}},"spec":{"containers":[{"image":"nginx:1.14.2","name":"nginx"}]}}}}                                   
  creationTimestamp: "2021-02-18T07:41:40Z"
  generation: 1
  labels:
    app: nginx
  managedFields:
  - apiVersion: apps/v1
    fieldsType: FieldsV1
    fieldsV1:
      f:metadata:
        f:annotations:
          .: {}
          f:kubectl.kubernetes.io/last-applied-configuration: {}
        f:labels:
          .: {}
          f:app: {}
      f:spec:
        f:progressDeadlineSeconds: {}
        f:replicas: {}
        f:revisionHistoryLimit: {}
        f:selector: {}
        f:strategy:
          f:rollingUpdate:
            .: {}
            f:maxSurge: {}
            f:maxUnavailable: {}
          f:type: {}
        f:template:
          f:metadata:
            f:labels:
              .: {}
              f:app: {}
          f:spec:
            f:containers:
              k:{"name":"nginx"}:
                .: {}
                f:image: {}
                f:imagePullPolicy: {}
                f:name: {}
                f:resources: {}
                f:terminationMessagePath: {}
                f:terminationMessagePolicy: {}
            f:dnsPolicy: {}
            f:restartPolicy: {}
            f:schedulerName: {}
            f:securityContext: {}
            f:terminationGracePeriodSeconds: {}
    manager: kubectl-client-side-apply
    operation: Update
    time: "2021-02-18T07:41:40Z"
  - apiVersion: apps/v1
    fieldsType: FieldsV1
    fieldsV1:
      f:metadata:
        f:annotations:
          f:deployment.kubernetes.io/revision: {}
      f:status:
        f:availableReplicas: {}
        f:conditions:
          .: {}
          k:{"type":"Available"}:
            .: {}
            f:lastTransitionTime: {}
            f:lastUpdateTime: {}
            f:message: {}
            f:reason: {}
            f:status: {}
            f:type: {}
          k:{"type":"Progressing"}:
            .: {}
            f:lastTransitionTime: {}
            f:lastUpdateTime: {}
            f:message: {}
            f:reason: {}
            f:status: {}
            f:type: {}
        f:observedGeneration: {}
        f:readyReplicas: {}
        f:replicas: {}
        f:updatedReplicas: {}
    manager: kube-controller-manager
    operation: Update
    time: "2021-02-18T07:41:42Z"
  name: nginx-deployment

其中 f 后面跟的是 field 的名字,其他部分都比较直观.可以看到 managedFieds 包含两个 item:

  • 第一个manager 是 kubectl-client-side-apply.因为这个 deploy 是我直接用 kubectl apply 创建的. 默认 kubectl apply 使用的是 Client-Side Apply 而不是 Server-Side Apply.
  • 第二个manager 是 kube-controller-manager, 比较直观.对比两个 manager 所管理的字段列表来看, kube-controller-manager管理的都是我们通常看到的由 系统 自动填充的信息, 比如 conditions, .status.repliacs等.而kubectl-client-side-apply 则是管理那些用户自己输入的信息.

如上所展示的 manager 机制即是 Server-Side Apply的核心, 下面将详述细节.

kubectl apply

首先我们需要大概了解 kubectl apply做了什么,分为如下几步:

  1. GET 查询目标 Object
  2. 如果 Object 不存在,则提交到API创建,并且将用户提供的 input 信息放到 新创建的 Object 的 annotations里(key 为 kubectl.kubernetes.io/last-applied-configuration)
  3. 如果 Object 已存在, 先读取到旧的 Object 的 kubectl.kubernetes.io/last-applied-configuration 的值, 然后与当前的做对比, 生成一个 diff (strategic merge patch), 用 PATCH 发给 API Server. 其中 diff 里面也会附带上最新的 kubectl.kubernetes.io/last-applied-configuration.

其中, strategic merge patch 需要单独提一下,它的主要特点就是对于 list 的 update 提供了 patchStrategy 以便对不同的字段做不同的处理.比如我们常见的 Pod 的 containers 列表:

1
2
3
type PodSpec struct {
  ...
  Containers []Container `json:"containers" patchStrategy:"merge" patchMergeKey:"name" ...`

举个例子.最初始的 Pod Template 如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
apiVersion: apps/v1
kind: Deployment
metadata:
  name: patch-demo
spec:
  replicas: 2
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: patch-demo-ctr
        image: nginx
      tolerations:
      - effect: NoSchedule
        key: dedicated
        value: test-team

假设想用如下的 yaml PATCH 这个 deploy

1
2
3
4
5
6
spec:
  template:
    spec:
      containers:
      - name: patch-demo-ctr-2
        image: redis
1
kubectl patch deployment patch-demo --patch "$(cat patch-file.yaml)"

依赖于上面的 containers patch 策略的声明,我们能得到如下的最终结果:

1
2
3
4
5
6
7
8
9
containers:
- image: redis
  imagePullPolicy: Always
  name: patch-demo-ctr-2
  ...
- image: nginx
  imagePullPolicy: Always
  name: patch-demo-ctr
  ...

这样更符合用户的期望.而其他的一些字段,如果没有声明 patchStrategy, 那么默认操作是 replace.

何为 Service-Side Apply

Server-Side Apply 可以字面地理解为将 kubectl apply的工作迁移到 Server 端来进行, 准确地可以定义为一种新的Merge算法.它的好处如下:

  • --dry-run如果是在 client 端进行,那么因为 Server 端存在各种 webhook以及 injector,很难模拟真实的情况.如果 –dry-run 放在服务端, 如果出错了,可以通过新加的 mangedFields 机制给用户更明确的提示
  • 如果不用 kubectl 而是 API 开发则很难利用到 kubectl 提供的功能
  • kubectl apply 使用的 kubectl.kubernetes.io/last-applied-configuration 更加声明式一些.用户发送一个资源的manifest,其中包含了自己关心的字段的期待状态.
  • 防止用户误修改一些字段
  • 多个组件/用户对资源的协作管理
  • 其他一些在 Server 端实现起来更简单的地方

Server-Side Apply 利用 managedFields 字段,追踪了各个字段的归属,并且能在更新的时候提供冲突检测(manager不匹配),并提供更为准确的提示.

managedFields 详解

现在我们可以详细看下 managedFields的内容:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
  - apiVersion: apps/v1
    fieldsType: FieldsV1
    fieldsV1:
      f:metadata:
        f:annotations:
          .: {}
          f:deployment.kubernetes.io/revision: {}
      f:status:
        f:availableReplicas: {}
        f:conditions:
          .: {}
          k:{"type":"Available"}:
            .: {}
            f:lastTransitionTime: {}
            f:lastUpdateTime: {}
            f:message: {}
            f:reason: {}
            f:status: {}
            f:type: {}
          k:{"type":"Progressing"}:
            .: {}
            f:lastTransitionTime: {}
            f:lastUpdateTime: {}
            f:message: {}
            f:reason: {}
            f:status: {}
            f:type: {}
        f:observedGeneration: {}
        f:readyReplicas: {}
        f:replicas: {}
        f:updatedReplicas: {}
    manager: kube-controller-manager
    operation: Update
    time: "2021-02-18T04:58:50Z"

其中:

  • f: 代表字段名
  • v: 代表 value, 字段的值
  • k: 代表 key, map object, 包含 f/v
  • i: index, 数组的索引

k 不太好理解, 对应的资源字段示例如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
status:
  availableReplicas: 3
  conditions:
  - lastTransitionTime: "2021-02-18T04:58:50Z"
    lastUpdateTime: "2021-02-18T04:58:50Z"
    message: Deployment has minimum availability.
    reason: MinimumReplicasAvailable
    status: "True"
    type: Available
  - lastTransitionTime: "2021-02-18T04:56:00Z"
    lastUpdateTime: "2021-02-18T04:58:50Z"
    message: ReplicaSet "nginx-deployment-877f48f6d" has successfully progressed.
    reason: NewReplicaSetAvailable
    status: "True"
    type: Progressing
  observedGeneration: 1
  readyReplicas: 3
  replicas: 3
  updatedReplicas: 3

另外:

  • manager: 操作主体
  • operator: 动作
  • time: 操作时间

如下所示,我们将上面用到的 deployment 修改一下,然后用不同的 manager 去 apply 一下试试:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
➜  server-side-apply git:(master) ✗ kubectl apply -f d.yaml --server-side --field-manager='test1'              
error: Apply failed with 1 conflict: conflict with "kubectl-client-side-apply" using apps/v1: .spec.replicas
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 http://k8s.io/docs/reference/using-api/api-concepts/#conflicts

上面的错误提示也展示了在冲突时可能的几种解决办法:

  • 覆盖: 如果确定是要修改,可以使用foce参数,强制覆盖,并且将其manager变更为自己
  • 忽略: 如果本身并不关心这个字段的值,可以将其从自己的 manifest 中移除然后重试
  • 共享: 将冲突字段改为原来的值,这种情况下更新后将与原来的manager共享管理.

同时需要注意的时,通常的两种更新操作对 manager 的处理也不同:

  1. PATCH: content type 为 application/apply-patch+yaml, 冲突逻辑如上所示
  2. UPDATE: 其他更新方式,完全忽略冲突

Controller 的使用

目前常用的对 resource 的操作基本上都遵循一个 READ-UPDATE 的步骤,因为 resourceVersion 的冲突问题. Service-Side Apply 有望能去掉 READ 这步,直接进行 UPDATE 操作.

一般也推荐 controller 在遇到冲突的时候执行 force 操作.

现状

功能仍有缺陷, 接口复杂,未到 stable 状态,尽量不用使用.

  1. Kubernetes 1.18 Feature Server-side Apply Beta 2
  2. Understanding OpenKruise Kubernetes Resource Update Mechanisms
  3. Strategic Merge Patch
  4. Update API Objects in Place Using kubectl patch
  5. Break Down Kubernetes Server-Side Apply