服务器端应用(Server-Side Apply)

特性状态: Kubernetes v1.22 [stable]

Kubernetes 支持多个应用程序协作管理一个对象的字段。 服务器端应用为集群的控制平面提供了一种可选机制,用于跟踪对对象字段的更改。 在特定资源级别,服务器端应用记录并跟踪有关控制该对象字段的信息。

服务器端应用协助用户和控制器通过声明式配置的方式管理他们的资源。 客户提交他们完整描述的意图,声明式地创建和修改对象

一个完整描述的意图并不是一个完整的对象,仅包括能体现用户意图的字段和值。 该意图可以用来创建一个新对象(未指定的字段使用默认值), 也可以通过 API 服务器来实现与现有对象的合并

与客户端应用对比小节解释了服务器端应用与最初的客户端 kubectl apply 实现的区别。

字段管理

Kubernetes API 服务器跟踪所有新建对象的受控字段(Managed Fields)

当尝试应用对象时,由另一个管理器拥有的字段且具有不同值,将导致冲突。 这样做是为了表明操作可能会撤消另一个合作者的更改。 可以强制写入具有托管字段的对象,在这种情况下,任何冲突字段的值都将被覆盖,并且所有权将被转移。

每当字段的值确实发生变化时,所有权就会从其当前管理器转移到进行更改的管理器。

服务器端应用会检查是否存在其他字段管理器也拥有该字段。 如果该字段不属于任何其他字段管理器,则该字段将被设置为其默认值(如果有),或者以其他方式从对象中删除。 同样的规则也适用于作为列表(list)、关联列表或键值对(map)的字段。

用户管理字段这件事,在服务器端应用的场景中,意味着用户依赖并期望字段的值不要改变。 最后一次对字段值做出断言的用户将被记录到当前字段管理器。 这可以通过发送 POSTcreate)、PUTupdate)、或非应用的 PATCHpatch) 显式更改字段管理器详细信息来实现。 还可以通过在服务器端应用操作中包含字段的值来声明和记录字段管理器。

如果两个或以上的应用者均把同一个字段设置为相同值,他们将共享此字段的所有权。 后续任何改变共享字段值的尝试,不管由那个应用者发起,都会导致冲突。 共享字段的所有者可以放弃字段的所有权,这只需发出不包含该字段的服务器端应用 patch 请求即可。

字段管理的信息存储在 managedFields 字段中,该字段是对象的 metadata。 中的一部分。

如果从清单中删除某个字段并应用该清单,则服务器端应用会检查是否有其他字段管理器也拥有该字段。 如果该字段不属于任何其他字段管理器,则服务器会将其从活动对象中删除,或者重置为其默认值(如果有)。 同样的规则也适用于关联列表(list)或键值对(map)。

与(旧版)由 kubectl 所管理的注解 kubectl.kubernetes.io/last-applied configuration 相比,服务器端应用使用了一种更具声明式的方法, 它跟踪用户(或客户端)的字段管理,而不是用户上次应用的状态。 作为服务器端应用的副作用,哪个字段管理器管理的对象的哪个字段的相关信息也会变得可用。

示例

服务器端应用创建对象的简单示例如下:

---
apiVersion: v1
kind: ConfigMap
metadata:
  name: test-cm
  namespace: default
  labels:
    test-label: test
  managedFields:
  - manager: kubectl
    operation: Apply # 注意大写: “Apply” (或者 “Update”)
    apiVersion: v1
    time: "2010-10-10T0:00:00Z"
    fieldsType: FieldsV1
    fieldsV1:
      f:metadata:
        f:labels:
          f:test-label: {}
      f:data:
        f:key: {}
data:
  key: some value

示例的 ConfigMap 对象在 .metadata.managedFields 中包含字段管理记录。 字段管理记录包括关于管理实体本身的基本信息,以及关于被管理的字段和相关操作(ApplyUpdate)的详细信息。 如果最后更改该字段的请求是服务器端应用的patch操作,则 operation 的值为 Apply;否则为 Update

还有另一种可能的结果。客户端会提交无效的请求体。 如果完整描述的意图没有构造出有效的对象,则请求失败。

