书接上一篇Kubernetes之List参数使用不当引发的ETCD网络风暴说最近排查了一个因业务层使用List接口时因参数使用不当引起的etcd压力极速增长的问题, 该篇将按图索骥来看看ListOption在源码是如何处理的
处理逻辑
kube-apiserver
LIST
请求处理逻辑可以看到下图原图地址:
以上可以看到,系统路径中存在两级 List/ListWatch(但数据是同一份):
- apiserver List/ListWatch etcd
- 其它对象如controller/operator List/ListWatch apiserver
因此,从最简形式上来说,apiserver 就是挡在 etcd 前面的一个代理(proxy),
1 | +--------+ +---------------+ +------------+ |
对于List请求可归类为两种:
- apiserver直接从自己的缓存中读数据
- apiserver跳过缓存,直接从etcd读数据
还是以使用client-go中listJob为例, 常见写法:
1 | // ListJobs lists all jobs details. |
来看看ListOptions的struct, 这里因篇幅有限,所以将注释去除了
kubernetes based on v1.22
ListOptions
1 | // pkg/apis/meta/v1/types.go |
- LabelSelector/FieldSelector: 标签选择器与字段选择器, 上文提到, etcd只是KV存储,并不理解label/field这些信息,因此在etcd层面无法处理这些过滤条件。 所以如果是没走apiserver的缓存直接到达etcd的实际的过程是:apiserver 从 etcd 拉全量数据,然后在内存做过滤,再返回给客户端
- Watch及AllowWatchBookmarks: AllowWatchBookmarks也是个很有用的功能,但并不是本文的重点,并不展开说明
- ResourceVersion/ResourceVersionMatch: 资源版本
- Limit: 分页功能, 最常见的例子是kubectl命令行工具,会自动将请求加上limit=500这个查询参数, 可以使用kubectl –v=8看到
- Continue: 是个token,是否期望从server端返回最多的结果
这里主要会聚焦ResourceVersion
从上面可以看出ResousrceVersion的默认类型为string,所以默认值为空,来看看apiserver List()
操作源码分析
List()调用链
1 | store.List |
store
List()
1 | // staging/src/k8s.io/apiserver/pkg/registry/generic/registry/store.go |
ListPredicate()
1 | // staging/src/k8s.io/apiserver/pkg/registry/generic/registry/store.go |
ListPredicate()
中:
如果客户端没传 **
ListOption
**,则初始化一个默认值,其中的ResourceVersion
设置为空字符串;用 listoptions 中的字段分别初始化过滤器(SelectionPredicate)的 limit/continue 字段;
初始化返回结果,
list := e.NewListFunc()
;将 API 侧的 ListOption 转成底层存储的 ListOption;
p.MatchesSingle()判断请求中是否指定了metadata.name,如果指定了,说明是查询单个对象,因为
Name
是唯一的,接下来转入查询单个 object 的逻辑;如果p.MatchesSingle()不成立,则需要获取全量数据,然后在 apiserver 内存中根据 SelectionPredicate 中的过滤条件进行过滤,将最终结果返回给客户端;
最终e.Storage.GetToList()/e.Storage.List()
会执行到cacher。这两个funciont很相似
不管是获取单个 object,还是获取全量数据,都经历类似的过程:
- 优先从 apiserver 本地缓存获取(决定因素包括 ResourceVersion 等),
- 不得已才到 etcd 去获取;
apiserver cache
List()
1 | // staging/src/k8s.io/apiserver/pkg/storage/cacher/cacher.go |
shouldDelegateList()
判断是否必须从 etcd 读数据
1 | // staging/src/k8s.io/apiserver/pkg/storage/cacher/cacher.go |
这里非常重要:
客户端未设置 ListOption{} 中的
ResourceVersion
字段,对应到这里的resourceVersion == ""
客户端设置了
limit=500&resourceVersion=0
不会导致下次hasContinuation==true
,因为resourceVersion=0 将导致 limit 被忽略(hasLimit
那一行代码),也就是说, 虽然指定了 limit=500,但这个请求会返回全量数据。ResourceVersionMatch的作用是用来告诉 apiserver,该如何解读 ResourceVersion。官方的详细说明表格 ,有兴趣可以看看。
接下来再返回到 cacher 的 GetList()
逻辑,来看下具体有哪几种处理情况。
ListOption要求从 etcd 读数据
有两种情况:
- 当客户端要求必须从etcd读取数据时,适用于数据一致性要求极其高的场景
- 当apiserver缓存还没有创建好时,比如apiserver重启到ready这阶段
apiserver 会直接从 etcd 读取所有 objects 并过滤,然后返回给客户端, 适用于数据一致性要求极其高的场景。 当然,也容易误入这种场景造成 etcd 压力过大
这里将会从cache的GetList()
转到etcd的List()
etcd List()
1 | //staging/src/k8s.io/apiserver/pkg/storage/etcd3/store.go |
client.KV.Get()
就进入 etcd client 库了appendListItem()
会对拿到的数据进行过滤,这就是我们第一节提到的 apiserver 内存过滤操作。
apiserver使用本地缓存
1 | // staging/src/k8s.io/apiserver/pkg/storage/cacher/cacher.go |
总结建议
- List请求默认设置
ResourceVersion=0
以防etcd拉全量数据再过滤,导致,很慢或者扛不住 - 不要滥用List接口,如果能使用watch就使用watch
- 优先通过 label/field selector 在服务端做过滤
如果需要缓存某些资源并监听变动,那需要使用 ListWatch 机制,将数据拉到本地,业务逻辑根据需要自己从 local cache 过滤。 这是 client-go 的 ListWatch/informer 机制。 - 优先使用 namespaced API,
etcd 中 namespace 是前缀的一部分,因此能指定 namespace 过滤资源,速度比不是前缀的 selector 快很多,如果要 LIST 的资源在单个或少数几个 namespace,考虑使用 namespaced API:
- Namespaced API:
/api/v1/namespaces/<ns>/pods?query=xxx
- Un-namespaced API:
/api/v1/pods?query=xxx
- 细化etcd/apiserver等核心监控,比如作者之前不怎么关注的网络方面的metrics
- 经过上述的方法还是扛不住的话那可以把event等不是很重要的但很具有冲击对象的数据进行etcd分离
- 遇到性能问题是最容易出现多方的扯皮, 作者觉得最好的说服方式就是: 直接用数据说话,数据不会骗人
其它
本篇主要追了一下ResourceVersion是怎么处理的,但还有很多细节是没有展开的,比如
- ListOption中的其它参数都起到什么作用
- Reflector中ListandWatch关于ResourceVersion又是怎样的
- relist如何使用
但实在是不想篇幅太长,所以给自己留个作业,下回再探讨