Informer启动后会连接API Server并进行全量资源查询,之后会对资源对象进行监听。以上操作主要是由Reflector实现的。源码路径为k8s.io/client-go/tools/cache/reflector.go。

Reflector使用的List/Watch方法主要分为2部分,第一部分用来获取全量的资源列表;第二部分是对资源对象进行监控。

首先看一下Reflector的结构体定义,示例如下所示。

type Reflector struct {
name string
expectedTypeName string
expectedType reflect.Type
expectedGVK *schema.GroupVersionKind
store Store
listerWatcher ListerWatcher
backoffManager wait.BackoffManager
initConnBackoffManager wait.BackoffManager
resyncPeriod time.Duration
ShouldResync func() bool
clock clock.Clock
paginatedResult bool
lastSyncResourceVersion string
isLastSyncResourceVersionUnavailable bool
lastSyncResourceVersionMutex sync.RWMutex
WatchListPageSize int64
watchErrorHandler WatchErrorHandler
}

其中,listerWatcher这个字段是一个接口类型,Reflector在run方法中会调用ListAndWatch方法,而ListerAndWatch方法中实际执行的就是ListerWatcher的Lister方法和Watcher方法,分别用来获取全量的资源对象和对后面的事件变更进行监控。ListerWatcher定义如下所示。

type ListerWatcher interface {
Lister
Watcher
}Lister和Watcher也是接口,Lister接口的代码如下所示。
type Lister interface {

List(options metav1.ListOptions) (runtime.Object, error)
}Watcher接口的代码如下所示。
type Watcher interface {
Watch(options metav1.ListOptions) (watch.Interface, error)
}

而实际上r.ListerWacher.List真正调用的是Pod、Deployment等资源对象的Informer下的ListFunc函数和WatchFunc函数,源码路径为k8s.io/client-go/informers/core/v1/pods.go。

Pod Informer的ListFunc函数和WatchFunc函数代码如下所示。

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)
},
WatchFunc: func(options metav1.ListOptions) (watch.Interface, error) {
if tweakListOptions != nil {
tweakListOptions(&options)
}
return client.CoreV1().Pods(namespace).Watch(context.TODO(), options)
},
},
&corev1.Pod{},
resyncPeriod,
indexers,
)
}

ListFunc函数和WatchFunc函数是通过Clientset客户端与API Server交互后获得的,这也是为什么8.2.1节的代码示例中,在生成NewSharedInformerFactory之前需要先获得Clientset,示例如下所示。

//创建KController对象
func NewKubeController(kubeConfig *restclient.Config, clientset *kubernetes.
Clientset, defaultResync time.Duration) *KubeController {
kc := &KubeController{kubeConfig: kubeConfig, clientset: clientset}
//需要传入Clientset
kc.factory = informers.NewSharedInformerFactory(clientset, defaultResync)
...
kc.deploymentInformer = kc.factory.Apps().V1().Deployments()
kc.deploymentsLister = kc.deploymentInformer.Lister()
kc.deploymentsSynced = kc.deploymentInformer.Informer().HasSynced
kc.podInformer = kc.factory.Core().V1().Pods()
kc.podsLister = kc.podInformer.Lister()
kc.podsSynced = kc.podInformer.Informer().HasSynced
...
return kc
}

List/Watch的逻辑比较长,下面分为2部分来分析核心的函数调用,第一部分示例如下所示。

