01
前 序
Kubernetes是目前最流行的容器编排工具之一。它具备丰富的功能,用于管理和部署容器应用程序。在Kubernetes中,使用client-go来与Kubernetes API进行交互。client-go提供了简单易用的API,帮助用户轻松操作Kubernetes集群。然而,在使用client-go时,需要特别注意性能方面的问题。具体而言,client-go中的cache ListAll() 函数可能会引发CPU密集型操作。本文将分享eBay Cloud Network团队在实践过程中遇到的Kubernetes中处理CPU过载的案例,以便给同行提供有益的思考和指导。
eBay Cloud Network团队日常负责监控和维护各种Kubernetes控制器和服务的健康状态,并在出现故障、性能问题或安全问题时迅速响应并采取必要的措施。在最近的值班中,作者接到关于公司内部 tlb-service-controller 的告警电话,经快速调查后发现该问题源于CPU过载,导致了tlb-service-controller的重启。以下是对问题的排查过程和解决方案的记录。
问题排查
02
以下记录了我们对CPU过载问题的排查过程。
收到告警电话,扩容CPU
当我们接到告警电话时,发现当前 tlb-service-controller 的 CPU 限制设置为 10。最初认为是由于 controller 需要处理的对象太多,导致其无法正常工作。为了暂时解决问题,我们将 CPU 限制从 10 增加到 20。然而,即使增加到 20,依然存在CPU过载的情况。于是我们将 CPU 再次增加到 40,这样 tlb-service-controller 的 CPU 使用率稳定下来,上下游用户暂时不受影响。接下来,我们将查找导致当前问题的原因。
上图是在Prometheus上查看的当前tlb-service-controller在CPU扩容后的CPU和内存分布图。CPU使用率在短短的10分钟内迅速上升至约33。这表明,CPU使用率可能出现了异常。需要进一步调查才能确定原因。
pprof 排查CPU 使用率罪魁祸首
pprof 是一款 Golang 性能剖析工具,可用于分析应用程序的 CPU 和内存占用率等性能问题。pprof 可以在应用程序运行时收集性能数据,然后使用可视化工具进行简单的分析和展示。下面是使用 pprof 对当前 tlb-service-controller 的 CPU 使用率采样生成的分析图。
从上面的图中,我们可以看到,罪魁祸首是lockAllocationForPods()和client-go中cache的ListAll()函数。通过查看源代码,我们发现该函数通过client-go提供的标签选择器(labelSelector)功能,为每一个pod创建了一个对应的label selector, 用来寻找对应的allocation(读者可以忽略"allocation"的具体含义,它是与IP绑定的资源,每个pod应该对应一个IP)。
client-go 中的cache提供的源码分析
client-go 是 Kubernetes 官方提供的 Go 客户端,它提供了丰富的功能,用于操作 Kubernetes API。标签选择器是 client-go 中的一个重要功能,它可以用于通过标签选择器来过滤 Kubernetes API 响应的对象。client-go 的 cache 是基于 store 实现的。store 提供对 Kubernetes API 对象的访问和操作的统一接口。cache 依赖于 store 来获取 Kubernetes API 对象。
client-go的cache为什么会导致ListAll()函数的 CPU过载?为了解决这个问题,我们决定查看pprof提示的ListAll()函数的源代码。
从上面的源码我们可以看出,ListAll()其实是client-go从自己的Store中,使用给定的selector筛选对象,并将它们附加到给定的列表(appendFn)中。该函数通过遍历Store中的所有对象,对每个对象执行以下操作:
如果给定的selector为空,则跳过标签匹配操作并将对象附加到列表中
否则,通过元数据访问器(meta.Accessor),获取对象的元数据,然后将对象的标签转换为标签集,并将该集合与selector进行匹配。如果对象的标签匹配给定的selector,则添加该对象到列表中。
client-go 的 Store 是 client-go 提供的一种抽象的对象缓存机制。它提供了对 Kubernetes API 对象的访问和操作的统一接口。Store 依赖于 store 接口来实现,store 接口定义了 Store 的基本操作,如 List()、Get()、Update() 和 Delete() 等。Store 可以用于存储 Kubernetes API 对象的状态。应用程序可以使用 Store 来获取 Kubernetes API 对象,并监听对象状态的更改。Store 可以提高应用程序的性能,避免频繁地向 Kubernetes API Server 发送请求。
分析CPU过载原因
从前面的源码中,我们可以看到ListAll()的主要工作是从 Store中利用golang的for循环在逐一匹配对象是否特定的label,为什么会是过载的主要原因呢,笔者为此计算了一下当前对应的k8s上的allocation对象的数量。
原来有约14.5万个allocation对象。根据前面的代码,对于每个pod,都需要遍历这14.5万个allocation对象。假设每个K8s的service下有100个pod,而每个cluster仅有10个这样的K8s service,操作次数将达到1.45亿次。这些操作都需要在CPU中执行,直接使得该函数成为CPU密集型操作,进而导致了CPU限制。更复杂的情况是,每个cluster的service数量远远超过10个,在真实的环境中更加严重。
03
解决过程
通过比较Pod和allocation对象,我发现它们之间有一个交叉字段,即IP。因此,可以通过IP将这两个对象关联起来。由于 client-go提供了 添加了自定义 AddIndexers的功能,具体可查看An introduction to Go Kubernetes' informers 我们可以通过自定义Indexers来加快访问速度,比如下面是一个通过pod的IP而不是name和nameSpace来获取pod的例子。
Pod AddIndexers例子
给informeer添加indexer:
通过给pod的IP来获取pod
实现IpamIndexByIPFunc
我仿照上面的方式实现了IpamIndexByIPFunc,并配置了相应的informer。
最后改变lockAllocationForPods()即可
上述方案需要注意的是,通过AddIndexers添加索引可能会带来一定的内存消耗。每个索引都需要占用一定的内存空间来存储索引数据结构。索引的内存消耗随着索引的数量、索引字段的数量和索引数据量的增加而增加。在决定是否使用索引时,应该权衡查询性能的提升和额外内存消耗之间的关系,以确保整体系统的性能和可用性。
最终效果对比
上图是使用了上面的AddIndexers方法后,tlb-service-controller的CPU使用率。通过对比,使用AddIndexers方法后,tlb-service-controller的CPU使用率已经稳定在了1~4左右,和之前的40相比的话,性能提升了10倍以上。这验证了我们的IpamIndexByIP修复是正确的。同时我们看到内存使用率的没有明显变化,这证明我们通过AddIndexers来通过空间换时间的方法,并不会带来额外的内存消耗。
总 结
04
本文详细介绍了Kubernetes中处理CPU过载的问题排查和解决过程,包括 CPU 扩容、分析 client-go 的 ListAll() 函数以及解决方案的实施。通过自定义索引器。团队成功提高了性能,将 CPU 使用率从高峰值 40 降低到稳定的 1~4 左右,同时没有带来额外的内存消耗。这个案例为类似问题的处理提供了有价值的参考。
参考文章:
client-go:Indexer 源码分析
An introduction to Go Kubernetes' informers
client-go/tools/cache/index_test.go
https://githclient-go/tools/cache/shared_informer.go
k8s client-go源码分析 informer源码分析(1)-概要
https://cloudnative.to/blog/client-go-informer-source-code/