Kubernetes: client-go 源码剖析(二)
kubernetes:client-go
系列文章:
2.3 运行 informer
运行 informer
将 Reflector
,informer
和 indexer
组件关联以实现 informer
流程图的流程。
2.3.1 Reflector List&Watch
运行 informer
:
informer.Run(stopCh)
// client-go/tools/cache/shared_informer.go
func (s *sharedIndexInformer) Run(stopCh <-chan struct{}) {
func() {
...
// 创建 DeltaFIFO 队列
fifo := NewDeltaFIFOWithOptions(DeltaFIFOOptions{
KnownObjects: s.indexer,
EmitDeltaTypeReplaced: true,
Transformer: s.transform,
})
cfg := &Config{
Queue: fifo,
ListerWatcher: s.listerWatcher,
ObjectType: s.objectType,
ObjectDescription: s.objectDescription,
FullResyncPeriod: s.resyncCheckPeriod,
RetryOnError: false,
ShouldResync: s.processor.shouldResync,
Process: s.HandleDeltas,
WatchErrorHandler: s.watchErrorHandler,
}
// 根据 Config 创建 informer 的 controller
s.controller = New(cfg)
s.controller.(*controller).clock = s.clock
s.started = true
}()
...
// goroutine 运行 processor
wg.StartWithChannel(processorStopCh, s.processor.run)
...
// 运行 controller
s.controller.Run(stopCh)
}
首先,创建队列 Delta FIFO
:
func (s *sharedIndexInformer) Run(stopCh <-chan struct{}) {
...
func() {
fifo := NewDeltaFIFOWithOptions(DeltaFIFOOptions{
KnownObjects: s.indexer,
EmitDeltaTypeReplaced: true,
Transformer: s.transform,
})
}()
}
func NewDeltaFIFOWithOptions(opts DeltaFIFOOptions) *DeltaFIFO {
...
f := &DeltaFIFO{
items: map[string]Deltas{},
queue: []string{},
keyFunc: opts.KeyFunction,
knownObjects: opts.KnownObjects,
emitDeltaTypeReplaced: opts.EmitDeltaTypeReplaced,
transformer: opts.Transformer,
}
f.cond.L = &f.lock
return f
}
该队列中存储的是 Delta
资源对象,其存储结构为:
为什么队列要设计成这个样子?因为 informer
在读取队列时,根据 items
的 action type 调用对应 EventHandler
的回调函数。
接下来,实例化 informer
的 controller
对象,并且调用 controller.Run
运行 controller
:
func (c *controller) Run(stopCh <-chan struct{}) {
...
r := NewReflectorWithOptions(
c.config.ListerWatcher,
c.config.ObjectType,
c.config.Queue,
ReflectorOptions{
ResyncPeriod: c.config.FullResyncPeriod,
TypeDescription: c.config.ObjectDescription,
Clock: c.clock,
},
)
...
wg.StartWithChannel(stopCh, r.Run)
wait.Until(c.processLoop, time.Second, stopCh)
wg.Wait()
}
在 Run
方法中创建 Reflector
核心组件:
func NewReflectorWithOptions(lw ListerWatcher, expectedType interface{}, store Store, options ReflectorOptions) *Reflector {
...
// Reflector 中包括 ListerWatcher 对象和 DeltaFIFO 队列
r := &Reflector{
name: options.Name,
resyncPeriod: options.ResyncPeriod,
typeDescription: options.TypeDescription,
listerWatcher: lw,
store: store,
...
}
...
return r
}
继续进入 wg.StartWithChannel
中运行 Reflector.Run
:
func (r *Reflector) Run(stopCh <-chan struct{}) {
wait.BackoffUntil(func() {
// 调用 Reflector 的 ListAndWatch 方法
if err := r.ListAndWatch(stopCh); err != nil {
r.watchErrorHandler(r, err)
}
}, r.backoffManager, true, stopCh)
}
func (r *Reflector) ListAndWatch(stopCh <-chan struct{}) error {
...
if fallbackToList {
err = r.list(stopCh)
if err != nil {
return err
}
}
...
return r.watch(w, stopCh, resyncerrc)
}
在这里我们看到 Reflector
的 ListAndWatch
实现了资源的 list 和 watch 操作。这相当于 informer
流程图的第一步。
具体看 Reflector
的 list
方法做了什么:
func (r *Reflector) list(stopCh <-chan struct{}) error {
...
go func() {
...
pager := pager.New(pager.SimplePageFunc(func(opts metav1.ListOptions) (runtime.Object, error) {
return r.listerWatcher.List(opts)
}))
list, paginatedResult, err = pager.ListWithAlloc(context.Background(), options)
}()
select {
case <-stopCh:
return nil
case r := <-panicCh:
panic(r)
// 阻塞 list
case <-listCh:
}
...
}
首先,在 goroutine
内调用 pager.ListWithAlloc
获得 list 的资源对象:
func (p *ListPager) ListWithAlloc(ctx context.Context, options metav1.ListOptions) (runtime.Object, bool, error) {
return p.list(ctx, options, true)
}
func (p *ListPager) list(ctx context.Context, options metav1.ListOptions, allocNew bool) (runtime.Object, bool, error) {
...
for {
select {
case <-ctx.Done():
return nil, paginatedResult, ctx.Err()
default:
}
obj, err := p.PageFn(ctx, options)
...
}
...
}
list
方法内调用 p.PageFn
获得资源对象 obj
,调用 p.PageFn
实际调用的是 Reflector.listerWatcher
对象的 List
方法:
func (lw *ListWatch) List(options metav1.ListOptions) (runtime.Object, error) {
return lw.ListFunc(options)
}
func NewFilteredPodInformer(client kubernetes.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers, tweakListOptions internalinterfaces.TweakListOptionsFunc) cache.SharedIndexInformer {
return cache.NewSharedIndexInformer(
&cache.ListWatch{
ListFunc: func(options metav1.ListOptions) (runtime.Object, error) {
if tweakListOptions != nil {
tweakListOptions(&options)
}
return client.CoreV1().Pods(namespace).List(context.TODO(), options)
},
...
}
)
}
可以看到,这里将 Reflector
和前面的 ListFunc
回调函数关联上了,实际通过 ClientSet
客户端对象 list kube-apiserver
的资源。
2.3.2 Reflector Add Object
client-go informer
流程的第一步实现了,那么第二步在哪呢?带着这个问题,我们继续看 Reflector.list
方法:
func (r *Reflector) list(stopCh <-chan struct{}) error {
...
// 通过反射读取 list 的 meta field
listMetaInterface, err := meta.ListAccessor(list)
if err != nil {
return fmt.Errorf("unable to understand list result %#v: %v", list, err)
}
// 读取 resource version
resourceVersion = listMetaInterface.GetResourceVersion()
items, err := meta.ExtractListWithAlloc(list)
if err := r.syncWith(items, resourceVersion); err != nil {
return fmt.Errorf("unable to sync list result: %v", err)
}
return nil
}
重点看 Reflector.syncWith
方法:
func (r *Reflector) syncWith(items []runtime.Object, resourceVersion string) error {
found := make([]interface{}, 0, len(items))
for _, item := range items {
found = append(found, item)
}
return r.store.Replace(found, resourceVersion)
}
func (f *DeltaFIFO) Replace(list []interface{}, _ string) error {
...
for _, item := range list {
key, err := f.KeyOf(item)
if err != nil {
return KeyError{item, err}
}
keys.Insert(key)
if err := f.queueActionLocked(action, item); err != nil {
return fmt.Errorf("couldn't enqueue object: %v", err)
}
}
...
return nil
}
Reflector.syncWith
调用 Reflector.DeltaFIFO
的 Replace
方法将 list 的资源对象添加到 DeltaFIFO
队列中,实现 informer
流程的第二步。
watch 资源和 list 资源的流程类似,这里就不过多介绍了。
2.3.3 informer Pop Object
Reflector
作为生产者将 list&watch 的资源添加到 Delta FIFO
队列,那么消费者在哪里使用 Delta FIFO
的资源呢?
client-go
在 controller.Run
中的 controller.processLoop
处理 Delta FIFO
的资源:
wait.Until(c.processLoop, time.Second, stopCh)
// client-go/tools/cache/controller.go
func (c *controller) processLoop() {
for {
obj, err := c.config.Queue.Pop(PopProcessFunc(c.config.Process))
if err != nil {
if err == ErrFIFOClosed {
return
}
if c.config.RetryOnError {
// This is the safe way to re-enqueue.
c.config.Queue.AddIfNotPresent(obj)
}
}
}
}
controller.processLoop
会轮询队列中的资源,当队列中有资源加入时 Pop 资源:
func (f *DeltaFIFO) Pop(process PopProcessFunc) (interface{}, error) {
for {
for len(f.queue) == 0 {
...
// 当队列无资源的时候协程阻塞
f.cond.Wait()
}
id := f.queue[0]
f.queue = f.queue[1:]
depth := len(f.queue)
...
item, ok := f.items[id]
...
delete(f.items, id)
...
err := process(item, isInInitialList)
...
return item, err
}
}
DeltaFIFO.Pop
会循环读取队列中的资源,当队列无资源时进入阻塞状态。如果队列中有资源,每次读取队列的首元素,删除队列中读取的首元素,然后调用回调函数 PopProcessFunc
处理读取的首元素:
err := process(item, isInInitialList)
// client-go/tools/cache/shared_informer.go
func (s *sharedIndexInformer) HandleDeltas(obj interface{}, isInInitialList bool) error {
s.blockDeltas.Lock()
defer s.blockDeltas.Unlock()
if deltas, ok := obj.(Deltas); ok {
return processDeltas(s, s.indexer, deltas, isInInitialList)
}
return errors.New("object given as Process argument is not Deltas")
}
调用回调函数 PopProcessFunc
实际调用的是 sharedIndexInformer.HandleDeltas
方法,在该方法内处理从队列读取到的资源。
至此,实现了 informer
流程图的第三步。
2.3.4 informer Add and Store Object
继续看 sharedIndexInformer.HandleDeltas
的函数 processDeltas
:
func processDeltas(handler ResourceEventHandler, clientState Store, deltas Deltas, isInInitialList bool,) error {
for _, d := range deltas {
obj := d.Object
switch d.Type {
case Sync, Replaced, Added, Updated:
if old, exists, err := clientState.Get(obj); err == nil && exists {
if err := clientState.Update(obj); err != nil {
return err
}
handler.OnUpdate(old, obj)
} else {
if err := clientState.Add(obj); err != nil {
return err
}
handler.OnAdd(obj, isInInitialList)
}
case Deleted:
if err := clientState.Delete(obj); err != nil {
return err
}
handler.OnDelete(obj)
}
}
return nil
}
在 processDeltas
中根据不同的 Delta Type 执行不同的 case。我们以 Added
type 为例查看处理流程。
首先,clientState.Get
从本地 indexer
存储中读取资源,并判断资源是否存在:
// clientState.Get get 资源 obj
func (c *cache) Get(obj interface{}) (item interface{}, exists bool, err error) {
...
return c.GetByKey(key)
}
func (c *cache) GetByKey(key string) (item interface{}, exists bool, err error) {
item, exists = c.cacheStorage.Get(key)
return item, exists, nil
}
// 从 index 中读取资源
func (c *threadSafeMap) Get(key string) (item interface{}, exists bool) {
c.lock.RLock()
defer c.lock.RUnlock()
item, exists = c.items[key]
return item, exists
}
如果资源不存在,进入 cache.Add
:
func (c *cache) Add(obj interface{}) error {
...
// 添加资源到 indexer
c.cacheStorage.Add(key, obj)
return nil
}
func (c *threadSafeMap) Add(key string, obj interface{}) {
c.Update(key, obj)
}
func (c *threadSafeMap) Update(key string, obj interface{}) {
c.lock.Lock()
defer c.lock.Unlock()
oldObject := c.items[key]
c.items[key] = obj
c.index.updateIndices(oldObject, obj, key)
}
在 clientState.Add
中将队列 Delta FIFO
读取的资源存入 indexer
中。
至此,完成了 informer
流程图的第四步和第五步。
2.3.5 Event Handler
将资源存入 indexer
后继续往下看 sharedIndexInformer.OnAdd
是怎么处理 Pop 出的资源的:
func (s *sharedIndexInformer) OnAdd(obj interface{}, isInInitialList bool) {
...
s.processor.distribute(addNotification{newObj: obj, isInInitialList: isInInitialList}, false)
}
func (p *sharedProcessor) distribute(obj interface{}, sync bool) {
p.listenersLock.RLock()
defer p.listenersLock.RUnlock()
for listener, isSyncing := range p.listeners {
switch {
case !sync:
// non-sync messages are delivered to every listener
listener.add(obj)
case isSyncing:
// sync messages are delivered to every syncing listener
listener.add(obj)
default:
// skipping a sync obj for a non-syncing listener
}
}
}
func (p *processorListener) add(notification interface{}) {
if a, ok := notification.(addNotification); ok && a.isInInitialList {
p.syncTracker.Start()
}
p.addCh <- notification
}
可以看到,Pop 的资源被加入 processorListener.addCh
通道。
那么,通道的另一端是哪里在处理呢?
答案在 sharedIndexInformer.Run
中的 sharedProcessor.run
:
func (s *sharedIndexInformer) Run(stopCh <-chan struct{}) {
...
wg.StartWithChannel(processorStopCh, s.processor.run)
...
}
func (p *sharedProcessor) run(stopCh <-chan struct{}) {
func() {
p.listenersLock.RLock()
defer p.listenersLock.RUnlock()
for listener := range p.listeners {
p.wg.Start(listener.run)
p.wg.Start(listener.pop)
}
p.listenersStarted = true
}()
<-stopCh
...
}
在 sharedProcessor.run
方法中开启两个协程分别执行 listener.run
和 listener.pop
方法。
我们先看 listener.run
方法:
func (p *processorListener) run() {
stopCh := make(chan struct{})
wait.Until(func() {
for next := range p.nextCh {
switch notification := next.(type) {
case updateNotification:
p.handler.OnUpdate(notification.oldObj, notification.newObj)
case addNotification:
p.handler.OnAdd(notification.newObj, notification.isInInitialList)
if notification.isInInitialList {
p.syncTracker.Finished()
}
case deleteNotification:
p.handler.OnDelete(notification.oldObj)
default:
utilruntime.HandleError(fmt.Errorf("unrecognized notification: %T", next))
}
}
close(stopCh)
}, 1*time.Second, stopCh)
}
listener.run
从 processorListener.nextCh
通道中读取资源对象,根据资源对象的类型决定执行哪个 case。
前面通道的一端将 addNotification
加入到 processorListener
的 addCh
通道 p.addCh <- notification
。
这里 processorListener
根据 nextCh
通道的资源执行相应的 case。
那么 addCh
和 nextCh
的关联在哪里呢?
我们看 processorListener.pop
:
func (p *processorListener) pop() {
defer utilruntime.HandleCrash()
defer close(p.nextCh) // Tell .run() to stop
var nextCh chan<- interface{}
var notification interface{}
for {
select {
case nextCh <- notification:
// Notification dispatched
var ok bool
notification, ok = p.pendingNotifications.ReadOne()
if !ok { // Nothing to pop
nextCh = nil // Disable this select case
}
case notificationToAdd, ok := <-p.addCh:
if !ok {
return
}
if notification == nil { // No notification to pop (and pendingNotifications is empty)
// Optimize the case - skip adding to pendingNotifications
notification = notificationToAdd
nextCh = p.nextCh
} else { // There is already a notification waiting to be dispatched
p.pendingNotifications.WriteOne(notificationToAdd)
}
}
}
}
processorListener.pop
的逻辑比较复杂,这里不过多介绍。重点在通过 nextCh = p.nextCh
将 processorListener.nextCh
和函数内通道 nextCh
关联,从而实现 processorListener.addCh
通道到 processorListener.nextCh
通道的数据传递。
了解了通道间的数据传递。我们以 ResourceEventHandlerFuncs.OnAdd
为例看 client-go
是怎么调用 EventHandler
的:
func (p *processorListener) run() {
...
wait.Until(func() {
for next := range p.nextCh {
switch notification := next.(type) {
case updateNotification:
...
case addNotification:
p.handler.OnAdd(notification.newObj, notification.isInInitialList)
...
}
...
}
})
}
func (r ResourceEventHandlerFuncs) OnAdd(obj interface{}, isInInitialList bool) {
if r.AddFunc != nil {
r.AddFunc(obj)
}
}
func main() {
...
informer.AddEventHandler(cache.ResourceEventHandlerFuncs{
AddFunc: func(obj interface{}) {
mObj := obj.(v1.Object)
log.Printf("New Pod Added to Store: %s", mObj.GetName())
},
...
})
}
可以看到,最终通过回调函数执行我们定义的 AddFunc
handler。
至此,实现了 informer
流程图的第六步。
3. 总结
我们通过两篇文章从源码角度介绍了 client-go
的流程。下面要开始 kube-schduler
的学习了,敬请期待...
Kubernetes: client-go 源码剖析(二)的更多相关文章
- Django Rest Framework源码剖析(二)-----权限
一.简介 在上一篇博客中已经介绍了django rest framework 对于认证的源码流程,以及实现过程,当用户经过认证之后下一步就是涉及到权限的问题.比如订单的业务只能VIP才能查看,所以这时 ...
- Qt信号槽源码剖析(二)
大家好,我是IT文艺男,来自一线大厂的一线程序员 上节视频给大家讲解了Qt信号槽的基本概念.元对象编译器.示例代码以及Qt宏:今天接着深入分析,进入Qt信号槽源码剖析系列的第二节视频. Qt信号槽的宏 ...
- jdk源码剖析二: 对象内存布局、synchronized终极原理
很多人一提到锁,自然第一个想到了synchronized,但一直不懂源码实现,现特地追踪到C++层来剥开synchronized的面纱. 网上的很多描述大都不全,让人看了不够爽,看完本章,你将彻底了解 ...
- Dubbo源码剖析二之注册中心
Dubbo基础二之架构及处理流程概述 - 池塘里洗澡的鸭子 - 博客园 (cnblogs.com)中架构中,无论是服务提供者还是服务消费者都离不开注册中心,可见注册中心之重要.Redis.Nacos. ...
- kubernetes dashboard backend源码剖析
dashboard架构主要由一个API handler 和 五个manager构成: API handler用来处理来自客户的http请求,不同的path路由到不同的的handler处理,使用的是go ...
- muduo库源码剖析(二) 服务端
一. TcpServer类: 管理所有的TCP客户连接,TcpServer供用户直接使用,生命期由用户直接控制.用户只需设置好相应的回调函数(如消息处理messageCallback)然后TcpSer ...
- boost.asio源码剖析(二) ---- 架构浅析
* 架构浅析 先来看一下asio的0层的组件图. (图1.0) io_object是I/O对象的集合,其中包含大家所熟悉的socket.deadline_tim ...
- jdk源码剖析一:OpenJDK-Hotspot源码包目录结构
开启正文之前,先说一下源码剖析这一系列,就以“死磕到底”的精神贯彻始终,JDK-->JRE-->JVM(以openJDK代替) 最近想看看JDK8源码,但JDK中JVM(安装在本地C:\P ...
- jdk源码剖析:Synchronized
开启正文之前,先说一下源码剖析这一系列,就以"死磕到底"的精神贯彻始终,最少追踪到JVM指令(再往下C语言实现了). =========正文分割线=========== Sync ...
- jdk源码剖析三:锁Synchronized
一.Synchronized作用 (1)确保线程互斥的访问同步代码 (2)保证共享变量的修改能够及时可见 (3)有效解决重排序问题.(Synchronized同步中的代码JVM不会轻易优化重排序) 二 ...
随机推荐
- 数据仓库——Hive
数据仓库:是一个用于储存,分析,报告的数据系统 数据仓库的目的是构建面向分析的集成化数据环境,分析结果为企业提供决策支持 数仓专注分析 数据仓库仓库为何而来,解决什么问题的? 为了分析数据而来,分析结 ...
- Tensorflow2.0实战之Auto-Encoder
autoencoder可以用于数据压缩.降维,预训练神经网络,生成数据等等 Auto-Encoder架构 需要完成的工作 需要完成Encoder和Decoder的训练 例如,Mnist的一张图片大小为 ...
- 【2016】开机出现 system32\config\system,代码:0xc00000e9解决方法
这是16年刚工作时写的笔记,也带来这里做个记录吧.实际工作这几年里也时不时会遇到,大多数和非正常关机有关系 今天早上,就在刚才,一个同事的电脑开不了机了,开机提示的是system32\config\s ...
- Java线程池ThreadPoolExecutor源码解析
Java线程池ThreadPoolExecutor源码解析 1.ThreadPoolExecutor的构造实现 以jdk8为准,常说线程池有七大参数,通常而言,有四个参数是比较重要的 public T ...
- ASR项目实战-交付过程中遇到的内核崩溃问题
当前参与交付的语音识别产品服务,算法模块基于经典的Kaldi,算法中的一部分运行在GPU之上. 算法团队采用的是声学模型+语言模型的1-pass方案.这个方案的特点在于,语言模型数据文件(HCLG文件 ...
- WinRM服务应用及配置说明
一.什么是winRM服务 1.1.winRM服务介绍 Windows远程管理(WinRM)服务是Windows Server 2003 R2以上版本中一种新式的方便远程管理的服务.通过WinRM服务, ...
- 技本功|数据安全之IDC数据容灾设计实现
近年来,数据安全问题日渐受到大家的关注,对于任何一家企业,数据无疑是最重要的资产之一.提到数据容灾,大家可能会想到备库和备份的概念,那么我们先来谈谈备库与备份的区别. 备库与备份的区别 通常来讲,备库 ...
- Havoc插件编写
配置文件的webhook支持discord,所以尝试使用钉钉和企业微信. WebHook { Discord { Url = "" AvatarUrl = "&quo ...
- 虚拟化H搭建
虚拟化H搭建 H搭建所需要的硬件配置 最大值:所有组件不能超过160个cpu 按安装H需要2G内存+若干个guest(不明确多大),最大支持2个TB内存 最小磁盘2G 一个千兆网卡 lscpu信息 [ ...
- 如何实现gif格式图片倒放效果?
不知道大家看电影的时候有没有发现出现过这样的一个神奇场景: 一个子弹竟然从远处飞回到手枪中,整个场景呈现一种时空倒流的感觉? 正文 先来几个有趣的倒放动图娱乐一下~ 猫:我谢谢你们全家 萌娃快乐针 尊 ...