func (r *Reflector) ListAndWatch(stopCh <-chan struct{}) error {
klog.V(3).Infof("Listing and watching %v from %s", r.expectedTypeName, r.name)
var resourceVersion string
options := metav1.ListOptions{ResourceVersion: r.relistResourceVersion()}
if err := func() error {
initTrace := trace.New("Reflector ListAndWatch", trace.Field{"name", r.name})
defer initTrace.LogIfLong(10 * time.Second)
var list runtime.Object
var paginatedResult bool
var err error
listCh := make(chan struct{}, 1)
panicCh := make(chan interface{}, 1)
//调用r.listerWatcher.List获取资源数据
go func() {
defer func() {
if r := recover(); r != nil {
panicCh <- r
}
}()
pager := pager.New(pager.SimplePageFunc(func(opts metav1.ListOptions)
(runtime.Object, error) {
return r.listerWatcher.List(opts)
}))
switch {
case r.WatchListPageSize != 0:
pager.PageSize = r.WatchListPageSize
case r.paginatedResult:
case options.ResourceVersion != "" && options.ResourceVersion != "0":
pager.PageSize = 0
}
list, paginatedResult, err = pager.List(context.Background(), options)
if isExpiredError(err) || isTooLargeResourceVersionError(err) {
r.setIsLastSyncResourceVersionUnavailable(true)
list, paginatedResult, err = pager.List(context.Background(), metav1.
ListOptions{ResourceVersion: r.relistResourceVersion()})
}
close(listCh)
}()
select {
case <-stopCh:
return nil
case r := <-panicCh:
panic(r)
case <-listCh:
}
if err != nil {
return fmt.Errorf("failed to list %v: %v", r.expectedTypeName, err)
}
//调用listMetaInterface.GetResourceVersion获取资源的版本号
if options.ResourceVersion == "0" && paginatedResult {
r.paginatedResult = true
}
r.setIsLastSyncResourceVersionUnavailable(false)
initTrace.Step("Objects listed")
listMetaInterface, err := meta.ListAccessor(list)
if err != nil {
return fmt.Errorf("unable to understand list result %#v: %v", list, err)
}
resourceVersion = listMetaInterface.GetResourceVersion()
initTrace.Step("Resource version extracted")
//调用meta.ExtractList 将资源转换成对象列表
items, err := meta.ExtractList(list)
if err != nil {
return fmt.Errorf("unable to understand list result %#v (%v)", list, err)
}
initTrace.Step("Objects extracted")
//调用r.syncWith 将资源对象列表中的资源对象和对应的版本号存入DeltaFIFO
if err := r.syncWith(items, resourceVersion); err != nil {
return fmt.Errorf("unable to sync list result: %v", err)
}
initTrace.Step("SyncWith done")

第一部分的主体逻辑和调用函数如下。

·调用r.listerWatcher.List获取资源数据。

·调用listMetaInterface.GetResourceVersion获取资源的版本号。

·调用meta.ExtractList将资源转换成对象列表。

·调用r.syncWith将资源对象列表中的资源对象和对应的版本号存入DeltaFIFO。

第二部分示例如下所示。

//调用r.setLastSyncResourceVersion设置最新的资源版本号
r.setLastSyncResourceVersion(resourceVersion)
initTrace.Step("Resource version updated")
return nil
}(); err != nil {
return err
}
resyncerrc := make(chan error, 1)
cancelCh := make(chan struct{})
defer close(cancelCh)
go func() {
resyncCh, cleanup := r.resyncChan()
defer func() {
cleanup()
}()
for {
select {
case <-resyncCh:
case <-stopCh:
return
case <-cancelCh:
return
}
if r.ShouldResync == nil || r.ShouldResync() {
klog.V(4).Infof("%s: forcing resync", r.name)
if err := r.store.Resync(); err != nil {
resyncerrc <- err
return
}
}
cleanup()
resyncCh, cleanup = r.resyncChan()
}
}()
for {
select {
case <-stopCh:
return nil
default:
}
timeoutSeconds := int64(minWatchTimeout.Seconds() * (rand.Float64() + 1.0))
options = metav1.ListOptions{
ResourceVersion: resourceVersion,
TimeoutSeconds: &timeoutSeconds,
AllowWatchBookmarks: true,
}
start := r.clock.Now()
//调用r.listerWatcher.Watch进行资源对象的事件监控
w, err := r.listerWatcher.Watch(options)
if err != nil {
if utilnet.IsConnectionRefused(err) {
<-r.initConnBackoffManager.Backoff().C()
continue
}
return err
}
//触发事件后调用r.watchHandler将资源对象存入DeltaFIFO中并更新版本号
if err := r.watchHandler(start, w, &resourceVersion, resyncerrc, stopCh);
err != nil {
if err != errorStopRequested {
switch {
case isExpiredError(err):
t observed object.
}
}
return nil
}
}
}

第二部分的主体逻辑和调用函数如下。

·调用r.setLastSyncResourceVersion设置最新的资源版本号。

·调用r.listerWatcher.Watch进行资源对象的事件监控。

·触发事件后调用r.watchHandler将资源对象存入DeltaFIFO中并更新版本号。

List/Watch方法逻辑中调用的函数较多,感兴趣的读者可以查阅源码。

关于Watch的底层实现,其实是使用HTTP长连接来监听事件的,并且使用了HTTP的分块传输编码(Chunked Transfer Encoding)。根据百度百科上的介绍,分块传输编码是HTTP中的一种数据传输机制,允许HTTP由网页服务器发送给客户端应用(通常是网页浏览器)的数据可以分成多个部分。分块传输编码只在HTTP 1.1版本中提供。

通常,HTTP应答消息中的数据是整个发送的,Content-Length消息头字段表示数据的长度。数据的长度很重要,因为客户端需要知道哪里是应答消息的结束,以及哪里是后续应答消息的开始。然而,使用分块传输编码将数据分解成一系列数据块,并以一个或多个块发送,这样服务器可以发送数据而不需要预先知道发送内容的大小。通常,数据块的大小是固定的,但也不总是这种情况。