k8s-bigip-ctlr模块协同工作机制解析
2022-02-08 15:56:02
宗兆伟
k8s-bigip-ctlr模块协同工作机制解析
通道(channel 或者chan)和协程(goroutine)是golang语言的两大核心特性。goroutine实现了golang程序的并发,而channel则实现了并发中的通信机制。
CIS(https://github.com/F5Networks/k8s-bigip-ctlr)的各个模块多使用协程的方式独立运行,并广泛使用channel实现模块间逻辑的解耦。
本文主要阐述CIS关键逻辑间的交互过程,及使用的数据结构(多为chan 类型),从而分析、发掘性能瓶颈点。
模块及协同全貌
下图使用鱼骨图的方式展示CIS各个模块的启动顺序及核心功能及交互用的chan或者数据结构。这里,我们称每个代码调用分支为“模块”。圆角矩形代表与其他模块通信使用的chan或者数据结构。
模块与模块之间使用chan可以很容易实现阻塞式通信过程,即当前协程阻塞以等待某一事件或数据的到来,在后边分模块介绍中会展开描述。
从main函数入口开始,CIS依次展开执行了以下几个部分:
在下边的章节中,我们拆分鱼骨图,逐个部分展示全图表示的含义。
CCCL程序启动模块
这一部分并不复杂。
在函数startPythonDriver(https://github.com/F5Networks/k8s-bigip-ctlr/blob/v2.7.1/cmd/k8s-bigip-ctlr/pythonDriver.go#L136)中,配置python CCCL程序启动参数,启动程序,并使用runBigIPDriver (https://github.com/F5Networks/k8s-bigip-ctlr/blob/v2.7.1/cmd/k8s-bigip-ctlr/pythonDriver.go#L160)启动CCCL日志收集协程。
被启动的CCCL程序负责周期性监控config.json (https://github.com/F5Networks/k8s-bigip-ctlr/blob/v2.7.1/pkg/writer/configWriter.go#L70)文件的变化并连接BIG-IP实现下发。
Vxlan环境下,在非CCCL模式下主要下发的内容有两种:
如上图中展示 CIS 日志协程(runBigIPDriver)通过cmdOut chan 监听CCCL发来的日志。
CIS Agent初始化模块
启动CIS Agent中一件重要的事情是启动ConfigDeployer协程,它负责从各种chan或者数据结构(队列或者appMgr 变量群https://github.com/F5Networks/k8s-bigip-ctlr/blob/v2.7.1/pkg/appmanager/appManager.go#L63-L68)接收数据并处理部署任务。
而am.postAS3Declaration则是整合各种资源数据完成资源到BIG-IP的下发,这部分的代码逻辑比较细节,但并不复杂。
总体来讲,该模块处于整个处理逻辑的下游,其细节控制方面比较琐碎,但均比较可控,主要完成的内容就是将K8S中的资源做各种整合,生成AS3 declaration(rest API 到BIG-IP的AS3)或者CRD模式的iApp模板内容(写入config.json)。
K8S Node资源监听处理模块
该模块实现对K8S中Node这种资源的监听处理。众所周知,K8S中的各种内容均以资源的形式存在,想知道所有的资源类型和列表,可以通过命令 kubectl api-resources获取。Node资源也不例外。当Node发生变化时CIS需要适时的通知BIG-IP作出调整。
SetupNodePolling函数做了三件事(https://github.com/F5Networks/k8s-bigip-ctlr/blob/v2.7.1/cmd/k8s-bigip-ctlr/main.go#L640-L685),
ProcessAppmamagerEvent函数会从vxm.podChan中获取所有pod信息组装IP和Mac地址信息,写入config.json文件。
这里的vxm.podChan 有很多别名:eventChan / vxm.podChan / am.l2l3Agent.eventChan / cm.eventChan,他们名称不同,所处代码位置不同,但引用的是同一个chan类型地址。这一点需要在看代码时注意。统观各个模块也可以发现他们是通过参数的方式传递到各处并成为了新对象的属性。
函数“np.Run”会启动新的协程“go np.poller()”使用for循环持续的周期性的做kubeclient的Nodes().List操作以获取所有的Node节点列表,并触发注册的两个“ProcessNodeUpdate”函数(listener)的执行。
函数func (np *nodePoller) runListener(p PollListener) 和 func (np *nodePoller) poller() 需要放在一起看,因为函数的实现中使用 listener 管道 listener := make(chan pollData) 和 np.addCh 实现数据传递,np.addCh 包含了listener,它叫做listener,实际是个pollData类型。
这里有几个类型,我们需要搞清楚他们的关系。
type pollData struct { 被用作上边的listener
nl []v1.Node
err error
}
type pollListener struct { 被用作np.addCh
l chan pollData
s chan struct{}
}
type PollListener func(interface{}, error) 是个函数
该模块的实现,个人觉得 可以用“奇葩”来概括。分析出来后发现,它实现了一个“莫比乌斯环”:
负责使用kubeclient 定期获取node list,并收集从np.addCh中获取需要填充pollData的pollListener。填充pollData
等待poller函数填充pollData(变量名listener),并执行参数传来的(也即注册的)PollListener函数。填充np.addCh
个人觉得,它完全可以用NodeInformer的方式实现,而不是自己实现了一套informer机制,从代码提交看,此部分代码最早出现在五年前,而client-go repo的开启时间是2017年,也就是五年前。所以猜测它是残留的历史实现,没有采用新的informer实现,之后也没有太多更新。而奇怪的是在CIS代码中是存在nodeInformer声明的:
只是没有注册时间函数,只是空转,出现时间3年前。
另外:np.Run中也调用了runListener,RegisterListener中(https://github.com/F5Networks/k8s-bigip-ctlr/blob/v2.7.1/pkg/pollers/nodePoller.go#L111)为什么又调用了runListener,而,而实际上,RegisterListener中的runListener不会得到执行,因为此时,np.running==false:
setupWatchers模块
if len(*namespaceLabel) == 0 {
...
err = appMgr.AddNamespaceLabelInformer(ls, resyncPeriod)
...
if watchAllNamespaces == true {
...
err = appMgr.AddNamespace("", ls, resyncPeriod)
} else {
...
err = appMgr.AddNamespace("", ls, resyncPeriod)
}
} else {
...
err = appMgr.AddNamespaceLabelInformer(ls, resyncPeriod)
...
}
如上代码所示,found为false时会执行到L491行。 CIS中的代码风格中经常采用的方式是判断true而不判断false。
if condition == true {
do something
return nil
}
// condition == false: NO ELSE
do something else
读代码或者更改代码时,需要特别注意返回位置和隐形else的处理过程。
appMgr.Run模块
该模块是main启动的最后一个关键模块。相当于CIS的主程序,封装了从informer获取数据到组装,到最后发送给ConfigDeployer做下发的的所有逻辑。
该模块启动了很多协程,包括各个informer 协程。
informer的组织方式是以namespace为单位,每个namespace下会有一组informers(https://github.com/F5Networks/k8s-bigip-ctlr/blob/v2.7.1/pkg/appmanager/appManager.go#L96),收集如上图所示的cfgMap,svc,endpt,ing,route,secret,ingClass。
两个主协程namespaceWorker和virtualserverWorker被wait.Until 每间隔1秒钟循环执行:
他们的调用格式很类似,一个用于处理namespace 资源的变化(nsInformer: https://github.com/F5Networks/k8s-bigip-ctlr/blob/v2.7.1/pkg/appmanager/appManager.go#L100),一个用于处理namespace内各个资源的变化。
namespaceWorker
也就是说每次enqueueSvcFromNamespace的运行仅会处理一个,且是第一个。
函数“appInf.svcInformer.GetIndexer().ByIndex("namespace", namespace)”每次获取的objs列表的顺序不是唯一的,可以认为是随机的,所以triggerSyncResources函数间接实现了随机更新同步svc资源的能力。
个人觉得,这导致了程序的不确定性,问题不易调试跟踪。当然,这不是错误。从第437 行日志看,只以debug的方式输出了namespace,也没有注明到底同步了哪个svc资源。
从性能的角度看,设想有600多个svc资源,每次List一遍却仅用其中的objs[0],是一种浪费。好在这里使用了informer cache,而不是直接用kubeclient 去apiserver获取,性能的浪费显得不是很重要。
另外,如果确实要更新同步,为什么仅仅同步了svc,而没有endpoint或者cfgmap,或者是ingress 等等?
virtualserverWorker
virtualserverWorker中调用processNextVirualServer 操作appMgr.vsQueue的元素。读者可以自行查阅workqueue.RateLimitingInterface的各个方法,它们是workqueue的CRUD方法,但具有延迟操作的能力。
syncVirtualServer 函数比较长,但逻辑并不复杂。它根据资源类型、操作类型调用“appMgr.deployResource()”,deployResource负责封装MessageRequest
总结
以上就是CIS的各个模块的组织架构,更多的是关注数据是如何产生和传导的,AS3相关的组装逻辑没有太多涉及,后续会继续跟进调试,因为这部分对性能发掘有帮助。总之,理解CIS框架是做各种功能、性能分析的基础。
最后我们还需要看下鱼骨图中剩下的部分:用于数据传递的chan及相关数据结构:
它们在上文的讲述中已经都涉及到。它们的存在使得模块间的通信变得方便,但也容易增加系统的复杂度,只有清晰的认识他们才能发掘功能及性能优化点。
发布评论 加入社群
相关文章
Nginx Plus 设置JWT身份验证
Kimi Guo
2020-07-04 20:51:01 2247

NGINX 代码解读之模块架构及配置解析
宗兆伟
2020-04-06 15:27:05 2287

回复评论
发布评论