服务器端应用

功能状态: Kubernetes v1.22 [稳定]

Kubernetes 支持多个应用程序协作管理单个 对象 的字段。

服务器端应用提供了一种可选机制,用于您的集群控制平面跟踪对对象字段的更改。在特定资源级别,服务器端应用记录并跟踪有关控制该对象字段的信息。

服务器端应用帮助用户和 控制器 通过声明性配置管理他们的资源。客户端可以通过提交其完全指定的意图来声明式地创建和修改 对象

完全指定的意图是一个部分对象,它只包含用户对哪些字段和值有意见。该意图要么创建一个新对象(对未指定的字段使用默认值),要么由 API 服务器 组合 到现有的对象中。

与客户端应用的比较 解释了服务器端应用与原始的客户端 kubectl apply 实现的不同之处。

字段管理

Kubernetes API 服务器跟踪所有新创建对象的管理字段

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

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

应用检查是否有任何其他字段管理器也拥有该字段。如果该字段没有被任何其他字段管理器拥有,则该字段将被设置为其默认值(如果有),否则将从对象中删除。相同的规则适用于列表、关联列表或映射的字段。

为了让用户管理一个字段(从服务器端应用的角度来看),意味着用户依赖于该字段的值不会改变,并且期望它不会改变。最后对字段值做出断言的用户将被记录为当前字段管理器。这可以通过使用 HTTP POST创建)、PUT更新)或非应用 PATCH修补)明确更改字段管理器详细信息来完成。您还可以通过在服务器端应用操作中包含该字段的值来声明和记录字段管理器。

服务器端应用修补请求要求客户端提供其身份作为 字段管理器。当使用服务器端应用时,尝试更改由不同管理器控制的字段会导致请求被拒绝,除非客户端强制覆盖。有关覆盖的详细信息,请参阅 冲突

当两个或多个应用程序将字段设置为相同的值时,它们将共享该字段的所有权。任何随后尝试由任何应用程序更改共享字段的值都会导致冲突。共享字段所有者可以通过进行不包含该字段的服务器端应用修补请求来放弃该字段的所有权。

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

如果您从清单中删除一个字段并应用该清单,服务器端应用会检查是否有任何其他字段管理器也拥有该字段。如果该字段没有被任何其他字段管理器拥有,它要么从实时对象中删除,要么重置为其默认值(如果有)。相同的规则适用于关联列表或映射项。

与 (传统) kubectl.kubernetes.io/last-applied-configuration 注解 (由 kubectl 管理) 相比,服务器端应用使用了一种更声明性的方法,该方法跟踪用户的 (或客户端的) 字段管理,而不是用户的最后应用状态。使用服务器端应用的副作用是,有关哪个字段管理器管理对象中每个字段的信息也变得可用。

示例

使用服务器端应用创建的对象的简单示例可能如下所示

---
apiVersion: v1
kind: ConfigMap
metadata:
  name: test-cm
  namespace: default
  labels:
    test-label: test
  managedFields:
  - manager: kubectl
    operation: Apply # note capitalization: "Apply" (or "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)的详细信息。如果最后更改该字段的请求是服务器端应用修补,则 operation 的值为 Apply;否则,它为 Update

还有另一种可能的结果。客户端可能会提交无效的请求主体。如果完全指定的意图不会产生有效的对象,则请求将失败。

但是,可以通过更新或不使用服务器端应用的修补操作来更改 .metadata.managedFields。这样做强烈建议避免,但可能是合理的选项,例如,如果 .metadata.managedFields 处于不一致状态(在正常操作中不应该发生)。

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

冲突

