服务器端应用
Kubernetes v1.22 [stable]
(默认启用:true)Kubernetes 支持多个应用者协同管理单个对象的字段。
服务器端应用 (Server-Side Apply) 提供了一种可选机制,用于集群控制平面跟踪对象字段的更改。在特定资源层面,服务器端应用记录并跟踪对该对象字段的控制信息。
服务器端应用帮助用户和控制器通过声明式配置管理其资源。客户端可以通过提交其_完全指定的意图_来声明式地创建和修改对象。
完全指定的意图是一个部分对象,只包含用户认为应该存在的字段和值。该意图要么创建一个新对象(对未指定的字段使用默认值),要么由 API 服务器与现有对象合并。
与客户端应用对比解释了服务器端应用与原始客户端 kubectl apply
实现的区别。
字段管理
Kubernetes API 服务器跟踪所有新创建对象的_已管理字段_。
尝试应用对象时,如果字段具有不同的值且由其他管理者拥有,则会导致冲突。这是为了表明该操作可能会撤消其他协作者的更改。强制写入具有已管理字段的对象时,任何冲突字段的值都将被覆盖,并且所有权将转移。
每当字段的值发生变化时,所有权将从当前管理者转移到进行更改的管理者。
应用会检查是否有其他字段管理者也拥有该字段。如果该字段没有被任何其他字段管理者拥有,则该字段会被设置为其默认值(如果有),否则将从对象中删除。同样的规则也适用于列表、关联列表或映射类型的字段。
对于用户来说,以服务器端应用的方式管理字段,意味着用户依赖并期望该字段的值不会改变。最后对字段值进行断言的用户将被记录为当前字段管理者。这可以通过使用 HTTP `POST` (**创建**)、`PUT` (**更新**) 或非应用 `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
)的详细信息组成。如果最后更改该字段的请求是服务器端应用**补丁**,则 operation
的值为 Apply
;否则为 Update
。
还有另一种可能的结果。客户端可能会提交一个无效的请求体。如果完全指定的意图无法生成一个有效的对象,则请求会失败。
然而,可以通过**更新**或不使用服务器端应用的**补丁**操作来更改 .metadata.managedFields
。强烈不鼓励这样做,但如果 .metadata.managedFields
出现不一致状态(在正常操作中不应该发生),例如,这可能是一个合理的尝试选项。
managedFields
的格式在 Kubernetes API 参考中有所描述。
注意
.metadata.managedFields
字段由 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
。
注意
无论你提交的是 JSON 数据还是 YAML 数据,都使用 application/apply-patch+yaml
作为 Content-Type
标头的值。
所有 JSON 文档都是有效的 YAML。然而,Kubernetes 有一个 bug,它使用的 YAML 解析器并未完全实现 YAML 规范。某些 JSON 转义字符可能无法识别。
序列化与 Kubernetes 对象相同,不同之处在于客户端无需发送完整的对象。
以下是服务器端应用消息体(完全指定的意图)的示例
{
"apiVersion": "v1",
"kind": "ConfigMap"
}
(如果它作为**补丁**请求的正文发送到有效的 v1/configmaps
资源,并且具有适当的请求 Content-Type
,这将是一个无更改的更新)。
字段管理范围内的操作
Kubernetes API 中考虑字段管理的操作有:
- 服务器端应用(HTTP `PATCH`,内容类型为
application/apply-patch+yaml
) - 替换现有对象(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
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
的管理者作为**更新**运行。更新请求成功并更改了数据字段中的值,导致该字段的管理权更改为 kube-controller-manager
。
如果此更新尝试使用服务器端应用,则该请求将由于所有权冲突而失败。
合并策略
通过服务器端应用实现的合并策略提供了通常更稳定的对象生命周期。服务器端应用试图根据管理字段的角色进行合并,而不是根据值进行否决。这样,多个角色可以更新同一个对象而不会引起意外干扰。
当用户向服务器端应用端点发送一个_完全指定的意图_对象时,服务器会将其与活动对象合并,如果两者都指定了该值,则优先使用请求体中的值。如果应用配置中存在的项目集不是同一用户上次应用的项目集的超集,则所有未被其他应用者管理的缺失项目都将被删除。有关对象模式如何用于在合并时做出决策的更多信息,请参阅 sigs.k8s.io/structured-merge-diff。
Kubernetes API(以及为 Kubernetes 实现该 API 的 Go 代码)允许定义_合并策略标记_。这些标记描述了 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 定义了一个包含前一合并策略部分中定义的注释的模式,则在合并此类型对象时将使用这些注释。
拓扑变更兼容性
在极少数情况下,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
字段的所有权从用户转移到控制器,同时为 Deployment 启用自动水平扩缩,使用 HorizontalPodAutoscaler 资源及其附带的控制器。
假设用户已经定义了一个 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
注意
在这种情况下,用于 SSA 的 YAML 文件只包含您想要更改的字段。如果您只想使用 SSA 修改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
更新了副本字段,那么该字段不归客户端应用所有,并在 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]
API 实现
PATCH
动词(对于支持服务器端应用的对象)接受非官方的 application/apply-patch+yaml
内容类型。服务器端应用的用户可以将部分指定的对象作为 YAML 作为 PATCH
请求的正文发送到资源的 URI。应用配置时,应始终包含所有对结果(例如所需状态)至关重要的字段。
所有 JSON 消息都是有效的 YAML。因此,除了使用 YAML 请求体进行服务器端应用请求外,您还可以使用 JSON 请求体,因为它们也是有效的 YAML。无论哪种情况,HTTP 请求都使用媒体类型 application/apply-patch+yaml
。
访问控制和权限
由于服务器端应用是一种 PATCH
类型,因此主体(例如 Kubernetes RBAC 的 Role)需要**补丁**权限才能编辑现有资源,还需要**创建**动词权限才能使用服务器端应用创建新资源。
清除 managedFields
可以通过使用**补丁**(JSON 合并补丁、战略性合并补丁、JSON 补丁)或通过**更新**(HTTP PUT
)覆盖所有 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
部分中阅读更多信息。