k8s list机制与resourceVersion语义
K8s 架构:环形层次视图
对于 K8s 集群,从内到外的几个组件和功能:
-
etcd:持久化 KV 存储,集群资源(pods/services/networkpolicies/…)的唯一的权威数据(状态)源;
-
apiserver:从 etcd 读取(ListWatch)全量数据,并缓存在内存中;无状态服务,可水平扩展;
-
各种基础服务(e.g. kubelet、-agent、-operator):连接apiserver,获取(List/ListWatch)各自需要的数据;
-
集群内的 workloads:在 1 和 2 正常的情况下由 3 来创建、管理和 reconcile,例如 kubelet 创建 pod、cilium 配置网络和安全策略。
1、k8s list机制
kube-apiserver LIST 请求处理逻辑:
k8s在两级 List/ListWatch(但数据是同一份):
-
1、apiserver List/ListWatch etcd;
-
2、基础服务 List/ListWatch apiserver;
apiserver 就是挡在 etcd 前面的一个代理(proxy)
绝大部分情况下,kube-apiserver 都会直接从本地缓存提供服务(因为它缓存了etcd全量数据),某些特殊情况,apiserver 就只能将请求转发给 etcd,例如:
-
1、客户端明确要求从 etcd 读数据,追求最高的数据准确性,客户端 LIST 参数设置不当也可能会走到这个逻辑;
-
2、apiserver 本地缓存还没建好
如何判断是否必须从 etcd 读数据:
//https://github.com/kubernetes/kubernetes/blob/v1.24.0/staging/src/k8s.io/apiserver/pkg/storage/cacher/cacher.go#L591
func shouldDelegateList(opts storage.ListOptions) bool {
resourceVersion := opts.ResourceVersion
pred := opts.Predicate
pagingEnabled := DefaultFeatureGate.Enabled(features.APIListChunking) // 默认是启用的
hasContinuation := pagingEnabled && len(pred.Continue) > 0 // Continue 是个 token
hasLimit := pagingEnabled && pred.Limit > 0 && resourceVersion != "0" // 只有在 resourceVersion != "0" 的情况下,hasLimit 才有可能为 true
// 1. 如果未指定 resourceVersion,从底层存储(etcd)拉去数据;
// 2. 如果有 continuation,也从底层存储拉数据;
// 3. 只有 resourceVersion != "0" 时,才会将 limit 传给底层存储(etcd),因为 watch cache 不支持 continuation
return resourceVersion == "" || hasContinuation || hasLimit || opts.ResourceVersionMatch == metav1.ResourceVersionMatchExact
}
-
客户端未设置 ListOption{} 中的 ResourceVersion 字段,会对应到这里的 resourceVersion == ""从而导致从 etcd 拉全量数据;
-
客户端设置了 limit=500&resourceVersion=0 不会导致下次 hasContinuation==true,因为resourceVersion=0 将导致 limit 被忽略仍会返回全量数据,因为resourceVersion=0就不会去拉取etcd了
-
因此,未指定resourceVersion、resourceVersionMatch=exact(即同时resourceVersion=非零值)、有limit/continue都会直接从etcd读数据。
2、resourceVersion语义
参考链接:官方文档
etcd默认保留5分钟以内的变更记录,每个资源发生变更都会更新一个更大的资源版本ResourceVersion,ResourceVersion是一个所有资源类型共享的全局变量。
-
对于watch请求来说,你可以指定一个resourceVersion=0来获取5分钟以内的任意变更记录及其之后,这种表现很奇怪,所以不建议指定0。可以指定一个resourceVersion来获取这个资源版本之后的变更记录,但这个资源版本早于5分钟以内保留的最小版本,则会回复一个410状态码,如果大于最大版本,则可能会一直等下去,直到超时。
-
对于list,请求后会返回一个Kind=XXList的资源类型,XXList这种资源类型是按照惯例附带创建的,比如Pod和PodList;其中items字段内包含资源列表,metadata包含的了resourceVersion,但这个resourceVersion是PodList的资源版本,而不是Pod的资源版本,指定resourceVersion=0来获取任意的PodList,也可以指定一个resourceVersion来获取这个资源版本或之后的PodList,如果指定的resourceVersion小于当前最新资源版本,它总是返回最新的PodList,如果大于则返回504状态码。
但如果你指定了limit参数或resourceVersionMatch=Excat,就意味着apiserver必须精准匹配你填写的resourceVersion,这时候就和watch一样了,如果找不到指定的resourceVersion(可能是超过了5分钟),则会返回410状态码。
-
变更事件有四种:ADD, DELETE, MODIFY, BOOKMARK。前面三个容易看懂,但第四个BOOKMARK是干什么的?正如前面所说etcd只保留5分钟的变更记录,万一客户端很长时间内都没有watch到变更,然后断连之后又重连到apiserver时,客户端可能按常规的把上次收到的resourceVersion传到url里,但这个resourceVersion已经是一个过期的资源版本,apiserver找不到资源版本,就会回复一个410状态码。那么这时客户端为了能获取最新的资源版本号就不得不先list一次。为了防止这种情况,apiserver会定期发送BOOKMARK事件,BOOKMARK将包含一个当前最新的资源版本号,尽管这个版本号对应的资源类型并不是你监听的那种,但这样是为了客户端能更新最新的资源版本号。
resourceVersion与resourceVersionMatch:
get、list 和 watch 操作支持 resourceVersion 参数。 从 v1.19 版本开始,Kubernetes API 服务器支持 list 请求的 resourceVersionMatch 参数。
API 服务器根据你请求的操作和 resourceVersion 的值对 resourceVersion 参数进行不同的解释。 如果你设置 resourceVersionMatch 那么这也会影响匹配发生的方式。