eBay的私有云平台(Tess)基于Kubernetes搭建。上百个Kubernetes集群支持着多个eBay核心业务。为了统一管理,eBay Cloud团队引入了集群联邦(Federation),通过集群注册(Cluster Registry)和自研的同步控制器(Sync Controller),实现多集群中对象状态的统一管理。在本文中,eBay Cloud团队将介绍一个Kubernetes Controller性能瓶颈问题的排查思路,更能帮助相关从业者快速理解Kubernetes核心组件之间的运行机制。
Part01.
背景介绍 情景揭幕
—Introduce Background—
eBay的每一个应用程序(Application),都允许有多个应用程序实例(ApplicationInstance)存在。ApplicationInstance是eBay自定义的CRD对象,可以关联到不同的workload上,如下图所示:
eBay的私有云平台引入了集群联邦(Federation Control Plane,下文简称fcp)用来管理多个成员集群(下文简称member cluster),那么我们希望ApplicationInstance这种对象是可以“一次创建、随处使用”的,即发起一次对着fcp的创建请求、对象可以自动同步到所有的member cluster。这样用户就不用在多个的集群上执行同一个对象的创建操作了。为此,eBay专门研发了同步控制器来实现全局对象在不同集群中的状态同步,同步控制器支持两种同步模式:
从fcp到member cluster的同步 - 大部分的场景是属于这种情况,可以理解为“正向同步”
从member cluster到fcp的同步 - 少部分的场景属于这种情况,可以理解为“反向同步”。反向同步只负责把member cluster上新创建的ApplicationInstance同步到fcp,接下来fcp继续完成该对象到其他member cluster上的同步。
“正向同步”与“反向同步”的工作原理如下图所示:
Part02.
发现问题 疑点重重
—Discover Issues —
事件的起因是eBay的云平台用户发现数起自动化创建workload的任务失败。
该自动化任务里包括很多Kubernetes资源的创建,并且有一定的依赖顺序。例如先创建Namespace,然后是ApplicationInstance, PVC等,接下来是Deployment/Statefulset/Pod等实际运行业务的容器。所有失败任务的报错都是Pod启动失败,报错Pod spec指定的ServiceAccount不存在。
ServiceAccount在eBay的云平台里面是被自动托管的,跟ApplicationInstance一一对应,随着ApplicationInstance的创建而自动创建。所以ServiceAccount不存在意味着ApplicationInstance不存在。在前面创建ApplicationInstance的步骤中,创建请求收到了201 response,但这个创建请求是对着fcp发起的,后续pod是运行在member cluster上的,问题很自然的就怀疑到了同步控制器从fcp到member cluster上有同步延迟。让我们比较下ApplicationInstance在fcp和member cluster上的创建时间,果然发现了有将近1分钟的延迟:
涉及到高延时的问题,团队的第一反应是网络层面或者Kubernetes控制面出现了异常,但此时我们并没有收到任何这方面的告警。尝试手工复现问题,对于ApplicationInstance的同步延迟可以稳定复现,但其他资源的同步(例如application,account,group等)几乎都是在1s内完成的。同一个控制器,同一份代码,到底是什么原因导致了只有ApplicationInstance这一种对象的同步延迟呢?
Part03.
定位问题 拨开迷雾
—Identify Issues —
首先,团队查看了同步控制器相关的metrics情况,发现同步控制器的工作队列中有1000+针对ApplicationInstance对象的待处理事件:
让我们来简单回顾下控制器的工作原理[1]:
同步控制器watch了ApplicationInstance对象的CREATE,UPDATE和DELETE事件,这些事件都被放入工作队列。控制器的worker是工作队列的消费者,从工作队列中取出事件进行处理。对于本文讲述的同步控制器而言,处理逻辑就是往其他member cluster进行状态同步,直到该对象在所有member cluster的状态都与fcp的状态一致。
大量的event堆积意味着控制器工作队列的生产速率和消费速率不匹配。要么是生产过快,要么是消费过慢。按照这个思路,我们开始着手调查根因。
怀疑一:ApplicationInstance变化速度太快,无法及时处理
在eBay的一个fcp中,一共有将近80,000个ApplicationInstance对象。通过Kubernetes audit log来看下ApplicationInstance对象的CRUD频率:
上图可以看到,ApplicationInstance对象存在大量的PATCH请求,这些请求有的来自于同步控制器本身,也有来自于其他的一些自定义控制器。比如:
ApplicationInstance status controller - 该控制器的作用是不停地搜集ApplicationInstance的状态信息,例如关联了几个Pod,分别在哪些cluster等,并把这些信息实时地更新到ApplicationInstance对象的status字段下。
ApplicationInstance adoption controller - 该控制器的作用是不停地判断该ApplicationInstance是否达到了go live的要求,只有满足一定要求的ApplicationInstance才可以承接线上流量,这些要求包括配置了metric,PDB(Pod Disruption Budget)等。
通过分析这些发出大量PATCH请求控制器逻辑,发现PATCH的内容基本上都是status字段的变动,很少涉及到spec下的字段变动。同步控制器在往member cluster同步ApplicationInstance的时候,会跳过status字段的信息,只比较spec字段下的信息。只要spec字段一致,同步控制器的worker就直接完成处理。另外,spec字段的比较都是基于控制器的的本地缓存,所以毫秒级别就会结束,理论上不会造成大量的事件堆积在工作队列中,所以排除了该猜想。
怀疑二:控制器发出的请求被限流
根据workqueue_work_duration_seconds这个metrics看到,控制器当前99%的处理时间在10秒左右,所以该猜想怀疑控制器发出的create/patch/delete等请求被限流。
通过排查代码发现,同步控制器是通过读取一个kubeconfig初始化的Kubernetes client,利用了client-go library,如果是client被限流,默认情况下等待超过1s就会打印出相关日志[2]。但我们在控制器的日志中并没有看到任何throttling相关的输出,所以也排除了这种原因。
怀疑三:API Server过载
进一步去查看控制器的日志,发现了一些错误日志:
这个错误发生在同步控制器的“反向同步”逻辑中,意味着这个ApplicationInstance(Name: testai, Namespace: e2e-staging)在member cluster中首先被创建出来,需要被同步到fcp上。但由于ApplicationInstance是一个Namespace scope的对象,对应的Namespace在fcp上不存在,导致输出了这个错误。再看下fcp API Server Pod的日志,发现fcp API server 花费了将近10秒才完成一个CREATE请求:
联想到API Server有--max-requests-inflight 和 --max-mutating-requests-inflight 两个参数限制并发请求,防止过量请求导致 API 服务器崩溃。会不会此时有其他的大量请求到达API Server而导致了CREATE请求被限流呢?查看相关request-inflight的metrics,发现还远远未到设置的阈值,所以这个猜想也被排除。
怀疑四:API Server优先级与公平性导致限流
虽然API Server整体没有过载,但eBay所有集群的API Server都启动了优先级与公平性(Priority and Fairness)特性[3],所有来自于同步控制器的请求会落到同一优先级。会不会是只针对该优先级被QPS限流了呢?我们查看了APF相关的metric:
apiserver_flowcontrol_request_wait_duration_seconds 这个metric记录的是请求在APF队列中的等待时间(纵轴是ms的单位),可以看到此时并没有大量的等待延迟,该疑点也被排除。
怀疑五:准入控制延迟
该猜想是怀疑ApplicationInstance的创建请求在经过某些准入控制的时候导致延迟。让我们先回顾下请求在API Server端处理的顺序[4]:
可以看到准入控制器会在请求经过认证和授权后,先通过变更准入控制器(Mutation Admission),再进入验证准入控制器(Validating Admission)。Kubernetes一些内置的Admission与kube API Server构建在一起,通过一些参数进行禁启用。其中有两个准入控制器比较特殊,分别是 MutatingAdmissionWebhook 和 ValidatingAdmissionWebhook,它们提供了一种可扩展的方式来动态配置准入webhook, 通过 MutatingAdmissionWebhook 和 ValidatingAdmissionWebhook 来动态配置准入webhook的回调信息。
eBay也是采用了这种扩展方式,独立开发了一些准入插件,通过运行时的Webhook动态配置。有没有可能是某个准入控制碰到了bug导致了延迟呢?
沿着这个思路,继续查看apiserver_admission_webhook_admission_duration_seconds metric,这个metric统计了所有准入控制处理请求所花费的时间,并未发现明显异常:
但除了eBay动态配置的准入插件,Kubernetes还有一部分内置的准入控制器,它们直接编译进了APIserver的可执行文件。当查看这部分准入控制器的 metrics的时候,发现了异常:
NamespaceLifecycle这个Admission的时间处理时间稳定在2.5s左右。2.5秒跟上文观察到的10s虽然不匹配,但是经过查看源代码发现,该metric是一个Histogram类型的metric,对应的bucket的最大范围就是2.5s [5],也就是说大于2.5s的处理时间全部都放在了2.5s对应的bucket里面。
让我们研究下NamespaceLifecycle这个准入控制器,它主要有2个作用:
禁止terminating状态下的 Namespace 中创建新对象
拒绝不存在的 Namespace 的创建请求
第二个目的恰好是我们的场景,ApplicationInstance就是一个Namespace范围的对象,当同步控制器想要往fcp API Server发起创建请求的时候,对应的Namespace在fcp的API Server中不存在。那么这个Admission是如何判断Namespace是否存在呢?我们来查看下源代码[6]:
发现该Admission会先从本地的缓存中尝试读取Namespace,如果不存在最终会发起一个GET请求到API Server。这个GET请求的client端在初始化的时候,并没有指定QPS的参数,继而使用了默认的QPS - 5。当大量的这种对应Namespace不存在的创建对象请求过来时,NamespaceLifecycle Admission中校验Namespace是否存在的请求很快达到QPS上限,进而导致了一个CREATE请求就会有将近10s的延迟。10s后,同步控制器最终收到来自API Server的error response。由于控制器会不停地reconcile,这个event被再次放入工作队列中,等待下次worker的处理。如此往复循环,导致大量的worker被占用,其他进入工作队列的事件没有空闲的worker处理,只能慢慢等待,最终导致了正向同步的高延迟情况。
为了验证上述猜想是否正确,我们快速删除了几十个需要被反向同步的ApplicationInstance对象,目的是减少一些reconcile的循环次数。即刻看到了一些metrics的下降,包括事件在同步控制器中的等待时间,以及平均的处理时间:
Part04.
解决问题 寻找出路
—Find a Solution —
至此,终于定位到高延迟问题的根因是:大量请求导致准入控制的请求被QPS限流。为了彻底解决问题,我们采取了以下措施:
优化控制器的代码,忽略掉Namespace不存在情况下的ApplicationInstance反向同步,避免无休止地循环产生大量不合法的API请求。
增大准入控制使用的Kubernetes客户端的QPS。通过分析该客户端的请求类型、请求频率,结合API Server端目前的限流策略,在保证API Server可用性的前提下适当增加了QPS配置,避免准入控制成为性能瓶颈。
建立API Server响应请求的监控和告警,设置了服务水平指标(Service Level Indicator),保证任何高延时的异常请求都能被及时发现。
Part05.
总结反思 整装待发
—Reflect and Summarize —
回顾整个事件的排查过程,充满了曲折。虽然每一个疑点都是一次技术上的探寻,但同时也在思考:有什么办法可以不这么曲折地定位到根因吗?笔者认为是有的,总结以下几点反思分享给大家:
善用metric指标
如果我们在整个调用链路中都有metric埋点,那么在排查问题时就像拥有了指路明灯,帮助快速定位问题。Kubernetes控制面的各个组件原生都有很多metric,应当充分了解并设置合理的监控和告警。控制器的设计遵循“最小化原则”
最小化原则指软件的设计和开发需要尽可能地保持精简。例如在本案例中,ApplicationInstance对象的status字段不在控制器的同步逻辑管理范围内,那么status字段的变动其实不需要进入到控制器的工作队列,造成没有必要的调和循环,挤占worker的资源。合理设计QPS
QPS就像是一把双刃剑,既可以保护服务不过载,也可能造成单点瓶颈。但我们的日常开发过程中,经常会忽略QPS的配置,也经常在上线前缺失一些压力测试。出现瓶颈再去优化不是好的做法,应该在上线前就充分了解并评估业务需要的请求频率和并发度。用合理的QPS提升系统性能、稳定性,保证可靠的用户体验。
参考文献
https://github.com/kubernetes/sample-controller/blob/master/docs/images/client-go-controller-interaction.jpeg
https://github.com/kubernetes/kubernetes/blob/master/staging/src/k8s.io/client-go/rest/request.go#L607-L639
https://kubernetes.io/docs/concepts/cluster-administration/flow-control/
https://kubernetes.io/blog/2019/03/21/a-guide-to-kubernetes-admission-controllers/
https://github.com/kubernetes/kubernetes/blob/master/staging/src/k8s.io/apiserver/pkg/admission/metrics/metrics.go#L168
https://github.com/kubernetes/kubernetes/blob/v1.23.0/staging/src/k8s.io/apiserver/pkg/admission/plugin/namespace/lifecycle/admission.go#L120-L166