今天又踩一坑。
我们开发了有个k8s controller,是基于”Knative/pkg“这个框架的(吐槽下google这帮人,就是不用kubebuild), 其中有一个功能是是指定reconcile的线程数,也就是说有多少个线程来运行reconcile loop。因为我们这个controller 要求吞吐量大,所以定了个比较大的数:100.
然后测试,同时并发的创建1000个CR,结果是吞吐量一直上不去,查了查发现奇怪的事情:
- Workqueue中排队request一直稳定在900左右
- 在Controller末尾client.UpdateStatus 这个仅仅对CR的status做更新的操作稳定延迟了十多秒
这太扯了,UpdateStatus就是发个Rest request到k8s-apiserver改个东西而已嘛,为啥卡个十几秒,也没干啥啊。
第一个怀疑是api-server扛不住,很快就否定了,这可是kubernetes的api-server啊,区区一千QPS,应该不在话下。
另一个证据是在我们controller延迟的时候,执行kubectl命令并不受影响。
否定以后开始怀疑是不是api-server对当个client限制了流量(throttle), 查了查也没查到啥信息,折腾了好几个小时。
后来忽然怀疑到发送request的client上了,莫名的觉得有戏,查了查代码,使用的client是使用client-go搞出来的,
k8s.io/client-go/rest.RESTClientFor(rest.Config)
而这个rest.config的结构中有两个参数是这样的:
// Maximum burst for throttle.
// If it's zero, the created RESTClient will use DefaultBurst: 10.
Burst int
// Rate limiter for limiting connections to the master from this client. If present overwrites QPS/Burst
RateLimiter flowcontrol.RateLimiter
看看,知识的盲区啊,以前开kubelet的配置是看到过这两参数的,换成controller愣是没想起来,话说kubelet和controller对api-server有啥区别,不都是client吗?
统统改成100,好了,也不卡了,workqueue深度也为0了。
趁热补了补QPS和burst的知识,在web server领域,对于限流是有两种方案:
漏桶算法(leaky bucket)
这个不想多说,就贴张图:这种方法保证处理(漏出)的速率恒定,如果收到request过多,超过了暂存的数量(桶的容量),那多出来的request就被丢弃了。
令牌桶(token bucket)
这是client-go中client使用的算法
一个request消耗掉一个令牌,令牌桶的初始容量是可配置的(burst),当令牌用完了新来的request就得排队,这就是我request卡住的原因。
稍微解释下这个算法,比如我QPS设置为100, burst也设置为100, 那我的系统就会以没10ms一个的速度往令牌东中发令牌,而令牌桶的初始令牌数为100.
如果没啥request,令牌桶就是满的,但是还是以10ms一个的速度放,放不进去的就被丢掉了,这是忽然来个小高潮,一秒来了200个request,QPS可是100哦,想想,如果是漏铜算法,那对不起,200个处理不了,多余的100个扔掉。但如果是令牌桶,因为桶里本身有100个,在加上QPS也是100,那这200个就处理了,但是呢,后续再来200/sec就搞不定了,因为令牌桶的存货没了!
参考:
https://amsimple.com/blog/article/89.html
https://github.com/voyagermesh/voyager/issues/640
https://pkg.go.dev/k8s.io/client-go/rest#Config
https://studygolang.com/articles/10148