冲突是一种特殊的错误状态,当 Apply 操作尝试更改另一个管理器也声称要管理的字段时发生。这可以防止应用程序无意中覆盖由另一个用户设置的值。当这种情况发生时,应用程序有 3 种选择来解决冲突

  • 覆盖值,成为唯一管理器:如果覆盖值是故意的(或者如果应用程序是自动流程,如控制器),则应用程序应将 force 查询参数设置为 true(对于 kubectl apply,您使用 --force-conflicts 命令行参数),并再次发出请求。这将强制操作成功,更改字段的值,并将该字段从 managedFields 中所有其他管理器的条目中删除。

  • 不要覆盖值,放弃管理声明:如果应用程序不再关心该字段的值,则应用程序可以将其从其资源的本地模型中删除,并发出一个新的请求,其中省略了该特定字段。这将使值保持不变,并将导致该字段从应用程序在 managedFields 中的条目中删除。

  • 不要覆盖值,成为共享管理器:如果应用程序仍然关心该字段的值,但不想覆盖它,则它们可以在其资源的本地模型中更改该字段的值,以匹配服务器上对象的的值,然后发出一个新的请求,该请求考虑了该本地更新。这样做将使值保持不变,并将导致该字段的管理由应用程序与所有其他已声称要管理它的字段管理器共享。

字段管理器

管理器标识正在修改对象的独特工作流(在冲突时特别有用!),并且可以通过 fieldManager 查询参数指定为修改请求的一部分。当您应用到资源时,fieldManager 参数是必需的。对于其他更新,API 服务器从 "User-Agent:" HTTP 标头 (如果有) 推断字段管理器身份。

当您使用 kubectl 工具执行服务器端应用操作时,kubectl 默认将管理器身份设置为 "kubectl"

序列化

在协议级别,Kubernetes 将服务器端应用消息主体表示为 YAML,媒体类型为 application/apply-patch+yaml

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

以下是一个服务器端应用消息主体的示例(完全指定的意图)

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

(这将进行无更改更新,前提是它作为修补请求的主体发送到有效的 v1/configmaps 资源,并具有适当的请求 Content-Type)。

字段管理范围内操作

Kubernetes API 操作,其中考虑了字段管理,包括

  1. 服务器端应用 (HTTP PATCH,内容类型为 application/apply-patch+yaml)
  2. 替换现有对象 (对 Kubernetes 的更新;在 HTTP 级别上为 PUT)

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

除非您指定强制覆盖,否则遇到字段级冲突的应用操作始终会失败;相反,如果您使用更新进行更改,这会影响管理字段,则冲突永远不会引发操作失败。

所有服务器端应用补丁请求都需要通过提供fieldManager查询参数来标识自己,而该查询参数对于更新操作是可选的。最后,在使用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-manager的管理器运行。更新请求成功并更改了数据字段中的一个值,这导致该字段的管理更改为kube-controller-manager

如果此更新尝试使用服务器端应用,则请求将由于所有权冲突而失败。

合并策略

与服务器端应用一起实现的合并策略提供了通常更稳定的对象生命周期。服务器端应用尝试根据管理它们的参与者合并字段,而不是根据值覆盖。这样,多个参与者可以更新同一个对象,而不会造成意外的干扰。

当用户将一个完全指定意图对象发送到服务器端应用端点时,服务器会将其与实时对象合并,如果请求正文中同时指定了两个地方的值,则优先使用请求正文中的值。如果应用配置中存在的项目集不是上次同一用户应用的项目的超集,则将删除任何未由其他应用器管理的缺失项目。有关在合并时如何使用对象架构来做出决策的更多信息,请参见sigs.k8s.io/structured-merge-diff

Kubernetes API(以及为 Kubernetes 实现该 API 的 Go 代码)允许定义合并策略标记。这些标记描述了 Kubernetes 对象中字段支持的合并策略。对于自定义资源定义,您可以在定义自定义资源时设置这些标记。

Golang 标记OpenAPI 扩展可能的值描述
//+listTypex-kubernetes-list-typeatomic/set/map适用于列表。set适用于仅包含标量元素的列表。这些元素必须是唯一的。map仅适用于嵌套类型的列表。键值(参见listMapKey)在列表中必须是唯一的。atomic可以应用于任何列表。如果配置为atomic,则在合并期间将替换整个列表。在任何时间点,单个管理器都拥有该列表。如果为setmap,则不同的管理器可以分别管理条目。
//+listMapKeyx-kubernetes-list-map-keys字段名称列表,例如["port", "protocol"]仅在+listType=map时适用。字段名称列表,其值唯一标识列表中的条目。虽然可以有多个键,但listMapKey是单数的,因为需要在 Go 类型中分别指定键。键字段必须是标量。
//+mapTypex-kubernetes-map-typeatomic/granular适用于映射。atomic表示映射只能被单个管理器完全替换。granular表示映射支持不同的管理器更新各个字段。
//+structTypex-kubernetes-map-typeatomic/granular适用于结构体;否则与//+mapType相同的使用方式和 OpenAPI 注释。

