经过上一篇文章的介绍,我们简单了解了整个Ingress的运行机制,这里我们将通过Ingress Controller的源码来更深入分析其运行过程。
要了解本文的内容我们要先了解一个概念,就是kuberentes的events
events
关于events的概念,kubernetes中文社区有一个系列文章剖析得很清析
文章详细介绍了Events的概念,从哪里产生以及去向哪里等,以及更复杂的Events聚合操作。事实上,kubernetes正是通过Events让Ingress Controller知道资源的变化情况。
开始
从官方提供的一个Ingress Controller简单实现的示例中,我们可以找到整个框架代码的入口
1 | func main() { |
main函数的工作内容十分简单,就是实例化一个IngressContrller并将其Start起来。
1 | import "k8s.io/ingress/core/pkg/ingress/controller" |
这是controller框架核心所在的包
我们看一下NewIngressController的定义
1 | func NewIngressController(backend ingress.Controller) *GenericController { |
该方法接收一个ingress.Controller接口,并返回一个GenericController结构体的指针
再来看一下ingress.Controller接口的定义
1 | type Controller interface { |
这个接口就是ingress controller留给用户自已实现代码的地方,只要实现了这个接口,那么你自定义的ingress controller也就完成了。这里先点一下最重要的两个方法 OnUpdate和Reload
当资源发生变化时,框架会调用OnUpdate方法,并将资源配置信息传入,用户根据这些配置信息生成配置(以[]byte返回),然后框架再调用Reload方法,用户在这个方法中可以重新加载配置(例如 nginx -r reload) 嘿嘿是不是一个很典型的模板方法!!
NewIngressController的主要工作是初始化命令行参数,接着在方法最后调用包内私有函数newIngressController
newIngressController
这个包内方法是整个框架核心所在,它真正的初始化了IngressController
来看函数定义:
1 | func newIngressController(config *Configuration) *GenericController { |
这里的Configuration包含了从命令行传进来的参数配置,以及用户实现的一个Controller接口
1 | type Configuration struct { |
这里的Backed就是上文提到的Controller接口,接下来看一下该方法做了哪些事情
1 | eventBroadcaster := record.NewBroadcaster() |
这段代码做了三件事情:
1.初始化了一个事件广播器
2.初始化了GenericController,将前面的配置传过去,并且new了一个事件的recorder,这个recorder用来在后面产生事件。
3.初始化了syncQueue和secretQueue
这两个Queue有什么作用呢?来看一下它的定义和注释:
1 | // NewTaskQueue creates a new task queue with the given sync function. |
注释已经解释得很清楚了,这个方法所创建的queue,每接收一个元素就会调用一个syncFn,并将该元素作为该方法的参数传进去。可以看到ic.syncQueue和ic.secretQueue对应的处理方法为ic.sync和ic.syncSecret,这两个方法到底做了些什么事情,我们后面再分析。
这里还有一个问题就是为什么不直接调用syncFn而要通过队列呢,很显然这里队列的作用就是将并行的事情串行化掉而已。
kubernetes客户端的资源监听机制
kubernetes的资源监听机制是一个相对比较复杂的过程,首先来看一下这段定义
在 “k8s.io/kubernetes/pkg/client/cache” 包下面存在着
1 | type ResourceEventHandlerFuncs struct { |
这样一样结构体,该结构体实现了以下接口
1 | type ResourceEventHandler interface { |
接着再来看一下NewInformer函数
1 | func NewInformer( |
这个函数初始化一个消息通知器,ListerWatcher指定了监听资源的方法,一旦资源发生了变化(增、删、改),就会触发ResourceEventHandler相应的函数。这里是一个观察者模式的简化版,将多播委托简化成单播委托,并且将多个事件聚合在了一起。好了,这里要说一下整个controller最重要的list和watch模型。
List和Watch
我们先来看一下这段代码:
1 | ic.ingLister.Store, ic.ingController = cache.NewInformer( |
顺藤摸瓜:
1 | func NewListWatchFromClient(c Getter, resource string, namespace string, fieldSelector fields.Selector) *ListWatch { |
到这里我们找到了controller如何和apiserver交互的代码,既然找到了,那我们就动起手来,看看它具体干了一些什么事睛。
list
1 |
|
在创建client的时候我们设置了http代理,这里我用了fiddler工具用于抓取http的请求内容。接着我们请求了default名称空间下的ingresses资源列表,设置了resourceVersion为0
在fiddler中我们发现其请求了/apis/extensions/v1beta1/namespaces/default/ingresses?resourceVersion=0这个api
并且返回了一下的内容
1 | { |
这段json列出了当前default名称空间下的所有ingress资源的情况。有了这些列表数据(可以使用同样的方法列出service,node,secret等其它资源),对于我们生成backend的配置(如nginx的配置)就已经足够了,我们可以通过不停的轮询这个接口,一旦发现数据发生了变化,我们就重新生成配置并加载它。一切工作到这里似乎就可以结束了,但是细心的读者可能会发生我们还有一watch接口。这里要记住list接口返回的resourceVersion:2264497
watch
1 | watch, err := kubeClient.Extensions().RESTClient(). |
通过fiddler可以看到请求了/apis/extensions/v1beta1/watch/namespaces/default/ingresses?resourceVersion=2264497这个接口,值得注意的是在返回的http头是这样子的
1 | HTTP/1.1 200 OK |
这个时候这个http请求是没有Content-Lenth头,而且服务端一直hold住这个请求,注意Transfer-Encoding: chunked。对于http服务端主动通知客户端的,除了轮询外,还有使用这种方式的,这也是大多数web聊天工具使用的方式。
这时候我们发现通过resourceVersion=2264497请求不到任何的东西,这是因为对于2264497这个版本号来说,当前ingress资源并没有发生任何变化
我们再做以下实验:在master机上运行kubectl delete -n default ing –all
这个命令删除default名称空间下面的所有ingress资源,这时候可以发下刚才hold住的http请求立即返回了一些信息:
1 | { |
json显示了我们所删掉的ingress资源信息,注意其中的resourceVersion,这个时候我们修改watch接口中的resourceVersion为2273842的话,那么其返回内容会变成
1 | { |
也就是说,watch接口根据请求的版本号返回当前服务器的状态与给定版本之间的差异。例如在版本2264497和2273843之间,有两个ingress被删除,而2273842和2273843这两个版本之间只有一个ingress被删除。
小结:listwatch在初始化的时候先通过list接口获取当前资源的列表以及resourceVersion,接着再通过watch接口监听资源的变化。
事件的传递
了解了资源的监听机制,那么程序是在什么时候开始监听的,并且发生变化后事件是如何传递的呢?
在上文件的NewInformer函数返回了两个值:cache.Store和cache.Controller,其中Controller在GenericController的Start方法中被用到
1 | func (ic GenericController) Start() { |
这个方法就是在文章开头的main函数中被调用到的ic.Start方法,这里可以看到有6个controller,分别对应了6种资源:ingresses,endpoints,services,nodes,secrets,configmaps。在调用cache.Controller的Run方法时,每个Controller都会开始ListWatch流程,对相应的资源进行监听。
看一下Run方法:
1 | func (c *Controller) Run(stopCh <-chan struct{}) { |
实际运行是通过Reflector的RunUntil
1 | func (r *Reflector) RunUntil(stopCh <-chan struct{}) { |
1 | // Until loops until stop channel is closed, running f every period. |
注释里面说到,Until循环调用f函数,每隔period时长调用一次,直到stop channel被关闭。可以看到这个period参数是在应用程序启动的时候通过命令行参数指定的,如果不指定,则默认值为60s
1 | resyncPeriod = flags.Duration("sync-period", 60*time.Second, |
笔者猜测,这么做的目的应该是防止watch的时候http连接异常断开之后导致后续的监听失效,毕竟http无法保证连接的稳定性。
那么真正干活的地方应该就是Reflactor的ListAndWatch方法了
1 | // ListAndWatch first lists all items and get the resource version at the moment of call, |
对于watch资源的处理方法:
1 | // watchHandler watches w and keeps *resourceVersion up to date. |
这里发现资源变化的时候,是通过cache.Store这样一个接口来存储变化的
1 | type Store interface { |
这个Store是在NewInformer的时候初始化的
1 | fifo := NewDeltaFIFO(MetaNamespaceKeyFunc, nil, clientState) |
并且在cache.Controller调用Run的时候,开始对该队列进行监听
1 | wait.Until(c.processLoop, time.Second, stopCh) |
1 | func (c *Controller) processLoop() { |
同样c.config.Process也是在NewInformer的时候定义的:
1 | Process: func(obj interface{}) error { |
这里的h就是上文提到的ResourceEventHandler接口。当资源发化变化时,会先将资源保存到本地缓存中,再触发对应的事件,这里将资源缓存起来,以便后续的程序可以直接取,不用再次请求服务端。
这里简单看一下对于ingress资源发生变动时相应的处理逻辑:
1 | ingEventHandler := cache.ResourceEventHandlerFuncs{ |
这里主要处理的就是对ingress资源的tsl节点,如果发现了对应的tsl资源,则会对secretQueue进行Enqueue操作。
到这里,整个框架的来龙去脉就基本上理清楚了,现在回到这两个队列上面:
1 | ic.syncQueue = task.NewTaskQueue(ic.sync) |
1 | func (ic *GenericController) sync(e interface{}) error { |
这里将一切资源组织成ingress.Configuration结构传给OnUpdate方法,OnUpdate由各个Ingress Controller实现方实现,生成对应的配置数据(例如nginx的config)以byte切片返回,然后再将这些配置数据传给Reload方法,这个方法同样由第三方实现。
小结
本文通过分析源码的方式理清了整个Ingress Controller框架的来龙去脉,在下一篇文章中,通过对Nginx Ingress Controller源码分析,来看一下如何实现一个Ingress Controller。