但是,可以通过 update 或不使用服务器端应用的 patch 操作去更新 .metadata.managedFields。 强烈不鼓励这么做,但当发生如下情况时, 比如 managedFields 进入不一致的状态(显然不应该发生这种情况), 这么做也是一个合理的尝试。

managedFields 的格式在 Kubernetes API 参考中描述

冲突

冲突是一种特定的错误状态, 发生在执行 Apply 改变一个字段,而恰巧该字段被其他用户声明过主权时。 这可以防止一个应用者不小心覆盖掉其他用户设置的值。 冲突发生时,应用者有三种办法来解决它:

  • 覆盖前值,成为唯一的管理器: 如果打算覆盖该值(或应用者是一个自动化部件,比如控制器), 应用者应该设置查询参数 force 为 true(对 kubectl apply 来说,你可以使用命令行参数 --force-conflicts),然后再发送一次请求。 这将强制操作成功,改变字段的值,从所有其他管理器的 managedFields 条目中删除指定字段。

  • 不覆盖前值,放弃管理权: 如果应用者不再关注该字段的值, 应用者可以从资源的本地模型中删掉它,并在省略该字段的情况下发送请求。 这就保持了原值不变,并从 managedFields 的应用者条目中删除该字段。

  • 不覆盖前值,成为共享的管理器: 如果应用者仍然关注字段值,并不想覆盖它, 他们可以更改资源的本地模型中该字段的值,以便与服务器上对象的值相匹配, 然后基于本地更新发出新请求。这样做会保持值不变, 并使得该字段的管理由应用者与已经声称管理该字段的所有其他字段管理者共享。

字段管理器

管理器识别出正在修改对象的工作流程(在冲突时尤其有用), 并且可以作为修改请求的一部分,通过 fieldManager 查询参数来指定。 当你 Apply 某个资源时,需要指定 fieldManager 参数。 对于其他更新,API 服务器使用 “User-Agent:” HTTP 头(如果存在)推断字段管理器标识。

当你使用 kubectl 工具执行服务器端应用操作时,kubectl 默认情况下会将管理器标识设置为 “kubectl”

序列化

在协议层面,Kubernetes 用 YAML 来表示 Server-Side Apply 的消息体, 媒体类型为 application/apply-patch+yaml

序列化与 Kubernetes 对象相同,只是客户端不需要发送完整的对象。

以下是服务器端应用消息正文的示例(完整描述的意图):

{
  "apiVersion": "v1",
  "kind": "ConfigMap"
}

(这个请求将导致无更改的更新,前提是它作为 patch 请求的主体发送到有效的 v1/configmaps 资源, 并且请求中设置了合适的 Content-Type)。

字段管理范围内的操作

