k8s client-go原始碼分析 informer原始碼分析(1)-概要分析

良凱爾發表於2022-04-23

k8s informer概述

我們都知道可以使用k8s的Clientset來獲取所有的原生資源物件,那麼怎麼能持續的獲取叢集的所有資源物件,或監聽叢集的資源物件資料的變化呢?這裡不需要輪詢去不斷執行List操作,而是呼叫Watch介面,即可監聽資源物件的變化,當資源物件發生變化,客戶端即可通過Watch介面收到資源物件的變化。

Watch介面雖然可以直接使用,但一般情況下很少直接使用,因為往往由於叢集中的資源較多,我們需要自己在客戶端去維護一套快取,而這個維護成本比較大。

也是因為如此,client-go提供了自己的實現機制,Informers應運而生。informers實現了持續獲取叢集的所有資源物件、監聽叢集的資源物件變化功能,並在本地維護了全量資源物件的記憶體快取,以減少對apiserver、對etcd的請求壓力。Informers在啟動的時候會首先在客戶端呼叫List介面來獲取全量的物件集合,然後通過Watch介面來獲取增量的物件,然後更新本地快取。

此外informers也有很強的健壯性,當長期執行的watch連線中斷時,informers會嘗試拉起一個新的watch請求來恢復連線,在不丟失任何事件的情況下恢復事件流。另外,informers還可以配置一個重新同步的週期引數,每間隔該週期,informers就會重新List全量資料。

在informers的使用上,通常每個GroupVersionResource(GVR)只例項化一個informers,但有時候我們在一個應用中往往會在多個地方對同一種資源物件都有informer的需求,所以就有了共享informer,即SharedInformerFactory。所以可以通過使用SharedInformerFactory來例項化informers,這樣本地記憶體快取就只有一份,通知機制也只有一套,大大提高了效率,減少了資源浪費。

k8s informer架構

k8s client-go informer主要包括以下部件:
(1)Reflector:Reflector從kube-apiserver中list&watch資源物件,然後呼叫DeltaFIFO的Add/Update/Delete/Replace方法將資源物件及其變化包裝成Delta並將其丟到DeltaFIFO中;
(2)DeltaFIFO:DeltaFIFO中儲存著一個map和一個queue,即map[object key]Deltas以及object key的queue,Deltas為Delta的切片型別,Delta裝有物件及物件的變化型別(Added/Updated/Deleted/Sync) ,Reflector負責DeltaFIFO的輸入,Controller負責處理DeltaFIFO的輸出;
(3)Controller:Controller從DeltaFIFO的queue中pop一個object key出來,並獲取其關聯的 Deltas出來進行處理,遍歷Deltas,根據物件的變化更新Indexer中的本地記憶體快取,並通知Processor,相關物件有變化事件發生;
(4)Processor:Processor根據物件的變化事件型別,呼叫相應的ResourceEventHandler來處理物件的變化;
(5)Indexer:Indexer中有informer維護的指定資源物件的相對於etcd資料的一份本地記憶體快取,可通過該快取獲取資源物件,以減少對apiserver、對etcd的請求壓力;
(6)ResourceEventHandler:使用者根據自身處理邏輯需要,註冊自定義的的ResourceEventHandler,當物件發生變化時,將觸發呼叫對應型別的ResourceEventHandler來做處理。

根據informer架構,對k8s informer的分析將分為以下幾部分進行,本篇為概要分析:
(1)informer概要分析;
(2)informer之初始化與啟動分析;
(3)informer之Reflector分析;
(4)informer之DeltaFIFO分析;
(5)informer之Controller&Processor分析;
(6)informer之Indexer分析;

informer使用示例程式碼

使用大致過程如下:
(1)構建與kube-apiserver通訊的config配置;
(2)初始化與apiserver通訊的clientset;
(3)利用clientset初始化shared informer factory以及pod informer;
(4)註冊informer的自定義ResourceEventHandler;
(5)啟動shared informer factory,開始informer的list & watch操作;
(6)等待informer從kube-apiserver同步資源完成,即informer的list操作獲取的物件都存入到informer中的indexer本地快取中;
(7)建立lister,可以從informer中的indexer本地快取中獲取物件;

func main() {
    // 自定義與kube-apiserver通訊的config配置
    master := "192.168.1.10" // apiserver url
    kubeconfig := "/.kube/config"
    config, err = clientcmd.BuildConfigFromFlags(master, kubeconfig)
    if err != nil {
		klog.Fatalf("Failed to create config: %v", err)
	}
	// 或使用k8s serviceAccount機制與kube-apiserver通訊
	// config, err = rest.InClusterConfig()
    
    // 初始化與apiserver通訊的clientset
    clientset, err := kubernetes.NewForConfig(config)
	if err != nil {
		klog.Fatalf("Failed to create client: %v", err)
	}
	
	// 初始化shared informer factory以及pod informer
	factory := informers.NewSharedInformerFactory(clientset, 30*time.Second)
	podInformer := factory.Core().V1().Pods()
	informer := podInformer.Informer()
	
	// 註冊informer的自定義ResourceEventHandler
	informer.AddEventHandler(cache.ResourceEventHandlerFuncs{
		AddFunc:    xxx,
		UpdateFunc: xxx,
		DeleteFunc: xxx,
	})
	
	// 啟動shared informer factory,開始informer的list & watch操作
	stopper := make(chan struct{})
	go factory.Start(stopper)
	
	// 等待informer從kube-apiserver同步資源完成,即informer的list操作獲取的物件都存入到informer中的indexer本地快取中 
	// 或者呼叫factory.WaitForCacheSync(stopper)
	if !cache.WaitForCacheSync(stopper, informer.HasSynced) {
		runtime.HandleError(fmt.Errorf("Timed out waiting for caches to sync"))
		return
	}
	
	// 建立lister
	podLister := podInformer.Lister()
	// 從informer中的indexer本地快取中獲取物件
	podList, err := podLister.List(labels.Everything())
	if err != nil {
		fmt.Println(err)
	}
	
}

總結

以上只是對K8s informer做了簡單的介紹,以及簡單的寫了一下如何使用informer的示例程式碼,後面將開始對informer的各個部件做進一步的原始碼分析,敬請期待。

最後以一張k8s informer的架構圖作為結尾總結,大家回憶一下k8s informer的架構組成以及各個部件的作用。

相關文章