Kubernetes v1.22 [稳定版] (默认启用)Kubernetes 支持多个应用方协同管理单个 对象 的字段。
服务端应用提供了一种可选机制,供集群控制平面追踪对象字段的变更。在特定资源层级上,服务端应用会记录并追踪关于谁控制了该对象哪些字段的信息。
服务端应用通过声明式配置,帮助用户和 控制器 管理资源。客户端可以通过提交其 完全指定的意图,以声明方式创建和修改 对象。
所谓完全指定的意图,是指一个仅包含用户有特定主张的字段和值的局部对象。该意图要么创建一个新对象(未指定的字段将使用默认值),要么由 API 服务器与现有对象进行 合并。
与客户端应用 (Client-Side Apply) 的比较 解释了服务端应用与原始的客户端 kubectl apply 实现有何不同。
Kubernetes API 服务器会追踪所有新建对象的 托管字段 (managed fields)。
在尝试应用对象时,如果字段值不同且被另一个 管理器 所拥有,则会导致 冲突。这样做是为了提示该操作可能会撤销其他协作者的更改。对于带有托管字段的对象,写入操作可以被强制执行,在这种情况下,任何冲突字段的值都将被覆盖,且所有权会被转移。
每当字段值发生变化时,所有权即从当前管理器转移至进行更改的管理器。
对于用户而言,在服务端应用的语境下,管理一个字段意味着用户依赖该字段的值且期望其不发生变化。最后一次对字段值做出断言的用户将被记录为当前的字段管理器。这可以通过使用 HTTP POST (创建)、PUT (更新) 或非 apply 类型的 PATCH (补丁) 操作显式更改字段管理器详情来实现。你也可以通过在服务端应用操作中包含该字段的值,来声明并记录一个字段管理器。
服务端应用 补丁 (patch) 请求要求客户端以 字段管理器 的身份提供请求。当使用服务端应用时,除非客户端强制覆盖,否则尝试更改由不同管理器控制的字段将导致请求被拒绝。有关覆盖的详细信息,请参阅 冲突。
当两个或多个应用方将同一个字段设置为相同的值时,他们将共享该字段的所有权。随后任何一方试图更改该共享字段的值,都会导致冲突。共享字段的所有者可以通过发出不包含该字段的服务端应用 补丁 (patch) 请求来放弃对该字段的所有权。
字段管理详情存储在对象 metadata 的 managedFields 字段中。
如果你从清单中移除某个字段并执行应用操作,服务端应用会检查是否有其他字段管理器也拥有该字段。如果该字段没有被其他字段管理器拥有,它将从现存对象中删除,或者如果存在默认值,则会被重置为默认值。同样的规则也适用于关联列表或映射项。
与 kubectl 管理的(旧版)kubectl.kubernetes.io/last-applied-configuration 注解相比,服务端应用采用了更声明式的方法,它追踪的是用户(或客户端)的字段管理情况,而非用户最后一次的应用状态。作为使用服务端应用的一个副作用,对象中每个字段由哪个字段管理器所管理的信息也将变得可用。
一个使用服务端应用创建的对象的简单示例如下
kubectl get 默认会省略托管字段。如果输出格式为 json 或 yaml,可以添加 --show-managed-fields 来显示 managedFields。---
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 中包含一条字段管理记录。字段管理记录由管理实体本身的基本信息,以及被管理字段和相关操作(Apply 或 Update)的详情组成。如果最后一次更改该字段的请求是服务端应用 补丁 (patch),则 operation 的值为 Apply;否则为 Update。
还有另一种可能的结果。客户端可能会提交无效的请求体。如果完全指定的意图无法生成有效的对象,请求将会失败。
不过,确实可以通过 更新 (update) 或不使用服务端应用的 补丁 (patch) 操作来更改 .metadata.managedFields。强烈不建议这样做,但如果例如 .metadata.managedFields 进入了不一致的状态(这在正常操作中不应发生),这可能是一个合理的尝试选项。
managedFields 的格式在 Kubernetes API 参考中有所 描述。
.metadata.managedFields 字段由 API 服务器管理。你应该避免手动更新它。冲突 (conflict) 是一种特殊的状态错误,当 Apply 操作试图更改另一个管理器也声明要管理的字段时会发生。这可以防止应用方无意中覆盖其他用户设置的值。当这种情况发生时,应用方有 3 种解决冲突的选择:
覆盖值,成为唯一管理器: 如果覆盖值是故意的(或者应用方是像控制器这样的自动化过程),应用方应将 force 查询参数设置为 true(对于 kubectl apply,需使用 --force-conflicts 命令行参数),并再次发起请求。这会强制操作成功,更改字段值,并从 managedFields 中所有其他管理者的条目中移除该字段。
不覆盖值,放弃管理声明: 如果应用方不再关心该字段的值,应用方可以将其从本地资源模型中删除,并省略该特定字段重新发起请求。这会保持原值不变,并导致该字段从应用方在 managedFields 中的条目中被移除。
不覆盖值,成为共享管理器: 如果应用方仍然关心该字段的值,但不想覆盖它,他们可以在本地资源模型中更改该字段的值以匹配服务器上对象的值,然后发起新的请求以纳入此本地更新。这样做会保持原值不变,并导致该字段的管理权由应用方与所有其他已声明管理该字段的字段管理器共同拥有。
管理器用于标识修改对象的不同工作流(这在发生冲突时特别有用!),可以通过修改请求中包含的 fieldManager 查询参数来指定。当你对资源执行 Apply 操作时,fieldManager 参数是必需的。对于其他更新,API 服务器会从 HTTP 头部 "User-Agent:"(如果存在)中推断字段管理者的身份。
当你使用 kubectl 工具执行服务端应用操作时,kubectl 默认会将管理器身份设置为 "kubectl"。
在协议层面,Kubernetes 将服务端应用的消息体表示为 YAML,媒体类型为 application/apply-patch+yaml。
无论你提交的是 JSON 数据还是 YAML 数据,都请使用 application/apply-patch+yaml 作为 Content-Type 头部的值。
所有 JSON 文档都是合法的 YAML。然而,Kubernetes 存在一个已知问题:其使用的 YAML 解析器并未完全实现 YAML 规范,某些 JSON 转义可能无法被识别。
序列化方式与 Kubernetes 对象相同,唯一的例外是客户端无需发送完整对象。
以下是一个服务端应用消息体(完全指定的意图)的示例:
{
"apiVersion": "v1",
"kind": "ConfigMap"
}
(如果将其作为 补丁 (patch) 请求的主体发送到合法的 v1/configmaps 资源,并带有适当的请求 Content-Type,这将执行一次无变更的更新)。
考虑字段管理的 Kubernetes API 操作包括:
PATCH,内容类型为 application/apply-patch+yaml)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
time: '2019-03-30T15:00:00.000Z'
apiVersion: v1
fieldsType: FieldsV1
fieldsV1:
f:metadata:
f:labels:
f:test-label: {}
- manager: kube-controller-manager
operation: Update
apiVersion: v1
time: '2019-03-30T16:00:00.000Z'
fieldsType: FieldsV1
fieldsV1:
f:data:
f:key: {}
data:
key: new value
在此示例中,第二个操作是由名为 kube-controller-manager 的管理器以 更新 (update) 方式运行的。该更新请求成功并更改了 data 字段中的一个值,这导致该字段的管理权变更为 kube-controller-manager。
如果改为使用服务端应用尝试此更新,该请求将因所有权冲突而失败。
服务端应用实现的合并策略提供了通常更稳定的对象生命周期。服务端应用试图基于管理该字段的参与者进行合并,而不是基于值进行强制覆盖。这样,多个参与者可以更新同一个对象而不会引起意外干扰。
当用户向服务端应用端点发送一个 完全指定的意图 对象时,服务器会将其与现存对象合并,如果某字段在两处都有指定,则优先选用请求体中的值。如果应用配置中存在的项目集不是同一用户上次应用项目的超集,则每个未被其他应用方管理的缺失项都将被移除。有关在合并时如何使用对象模式进行决策的更多信息,请参阅 sigs.k8s.io/structured-merge-diff。
Kubernetes API(以及实现该 API 的 Kubernetes Go 代码)允许定义 合并策略标记 (merge strategy markers)。这些标记描述了 Kubernetes 对象内字段支持的合并策略。对于 CustomResourceDefinition,你可以在定义自定义资源时设置这些标记。
| Golang 标记 | OpenAPI 扩展 | 可能的值 | 描述 |
|---|---|---|---|
//+listType | x-kubernetes-list-type | atomic/set/map | 适用于列表。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 | 适用于映射。atomic 意味着该映射只能由单个管理器完全替换。granular 意味着该映射支持不同的管理器更新各个字段。 |
//+structType | x-kubernetes-map-type | atomic/granular | 适用于结构体;其余用法和 OpenAPI 注解与 //+mapType 相同。 |
如果 listType 缺失,API 服务器会将 patchStrategy=merge 标记解释为 listType=map,并将相应的 patchMergeKey 标记解释为 listMapKey。
atomic 列表类型是递归的。
(在 Kubernetes 的 Go 代码中,这些标记被指定为注释,代码作者无需将其重复为字段标签)。
默认情况下,服务端应用将自定义资源视为非结构化数据。所有键都被视为结构体字段,所有列表都被视为原子列表。
如果 CustomResourceDefinition 定义了包含上一节 合并策略 中定义的注解的 模式 (schema),这些注解将在合并该类型的对象时使用。
极少数情况下,CustomResourceDefinition (CRD) 或内置资源的作者可能希望在不增加 API 版本的情况下更改其资源中字段的具体拓扑结构。通过升级集群或更新 CRD 来更改类型拓扑,在更新现有对象时会产生不同的后果。变更分为两类:当字段从 map/set/granular 变为 atomic,以及反之。
当 listType、mapType 或 structType 从 map/set/granular 变为 atomic 时,现有对象的整个列表、映射或结构体将最终由拥有这些类型元素的参与者所拥有。这意味着对此类对象的任何进一步更改都会导致冲突。
当 listType、mapType 或 structType 从 atomic 变为 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
fieldsType: FieldsV1
fieldsV1:
f:spec:
f:data: {}
spec:
data:
key1: val1
key2: val2
在 spec.data 从 atomic 更改为 granular 之前,manager-one 拥有 spec.data 字段及其内部的所有字段(key1 和 key2)。当 CRD 被更改为使 spec.data 变为 granular 后,manager-one 仍然拥有顶层字段 spec.data(意味着没有其他管理器可以在不产生冲突的情况下删除名为 data 的映射),但它不再拥有 key1 和 key2,因此另一个管理器可以修改或删除这些字段而不会产生冲突。
作为控制器开发者,你可以使用服务端应用来简化控制器的更新逻辑。与读取-修改-写入和/或补丁相比,主要区别在于:
resourceVersion。强烈建议控制器始终对其拥有和管理的对象强制冲突解决,因为它们可能无法解决或处理这些冲突。
除了 冲突解决 提供的并发控制外,服务端应用还提供了将字段所有权从用户协调转移给控制器的方法。
通过示例可以最好地说明这一点。让我们看看如何安全地将 replicas 字段的所有权从用户转移给控制器,同时使用 HorizontalPodAutoscaler 资源及其附带的控制器为 Deployment 启用自动水平扩展。
假设用户定义了一个将 replicas 设置为期望值的 Deployment:
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=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
spec.replicas 字段,则无需提供完全合规的 Deployment 清单。用户使用私有的字段管理器名称应用该清单。在此示例中,用户选择了 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 服务端应用,而不会遇到冲突。
保持 last-applied-configuration 注解为最新。该注解会推断客户端应用的托管字段。任何非客户端应用所管理的字段都会引发冲突。
例如,如果你在客户端应用之后使用 kubectl scale 更新了 replicas 字段,则该字段不归客户端应用所有,从而在执行 kubectl apply --server-side 时产生冲突。
此行为适用于使用 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]
PATCH 动词(对于支持服务端应用的对象)接受非官方的 application/apply-patch+yaml 内容类型。服务端应用的用户可以将部分指定的对象作为 YAML 发送,作为向资源 URI 发起的 PATCH 请求的主体。应用配置时,应始终包含所有对结果重要的字段(如期望状态)。
所有 JSON 消息都是有效的 YAML。因此,除了对服务端应用请求使用 YAML 请求主体外,你也可以使用 JSON 请求主体,因为它们也是有效的 YAML。无论哪种情况,HTTP 请求都应使用媒体类型 application/apply-patch+yaml。
由于服务端应用是一种 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 的内容。