考虑字段管理的 Kubernetes API 操作包括:

  1. 服务器端应用(HTTP PATCH,内容类型为 application/apply-patch+yaml
  2. 替换现有对象(对 Kubernetes 而言是 update;HTTP 层面表现为 PUT

这两种操作都会更新 .metadata.managedFields,但行为略有不同。

除非你指定要强制重写,否则应用操作在遇到字段级冲突时总是会失败; 相比之下,如果你使用 update 进行的更改会影响托管字段,那么冲突从来不会导致操作失败。

所有服务器端应用的 patch 请求都必须提供 fieldManager 查询参数来标识自己, 而此参数对于 update 操作是可选的。 最后,使用 Apply 操作时,不能在提交的请求主体中设置 managedFields

一个包含多个管理器的对象,示例如下:

---
apiVersion: v1
kind: ConfigMap
metadata:
  name: test-cm
  namespace: default
  labels:
    test-label: test
  managedFields:
  - manager: kubectl
    operation: Apply
    apiVersion: v1
    fields:
      f:metadata:
        f:labels:
          f:test-label: {}
  - manager: kube-controller-manager
    operation: Update
    apiVersion: v1
    time: '2019-03-30T16:00:00.000Z'
    fields:
      f:data:
        f:key: {}
data:
  key: new value

在这个例子中, 第二个操作被管理器 kube-controller-managerupdate 的方式运行。 更新操作执行成功,并更改了 data 字段中的一个值, 并使得该字段的管理器被改为 kube-controller-manager

如果尝试把更新操作改为服务器端应用,那么这一尝试会因为所有权冲突的原因,导致操作失败。

合并策略

由服务器端应用实现的合并策略,提供了一个总体更稳定的对象生命周期。 服务器端应用试图依据负责管理它们的主体来合并字段,而不是根据值来否决。 这么做是为了多个主体可以更新同一个对象,且不会引起意外的相互干扰。

当用户发送一个完整描述的意图对象到服务器端应用的服务端点时, 服务器会将它和当前对象做一次合并,如果两者中有重复定义的值,那就以请求体中的为准。 如果请求体中条目的集合不是此用户上一次操作条目的超集, 所有缺失的、没有其他应用者管理的条目会被删除。 关于合并时用来做决策的对象规格的更多信息,参见 sigs.k8s.io/structured-merge-diff.

Kubernetes API(以及为 Kubernetes 实现该 API 的 Go 代码)都允许定义合并策略标记(Merge Strategy Markers)。 这些标记描述 Kubernetes 对象中各字段所支持的合并策略。 Kubernetes 1.16 和 1.17 中添加了一些标记, 对一个 CustomResourceDefinition 来说,你可以在定义自定义资源时设置这些标记。

Golang 标记 OpenAPI 扩展 可接受的值 描述
//+listType x-kubernetes-list-type atomic/set/map 适用于 list。set 适用于仅包含标量元素的列表。其中的元素不可重复。map 仅适用于嵌套了其他类型的列表。列表中的键(参见 listMapKey)不可以重复。atomic 适用于所有类型的列表。如果配置为 atomic,则合并时整个列表会被替换掉。任何时候,只有一个管理器负责管理指定列表。如果配置为 setmap,不同的管理器也可以分开管理不同条目。
//+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;此外,起用法和 OpenAPI 注释与 //+mapType 相同。

若未指定 listType,API 服务器将 patchStrategy=merge 标记解释为 listType=map 并且视对应的 patchMergeKey 标记为 listMapKey 取值。

atomic 列表类型是递归的。

(在 Kubernetes 的 Go 代码中, 这些标记以注释的形式给出,代码作者不需要用字段标记的形式重复指定它们)。

自定义资源和服务器端应用

默认情况下,服务器端应用将自定义资源视为无结构的数据。 所有键被视为 struct 数据类型的字段,所有列表都被视为 atomic 形式。

如果 CustomResourceDefinition 定义了的 schema 包含在上一小节合并策略中定义的注解, 那么在合并此类型的对象时,就会使用这些注解。

拓扑变化时的兼容性

在极少的情况下,CustomResourceDefinition(CRD)的作者或者内置类型可能希望更改其资源中的某个字段的 拓扑配置,同时又不提升版本号。 通过升级集群或者更新 CRD 来更改类型的拓扑信息,与更新现有对象的结果不同。 变更的类型有两种:一种是将字段从 map/set/granular 更改为 atomic, 另一种是做逆向改变。

listTypemapTypestructTypemap/set/granular 改为 atomic 时,现有对象的整个列表、映射或结构的属主都会变为这些类型的 元素之一的属主。这意味着,对这些对象的进一步变更会引发冲突。

当某 listTypemapTypestructTypeatomic 改为 map/set/granular 之一时, API 服务器无法推导这些字段的新的属主。因此,当对象的这些字段 再次被更新时不会引发冲突。出于这一原因,不建议将某类型从 atomic 改为 map/set/granular

以下面的自定义资源为例:

---
apiVersion: example.com/v1
kind: Foo
metadata:
  name: foo-sample
  managedFields:
  - manager: manager-one
    operation: Apply
    apiVersion: example.com/v1
    fields:
      f:spec:
        f:data: {}
spec:
  data:
    key1: val1
    key2: val2

spec.dataatomic 改为 granular 之前, manager-onespec.data 字段及其所包含字段(key1key2)的属主。 当对应的 CRD 被更改,使得 spec.data 变为 granular 拓扑时, manager-one 继续拥有顶层字段 spec.data(这意味着其他管理器想删除名为 data 的映射而不引起冲突是不可能的),但不再拥有 key1key2。 因此,其他管理器可以在不引起冲突的情况下更改或删除这些字段。

在控制器中使用服务器端应用

控制器的开发人员可以把服务器端应用作为简化控制器的更新逻辑的方式。 读-改-写 和/或 patch 的主要区别如下所示:

  • 应用的对象必须包含控制器关注的所有字段。
  • 对于在控制器没有执行过应用操作之前就已经存在的字段,不能删除。 (控制器在这种用例环境下,依然可以发送一个 patchupdate
  • 对象不必事先读取,resourceVersion 不必指定。

强烈推荐:设置控制器始终在其拥有和管理的对象上强制冲突,这是因为冲突发生时,它们没有其他解决方案或措施。

转移所有权

除了通过冲突解决方案提供的并发控制, 服务器端应用提供了一些协作方式来将字段所有权从用户转移到控制器。

最好通过例子来说明这一点。 让我们来看看,在使用 Horizo​​ntalPodAutoscaler 资源和与之配套的控制器, 且开启了 Deployment 的自动水平扩展功能之后, 怎么安全的将 replicas 字段的所有权从用户转移到控制器。

假设用户定义了 Deployment,且 replicas 字段已经设置为期望的值:

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

并且,用户使用服务器端应用,像这样创建 Deployment:

kubectl apply -f https://k8s.io/examples/application/ssa/nginx-deployment.yaml --server-side

然后,为 Deployment 启用自动扩缩,例如:

kubectl autoscale deployment nginx-deployment --cpu-percent=50 --min=1 --max=10

现在,用户希望从他们的配置中删除 replicas,从而避免与 HorizontalPodAutoscaler(HPA)及其控制器发生冲突。 然而,这里存在一个竞态: 在 HPA 需要调整 .spec.replicas 之前会有一个时间窗口, 如果在 HPA 写入字段并成为新的属主之前,用户删除了 .spec.replicas, 那 API 服务器就会把 .spec.replicas 的值设为 1(Deployment 的默认副本数)。 这不是用户希望发生的事情,即使是暂时的——它很可能会导致正在运行的工作负载降级。

这里有两个解决方案:

  • (基本操作)把 replicas 留在配置文件中;当 HPA 最终写入那个字段, 系统基于此事件告诉用户:冲突发生了。在这个时间点,可以安全的删除配置文件。
  • (高级操作)然而,如果用户不想等待,比如他们想为合作伙伴保持集群清晰, 那他们就可以执行以下步骤,安全的从配置文件中删除 replicas

首先,用户新定义一个只包含 replicas 字段的新清单:

# 将此文件另存为 'nginx-deployment-replicas-only.yaml'
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-deployment
spec:
  replicas: 3

用户使用私有字段管理器名称应用该清单。在本例中,用户选择了 handover-to-hpa

kubectl apply -f nginx-deployment-replicas-only.yaml \
  --server-side --field-manager=handover-to-hpa \
  --validate=false

如果应用操作和 HPA 控制器产生冲突,那什么都不做。 冲突表明控制器在更早的流程中已经对字段声明过所有权。

在此时间点,用户可以从清单中删除 replicas

apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-deployment
  labels:
    app: nginx
spec:
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx
        image: nginx:1.14.2

注意,只要 HPA 控制器为 replicas 设置了一个新值, 该临时字段管理器将不再拥有任何字段,会被自动删除。 这里无需进一步清理。

在管理器之间转移所有权

通过在配置文件中把一个字段设置为相同的值,多个字段管理器可以在彼此之间转移字段的所有权, 从而实现字段所有权的共享。 当某管理器共享了字段的所有权,管理器中任何一个成员都可以从其应用的配置中删除该字段, 从而放弃所有权,并完成了所有权向其他字段管理器的转移。

与客户端应用的对比

服务器端应用意味着既可以替代原来 kubectl apply 子命令的客户端实现, 也可供控制器作为实施变更的简单有效机制。

kubectl 管理的 last-applied 注解相比, 服务器端应用使用一种更具声明性的方法来跟踪对象的字段管理,而不是记录用户最后一次应用的状态。 这意味着,使用服务器端应用的副作用,就是字段管理器所管理的对象的每个字段的相关信息也会变得可用。

由服务器端应用实现的冲突检测和解决方案的一个结果就是, 应用者总是可以在本地状态中得到最新的字段值。 如果得不到最新值,下次执行应用操作时就会发生冲突。 解决冲突三个选项的任意一个都会保证:此应用过的配置文件是服务器上对象字段的最新子集。

这和客户端应用(Client-Side Apply)不同,如果有其他用户覆盖了此值, 过期的值被留在了应用者本地的配置文件中。 除非用户更新了特定字段,此字段才会准确, 应用者没有途径去了解下一次应用操作是否会覆盖其他用户的修改。

另一个区别是使用客户端应用的应用者不能改变他们正在使用的 API 版本,但服务器端应用支持这个场景。

客户端应用和服务器端应用的迁移

从客户端应用升级到服务器端应用

客户端应用方式时,用户使用 kubectl apply 管理资源, 可以通过使用下面标记切换为使用服务器端应用。

kubectl apply --server-side [--dry-run=server]

默认情况下,对象的字段管理从客户端应用方式迁移到 kubectl 触发的服务器端应用时,不会发生冲突。

此操作以 kubectl 作为字段管理器来应用到服务器端应用。 作为例外,可以指定一个不同的、非默认字段管理器停止的这种行为,如下面的例子所示。 对于 kubectl 触发的服务器端应用,默认的字段管理器是 kubectl

kubectl apply --server-side --field-manager=my-manager [--dry-run=server]

从服务器端应用降级到客户端应用

如果你用 kubectl apply --server-side 管理一个资源, 可以直接用 kubectl apply 命令将其降级为客户端应用。

降级之所以可行,这是因为 kubectl server-side apply 会保存最新的 last-applied-configuration 注解。

此操作以 kubectl 作为字段管理器应用到服务器端应用。 作为例外,可以指定一个不同的、非默认字段管理器停止这种行为,如下面的例子所示。 对于 kubectl 触发的服务器端应用,默认的字段管理器是 kubectl

kubectl apply --server-side --field-manager=my-manager [--dry-run=server]

API 实现

支持服务器端应用的资源的 PATCH 动词可以接受非官方的 application/apply-patch+yaml 内容类型。 服务器端应用的用户可以将部分指定的对象以 YAML 格式作为 PATCH 请求的主体发送到资源的 URI。 应用配置时,你应该始终包含对要定义的结果(如所需状态)重要的所有字段。

所有 JSON 消息都是有效的 YAML。一些客户端使用 YAML 请求体指定服务器端应用请求, 而这些 YAML 同样是合法的 JSON。

访问控制和权限

由于服务端应用是一种 PATCH 类型的操作, 所以一个主体(例如 Kubernetes RBAC 的 Role)需要 patch 权限才能编辑存量资源,还需要 create 权限才能使用服务器端应用创建新资源。

清除 managedFields

通过使用 patch(JSON Merge Patch, Strategic Merge Patch, JSON Patch)覆盖对象, 或者通过 update(HTTP PUT),可以从对象中剥离所有 managedFields; 换句话说,通过除了 apply 之外的所有写操作均可实现这点。 清除 managedFields 字段的操作可以通过用空条目覆盖 managedFields 字段的方式实现。以下是两个示例:

PATCH /api/v1/namespaces/default/configmaps/example-cm
Accept: application/json
Content-Type: application/merge-patch+json

{
  "metadata": {
    "managedFields": [
      {}
    ]
  }
}
PATCH /api/v1/namespaces/default/configmaps/example-cm
Accept: application/json
Content-Type: application/json-patch+json
If-Match: 1234567890123456789

[{"op": "replace", "path": "/metadata/managedFields", "value": [{}]}]

这一操作将用只包含一个空条目的列表来覆盖 managedFields, 从而实现从对象中整体去除 managedFields。 注意,只把 managedFields 设置为空列表并不会重置该字段。 这一设计是有意为之的,目的是避免 managedFields 被与该字段无关的客户删除。

在某些场景中,执行重置操作的同时还会给出对 managedFields 之外的别的字段的变更, 对于这类操作,managedFields 首先会被重置,其他变更被压后处理。 其结果是,应用者取得了同一个请求中所有字段的所有权。

接下来

你可以阅读 Kubernetes API 参考中的 metadata 顶级字段的 managedFields

最后修改 February 29, 2024 at 11:10 AM PST: [zh-cn]resync server-side-apply.md (5d7bf72ffe)