Client-go 的 UpdateStatus 接口你用对了吗?
最近发现一个问题,发现使用 client-go 并行调用 UpdateStatus 接口与 PATCH 更新 annotation 会出现覆盖写的问题,这与之前的理解不一致,这里做一个记录。
先说结论:client-go 的 UpdateStatus 会 PUT 更新 Pod 的 Status 字段与 metadata 字段。
问题现象
使用 client-go 在两个协程并行调用 UpdateStatus 接口与 PATCH 接口更新 annotation,发现 annotation 更新失效。
问题分析
通过打印 client-go 的 response body 信息(klog 的 flag 设置 v=8 以上),发现两个协程先调用 PATCH 接口更新 annotation 成功,资源的 ResourceVersion 为1,但紧接着马上发出了一个 PUT status 的接口,资源的 ResourceVersion 更新为2,此时的 annotation 为老版本的 annotation,导致 PATCH 接口的更新失效了。
问题根因
client-go 的 UpdateStatus 接口本质为调用 APIServer 的 PUT {resource}/status
接口,按照官方 API 文档的说法,Update 接口不会更新 status,所以额外提供了 PUT {resource}/status
接口。
The PUT and POST verbs on objects MUST ignore the “status” values, to avoid accidentally overwriting the status in read-modify-write scenarios. A
/status
subresource MUST be provided to enable system components to update statuses of resources they manage.
查看 APIServer 代码的具体实现,
pkg/registry/core/pod/storage/storage.go 为初始化 Pod 相关接口的 handler
1 | func NewStorage(optsGetter generic.RESTOptionsGetter, k client.ConnectionInfoGetter, proxyTransport http.RoundTripper, podDisruptionBudgetClient policyclient.PodDisruptionBudgetsGetter) (PodStorage, error) { |
而 StatusUpdate 接口与普通 Update 接口的区别主要是 statusStore.UpdateStrategy = registrypod.StatusStrategy
。
深入观察两种策略的不同,发现 Update 使用的 registrypod.Strategy
会使用老 Pod 的 Status。
1 | // PrepareForUpdate clears fields that are not allowed to be set by end users on update. |
StatusUpdate 接口使用的 registrypod.StatusStrategy
策略会使用老 Pod 的 Spec。
1 | func (podStatusStrategy) PrepareForUpdate(ctx context.Context, obj, old runtime.Object) { |
而出问题的 annotation 即不属于 Spec,也不属于 Status,故两个操作会产生协程冲突。
解决方案
将 annotation 更新的 Pod 接口与 UpdateStatus 接口整合。