如果listType缺失,API 服务器将解释patchStrategy=merge标记为listType=map,并将相应的patchMergeKey标记解释为listMapKey

atomic列表类型是递归的。

(在 Kubernetes 的Go代码中,这些标记被指定为注释,代码作者无需将其重复为字段标签)。

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

默认情况下,服务器端应用将自定义资源视为非结构化数据。所有键都与结构体字段相同,所有列表都被视为原子性的。

如果 CustomResourceDefinition 定义了一个schema,其中包含前一合并策略部分中定义的注释,则这些注释将在合并此类型的对象时使用。

跨拓扑更改的兼容性

在罕见的情况下,CustomResourceDefinition (CRD) 或内置资源的作者可能希望更改资源中某个字段的特定拓扑,而无需增加其 API 版本。通过升级集群或更新 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-one拥有spec.data字段及其中的所有字段(key1key2)。当 CRD 更改为使spec.data变为granular时,manager-one继续拥有顶层字段spec.data(这意味着没有其他管理器可以删除名为data的映射,而不会发生冲突),但它不再拥有key1key2,因此另一个管理器可以修改或删除这些字段,而不会发生冲突。

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

作为控制器的开发人员,您可以使用服务器端应用来简化控制器的更新逻辑。与读-改-写和/或补丁的主要区别在于以下几点

  • 应用的对象必须包含控制器关心的所有字段。
  • 无法删除之前未由控制器应用的字段(控制器仍然可以发送补丁更新以用于这些用例)。
  • 对象不必事先读取;不必指定resourceVersion

强烈建议控制器始终强制对它们拥有和管理的对象进行冲突,因为它们可能无法解决或处理这些冲突。

转移所有权

除了冲突解决提供的并发控制之外,服务器端应用还提供了从用户到控制器执行协调字段所有权转移的方法。

这最好通过示例来解释。让我们看看如何安全地将replicas字段的所有权从用户转移到控制器,同时使用 HorizontalPodAutoscaler 资源及其伴随控制器为 Deployment 启用自动水平缩放。

假设用户定义了 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字段

# Save this file as '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注释相比,服务器端应用使用更具声明性的方法,该方法跟踪对象的字段管理,而不是用户最后应用的状态。这意味着使用服务器端应用的副作用是,有关哪些字段管理器管理对象中每个字段的信息也变得可用。

服务器端应用实现的冲突检测和解决机制的一个结果是,应用器在其本地状态中始终拥有最新的字段值。如果它们没有最新值,那么它们在下次应用时就会遇到冲突。解决冲突的三种选项中的任何一种都会导致应用的配置成为服务器字段上对象的最新子集。

这与客户端应用不同,在客户端应用中,被其他用户覆盖的过时值会保留在应用器的本地配置中。这些值只有在用户更新特定字段(如果有的话)时才会变得准确,而且应用器无法知道其下次应用是否会覆盖其他用户的更改。

另一个区别是,使用客户端应用的应用器无法更改其使用的 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 apply,kubectl 服务器端应用会将 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。某些客户端使用也是有效 JSON 的 YAML 请求正文指定服务器端应用请求。

访问控制和权限

由于服务器端应用是一种 PATCH 类型,因此主体(如 Kubernetes 的 RBAC 角色)需要 patch 权限来编辑现有资源,并且还需要 create 动词权限才能使用服务器端应用创建新资源。

清除 managedFields

可以通过使用 patch(JSON 合并补丁、策略合并补丁、JSON 补丁)或通过 update(HTTP PUT)覆盖 managedFields 来删除对象中的所有 managedFields;换句话说,通过除 apply 之外的所有写入操作。这可以通过用空条目覆盖 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

最后修改日期:2024 年 2 月 20 日下午 9:48 PST:将更多特征状态简码改为数据驱动 (7b6866063f)