eBay Cloud Network团队解决Kubernetes中CPU过载问题的案例分享

文摘   2023-09-22 11:00   上海  






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


Unset

10-->20-->40 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)。


Unset

func (p *TLBProvider) lockAllocationForPods(pods []v1.Pod, service *v1.Service) error {

     if pods == nil {

             return nil

      }

      for _, pod := range pods {

         // Check if allocation already exists for this pod

         labelSelector := labels.Set{

           PodNodeNameLabel:  pod.Spec.NodeName,

           PodNamespaceLabel: pod.Namespace,

           PodNameLabel:      pod.Name,

           PodUIDLabel:       string(pod.UID),

         }.AsSelector()


         allocations, err := p.allocationLister.List(labelSelector)



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()函数的源代码。


Unset

func ListAll(store Store, selector labels.Selector, appendFn AppendFunc) error {

      selectAll := selector.Empty()

      for _, m := range store.List() {

          if selectAll {

               // Avoid computing labels of the objects to speed up common flows

              // of listing all objects.

              appendFn(m)

              continue

           }

           metadata, err := meta.Accessor(m)

           if err != nil {

               return err

           }

           if selector.Matches(labels.Set(metadata.GetLabels())) {

               appendFn(m)

            }

         }

         return nil

}


从上面的源码我们可以看出,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 发送请求。



Unset

// Store is a generic object storage interface. Reflector knows how to watch a server

// and update a store. A generic store is provided, which allows Reflector to be used

// as a local caching system, and an LRU store, which allows Reflector to work like a

// queue of items yet to be processed.

//

// Store makes no assumptions about stored object identity; it is the responsibility

// of a Store implementation to provide a mechanism to correctly key objects and to

// define the contract for obtaining objects by some arbitrary key type.

type Store interface {

        Add(obj interface{}) error

        Update(obj interface{}) error

        Delete(obj interface{}) error

        List() []interface{}

        ListKeys() []string

        Get(obj interface{}) (item interface{}, exists bool, err error)

        GetByKey(key string) (item interface{}, exists bool, err error)


         // Replace will delete the contents of the store, using instead the

         // given list. Store takes ownership of the list, you should not reference

         // it after calling this function.

        Replace([]interface{}, string) error

        Resync() error

}


分析CPU过载原因


从前面的源码中,我们可以看到ListAll()的主要工作是从 Store中利用golang的for循环在逐一匹配对象是否特定的label,为什么会是过载的主要原因呢,笔者为此计算了一下当前对应的k8s上的allocation对象的数量。



Unset

(base)  ~/ kubectl get allocation -A | wc -l

  145457  


原来有约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例子



Unset

// arbitrary unique name for the new indexer

const ByIP = "IndexByIP"

func podIPIndexFunc(obj interface{}) ([]string, error) {

    pod, ok := obj.(*v1.Pod)

    if !ok {

        return nil, fmt.Errorf("object is not a Pod")

    }


    // Extract the IP addresses from the Pod and return them as a list of strings.

    var ipList []string

    for _, ip := range pod.Status.PodIPs {

        ipList = append(ipList, ip.IP)

    }

    return ipList, nil

}


给informeer添加indexer:


Unset

podsInformer.AddIndexers(map[string]cache.IndexFunc{ByIP: podIPIndexFunc})


通过给pod的IP来获取pod


Unset

items, err := podsInformer.GetIndexer().ByIndex(ByIP, ip)


实现IpamIndexByIPFunc


我仿照上面的方式实现了IpamIndexByIPFunc,并配置了相应的informer。


Unset

const (

         IpamIndexByIP        = "IpamIndexByIP"

         Slash32SubnetSize = 32

)


func IpamIndexByIPFunc(obj interface{}) ([]string, error) {

          alloc, ok := obj.(*ipamv1.Allocation)

          if !ok {

                 return nil, fmt.Errorf("object is not a Allocation")

          }


          // Extract the IP addresses from the ipamv1.Allocation and return them as a list of strings.

          var ipList []string

          for _, subNet := range alloc.Status.Subnets {

                  if subNet != "" {

                       ip, ipNet, err := net.ParseCIDR(subNet)

                       if err != nil {

return nil, fmt.Errorf("failed to parse subnet %s: %s", subNet, err.Error())

}

                        subnetSize, _ := ipNet.Mask.Size()

                        if subnetSize == Slash32SubnetSize {

                               ipList = append(ipList, ip.String())

                        }

                    }

            }

            return ipList, nil

}


最后改变lockAllocationForPods()即可


Unset

func (p *TLBProvider) lockAllocationForPods(pods []v1.Pod, service *v1.Service) error {

                       if pods == nil {

                              return nil

                       }

                       for _, pod := range pods {

alloc:=allocationInformer.Informer().GetIndexer().ByIndex(common.IpamIndexByIP, pod.Status.PodIP)


上述方案需要注意的是,通过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/



eBay技术荟
eBay技术荟,与你分享最卓越的技术,最前沿的讯息,最多元的文化。
 最新文章