代码版本:v1.26
前一篇已经介绍了 Informer 的实现,Informer 对 kube-apiserver 发起了 list 和 watch 请求。我们知道大规模集群下,kube-apiserver 会成为瓶颈,尤其在内存方面,相信很多人也遇到过 kube-apiserver OOM 等问题(碰巧的是最近线上连续出现两次 kube-apiserver OOM 的问题)。本篇主要讲 kube-apiserver 中 Informer 需要用到的两个接口 list 和 watch 的实现。
网上搜索的话,可以找到大量相关的源码解析的文章,这里我并不会去过多涉及代码,主要还是以讲原理、流程为主,最后简单介绍下当前存在的问题,理论实践相结合。本篇主要讲当前实现,只有了解了当前实现,明白了为什么会有问题,才知道如何去解决问题,接下来的一篇会详细分析如何解决这些问题。
在之前一篇 kubernetes 月光宝盒 - 时间倒流中我们已经介绍过 watch 的实现机制。
核心组件:Cacher,watchCache,cacheWatcher,reflector。其中 watchCache 作为 reflector 的 store,Etcd 作为 listerWatcher 的 storage,store 和 listerWatcher 作为参数用来构造 reflector。数据流大致如下:
用来缓存数据的核心结构是 watchCache,其内部又两个关键结构:cache(cyclic buffer),store(thread safe store),分别用来存储历史的 watchCacheEvent 和真实的资源对象,其中 store 里面存储的是全量对象,而 cache 虽然是自适应大小的,但还是有最大容量限制的,所以他存储的 watchCacheEvent 所代表的对象集合并不一定能覆盖 store 的全部数据。
kube-apiserver 在优化自身内存使用方面做了很多优化了,不过至今仍然存在一些尚未完全解决的问题。
kube-apiserver 的内存消耗,主要两个来源:
list 请求占用内存多的原因如下:
有两个常见的容易引起 kube-apiserver OOM 的场景:
严格说,这并不能算是一个问题,机制如此,理论上单机资源无限的情况下是可以避免这个现象的。为了方便描述,用 RV 代指 resourceversion。
其本质是客户端在调用 watch api 时携带非 0 的 RV,服务端在走到 cacher 的 watch 实现逻辑时需要根据传入的 RV 去 cyclic buffer 中二分查找大于 RV 的所有 watchCacheEvent 作为初始 event 返回给客户端。当 cyclic buffer 的最小 RV 还要比传入的 RV 大时,也就是说服务端缓存的事件的最小 RV 都要比客户端传过来的大,意味着缓存的历史事件不全,可能是因为事件较多,缓存大小有限,较老的 watchCacheEvent 已经被覆盖了。
客户端 Informer 遇到这个报错的话会退出 ListAndWatch,重新开始执行 LIstAndWatch,进而造成 kube-apiserver 内存增加甚至 OOM。问题本质原因:RV 是全局的。场景的景本质区别在于场景 1 是在一种资源中做了筛选导致的,场景 2 是多种资源类型之间的 RV 差异较大导致的。
经过上述分析,造成这个问题的原因有两个:
社区也已经在多个版本之前进行了优化来降低这个问题出现的概率。
针对问题一,采用了自适应窗口大小,虽然还是会有问题,但相比之前写死一个值出现问题的概率要小,同时在不必要的时候缩小长度,避免内存资源的浪费。
针对问题二,有两个优化,引入了 BOOKMARK 机制来优化同一种资源不同筛选条件导致的问题,BOOKMARK 是一种 event 类型,定期将最新的 RV 返回客户端;引入 ProgressNotify 解决多种资源类型 RV 差异较大,在 kube-apiserver 重启后,Informer resume 时导致的问题,本质是利用了 Etcd 的 clientv3 ProgressNotify 的机制,kube-apiserver 在 Watch Etcd 的时候携带了特定的 Options 开启此功能。ProgressNotify 参考 Etcd 官方文档:
WithProgressNotify makes watch server send periodic progress updates every 10 minutes when there is no incoming events. Progress updates have zero events in WatchResponse.
详情可以参考如下 KEP 956-watch-bookmark 和 1904-efficient-watch-resumption
这更是一个历史悠久的问题了,自从有了 watchCache 之后就有了这个问题,本质是将之前直接访问 Etcd 时的线性一致性读(Etcd 提供的能力),降级成了读 kube-apiserver cache 的顺序一致性。
pod-0
(uid 1) which is scheduled to node-1
pod-0
is deleted as part of a rolling upgradenode-1
sees that pod-0
is deleted and cleans it up, then deletes the pod in the apipod-0
(uid 2) which is assigned to node-2
node-2
sees that pod-0
has been scheduled to it and starts pod-0
node-1
crashes and restarts, then performs an initial list of pods scheduled to it against an API server in an HA setup (more than one API server) that is partitioned from the master (watch cache is arbitrarily delayed). The watch cache returns a list of pods from before T2node-1
fills its local cache with a list of pods from before T2node-1
starts pod-0
(uid 1) and node-2
is already running pod-0
(uid 2).详情可以参考 issue 59848。
我们经常看到各种源码分析,原理解析的文章,容易轻信其内容,但随着版本迭代,以及一些细节的处理,可能会导致我们理解不到位,或者并不能真正的掌握。例如是否在 list 请求时传 RV=0 就一定会走 kube-apiserver 的缓存?网上搜的话,应该都是说会走,但看代码你会发现并不是这样,例如当 kube-apiserver 重启后数据还没有完全加载好的时候,遇到 list 带了 RV=0 的请求会直接去访问 Etcd 获取数据。看似不起眼的细节,可能会影响我们处理问题的思路,比如 Etcd 负载较高要查原因,如果你知道这个细节的话,就会有意识的去看所有的 list 请求,而不只是那些 RV != 0 的请求。
最后留一个思考,kube-apiserver 的内存压力主要来自 list 请求,那么我们是否可以不使用 list 请求而是使用一种流式处理来实现 list 的功能呢?这样是不是就可以把内存消耗限制在一个常数的空间复杂度范围内了?下一篇将会专门分析使用流式 api 解决 list 导致的内存暴涨的问题,敬请期待~