淺析 Kubernetes 控制器的工做原理

原文地址:淺析 Kubernetes 控制器的工做原理html

Kubernetes 中運行了一系列控制器來確保集羣的當前狀態與指望狀態保持一致,它們就是 Kubernetes 的大腦。例如,ReplicaSet 控制器負責維護集羣中運行的 Pod 數量;Node 控制器負責監控節點的狀態,並在節點出現故障時及時作出響應。總而言之,在 Kubernetes 中,每一個控制器只負責某種類型的特定資源。對於集羣管理員來講,瞭解每一個控制器的角色分工相當重要,若有必要,你還須要深刻了解控制器的工做原理。git

本文我將會帶你深刻了解 Kubernetes 控制器的內部結構、基本組件以及它的工做原理。本文使用的全部代碼都是從 Kubernetes 控制器的當前實現代碼中提取的,基於 Go 語言的 client-go 庫。github

1. 控制器的模型

Kubernetes 官方文檔給出了控制器最完美的解釋:api

In applications of robotics and automation, a control loop is a non-terminating loop that regulates the state of the system. In Kubernetes, a controller is a control loop that watches the shared state of the cluster through the API server and makes changes attempting to move the current state towards the desired state. Examples of controllers that ship with Kubernetes today are the replication controller, endpoints controller, namespace controller, and serviceaccounts controller.緩存

翻譯:數據結構

在機器人設計和自動化的應用中,控制循環是一個用來調節系統狀態的非終止循環。而在 Kubernetes 中,控制器就是前面提到的控制循環,它經過 API Server 監控整個集羣的狀態,並確保集羣處於預期的工做狀態。Kubernetes 自帶的控制器有 ReplicaSet 控制器,Endpoint 控制器,Namespace 控制器和 Service Account 控制器等。併發

官方文檔:Kube-controller-managerapp

Kubernetes 控制器會監視資源的建立/更新/刪除事件,並觸發 Reconcile 函數做爲響應。整個調整過程被稱做 「Reconcile Loop」(調諧循環)或者 「Sync Loop」(同步循環)。Reconcile 是一個使用 object(Resource 的實例)的命名空間和 object 名來調用的函數,使 object 的實際狀態與 object 的 Spec 中定義的狀態保持一致。調用完成後,Reconcile 會將 object 的狀態更新爲當前實際狀態。函數

何時纔會觸發 Reconcile 函數呢?以 ReplicaSet 控制器爲例,當收到了一個關於 ReplicaSet 的事件或者關於 ReplicaSet 建立 Pod 的事件時,就會觸發 Reconcile 函數。oop

爲了下降複雜性,Kubernetes 將全部的控制器都打包到 kube-controller-manager 這個守護進程中。下面是控制器最簡單的實現方式:

for {
  desired := getDesiredState()
  current := getCurrentState()
  makeChanges(desired, current)
}
複製代碼

2. 水平觸發的 API

Kubernetes 的 API 和控制器都是基於水平觸發的,能夠促進系統的自我修復和週期協調。水平觸發這個概念來自硬件的中斷,中斷能夠是水平觸發,也能夠是邊緣觸發。

  • 水平觸發 : 系統僅依賴於當前狀態。即便系統錯過了某個事件(可能由於故障掛掉了),當它恢復時,依然能夠經過查看信號的當前狀態來作出正確的響應。
  • 邊緣觸發 : 系統不只依賴於當前狀態,還依賴於過去的狀態。若是系統錯過了某個事件(「邊緣」),則必須從新查看該事件才能恢復系統。

Kubernetes 水平觸發的 API 實現方式是:監視系統的實際狀態,並與對象的 Spec 中定義的指望狀態進行對比,而後再調用 Reconcile 函數來調整實際狀態,使之與指望狀態相匹配。

水平觸發的 API 也叫聲明式 API。

水平觸發的 API 有如下幾個特色:

  • Reconcile 會跳過中間過程在 Spec 中聲明的值,直接做用於當前 Spec 中聲明的值。
  • 在觸發 Reconcile 以前,控制器會併發處理多個事件,而不是串行處理每一個事件。

舉兩個例子:

例 1:併發處理多個事件

用戶建立了 1000 個副本數的 ReplicaSet,而後 ReplicaSet 控制器會建立 1000 個 Pod,並維護 ReplicaSet 的 Status 字段。在水平觸發系統中,控制器會在觸發 Reconcile 以前併發更新全部 Pod(Reconcile 函數僅接收對象的 Namespace 和 Name 做爲參數),只須要更新 Status 字段 1 次。而在邊緣觸發系統中,控制器會串行響應每一個 Pod 事件,這樣就會更新 Status 字段 1000 次。

例 2:跳過中間狀態

用戶修改了某個 Deployment 的鏡像,而後進行回滾。在回滾過程當中發現容器陷入 crash 循環,須要增長內存限制。而後用戶更新了 Deployment 的內容,調整內存限制,從新開始回滾。在水平觸發系統中,控制器會當即中止上一次回滾動做,開始根據最新值進行回滾。而在邊緣觸發系統中,控制器必須等上一次回滾操做完成才能進行下一次回滾。

3. 控制器的內部結構

每一個控制器內部都有兩個核心組件:Informer/SharedInformerWorkqueue。其中 Informer/SharedInformer 負責 watch Kubernetes 資源對象的狀態變化,而後將相關事件(evenets)發送到 Workqueue 中,最後再由控制器的 workerWorkqueue 中取出事件交給控制器處理程序進行處理。

事件 = 動做(create, update 或 delete) + 資源的 key(以 namespace/name 的形式表示)

Informer

控制器的主要做用是 watch 資源對象的當前狀態和指望狀態,而後發送指令來調整當前狀態,使之更接近指望狀態。爲了得到資源對象當前狀態的詳細信息,控制器須要向 API Server 發送請求。

但頻繁地調用 API Server 很是消耗集羣資源,所以爲了可以屢次 getlist 對象,Kubernetes 開發人員最終決定使用 client-go 庫提供的緩存機制。控制器並不須要頻繁調用 API Server,只有當資源對象被建立,修改或刪除時,才須要獲取相關事件。client-go 庫提供了 Listwatcher 接口用來得到某種資源的所有 Object,緩存在內存中;而後,調用 Watch API 去 watch 這種資源,去維護這份緩存;最後就再也不調用 Kubernetes 的任何 API:

lw := cache.NewListWatchFromClient(
      client,
      &v1.Pod{},
      api.NamespaceAll,
      fieldSelector)
複製代碼

上面的這些全部工做都是在 Informer 中完成的,Informer 的數據結構以下所示:

store, controller := cache.NewInformer {
	&cache.ListWatch{},
	&v1.Pod{},
	resyncPeriod,
	cache.ResourceEventHandlerFuncs{},
複製代碼

儘管 Informer 尚未在 Kubernetes 的代碼中被普遍使用(目前主要使用 SharedInformer,下文我會詳述),但若是你想編寫一個自定義的控制器,它仍然是一個必不可少的概念。

你能夠把 Informer 理解爲 API Server 與控制器之間的事件代理,把 Workqueue 理解爲存儲事件的數據結構。

下面是用於構造 Informer 的三種模式:

ListWatcher

ListWatcher 是對某個特定命名空間中某個特定資源的 listwatch 函數的集合。這樣作有助於控制器只專一於某種特定資源。fieldSelector 是一種過濾器,它用來縮小資源搜索的範圍,讓控制器只檢索匹配特定字段的資源。Listwatcher 的數據結構以下所示:

cache.ListWatch {
	listFunc := func(options metav1.ListOptions) (runtime.Object, error) {
		return client.Get().
			Namespace(namespace).
			Resource(resource).
			VersionedParams(&options, metav1.ParameterCodec).
			FieldsSelectorParam(fieldSelector).
			Do().
			Get()
	}
	watchFunc := func(options metav1.ListOptions) (watch.Interface, error) {
		options.Watch = true
		return client.Get().
			Namespace(namespace).
			Resource(resource).
			VersionedParams(&options, metav1.ParameterCodec).
			FieldsSelectorParam(fieldSelector).
			Watch()
	}
}
複製代碼

Resource Event Handler

Resource Event Handler 用來處理相關資源發生變化的事件:

type ResourceEventHandlerFuncs struct {
	AddFunc    func(obj interface{}) UpdateFunc func(oldObj, newObj interface{}) DeleteFunc func(obj interface{}) } 複製代碼
  • AddFunc : 當資源建立時被調用
  • UpdateFunc : 當已經存在的資源被修改時就會調用 UpdateFuncoldObj 表示資源的最近一次已知狀態。若是 Informer 向 API Server 從新同步,則無論資源有沒有發生更改,都會調用 UpdateFunc
  • DeleteFunc : 當已經存在的資源被刪除時就會調用 DeleteFunc。該函數會獲取資源的最近一次已知狀態,若是沒法獲取,就會獲得一個類型爲 DeletedFinalStateUnknown 的對象。

ResyncPeriod

ResyncPeriod 用來設置控制器遍歷緩存中的資源以及執行 UpdateFunc 的頻率。這樣作能夠週期性地驗證資源的當前狀態是否與指望狀態匹配。

若是控制器錯過了 update 操做或者上一次操做失敗了,ResyncPeriod 將會起到很大的彌補做用。若是你想編寫自定義控制器,不要把週期設置過短,不然系統負載會很是高。

SharedInformer

經過上文咱們已經瞭解到,Informer 會將資源緩存在本地以供本身後續使用。但 Kubernetes 中運行了不少控制器,有不少資源須要管理,不免會出現如下這種重疊的狀況:一個資源受到多個控制器管理。

爲了應對這種場景,能夠經過 SharedInformer 來建立一份供多個控制器共享的緩存。這樣就不須要再重複緩存資源,也減小了系統的內存開銷。使用了 SharedInformer 以後,無論有多少個控制器同時讀取事件,SharedInformer 只會調用一個 Watch API 來 watch 上游的 API Server,大大下降了 API Server 的負載。實際上 kube-controller-manager 就是這麼工做的。

SharedInformer 提供 hooks 來接收添加、更新或刪除某個資源的事件通知。還提供了相關函數用於訪問共享緩存並肯定什麼時候啓用緩存,這樣能夠減小與 API Server 的鏈接次數,下降 API Server 的重複序列化成本和控制器的重複反序列化成本。

lw := cache.NewListWatchFromClient(…)
sharedInformer := cache.NewSharedInformer(lw, &api.Pod{}, resyncPeriod)
複製代碼

Workqueue

因爲 SharedInformer 提供的緩存是共享的,因此它沒法跟蹤每一個控制器,這就須要控制器本身實現排隊和重試機制。所以,大多數 Resource Event Handler 所作的工做只是將事件放入消費者工做隊列中。

每當資源被修改時,Resource Event Handler 就會放入一個 key 到 Workqueue 中。key 的表示形式爲 <resource_namespace>/<resource_name>,若是提供了 <resource_namespace>,key 的表示形式就是 <resource_name>。每一個事件都以 key 做爲標識,所以每一個消費者(控制器)均可以使用 workers 從 Workqueue 中讀取 key。全部的讀取動做都是串行的,這就保證了不會出現兩個 worker 同時讀取同一個 key 的狀況。

Workqueueclient-go 庫中的位置爲 client-go/util/workqueue,支持的隊列類型包括延遲隊列,定時隊列和速率限制隊列。下面是速率限制隊列的一個示例:

queue :=
workqueue.NewRateLimitingQueue(workqueue.DefaultControllerRateLimiter())
複製代碼

Workqueue 提供了不少函數來處理 key,每一個 key 在 Workqueue 中的生命週期以下圖所示:

若是處理事件失敗,控制器就會調用 AddRateLimited() 函數將事件的 key 放回 Workqueue 以供後續重試(若是重試次數沒有達到上限)。若是處理成功,控制器就會調用 Forget() 函數將事件的 key 從 Workqueue 中移除。注意:該函數僅僅只是讓 Workqueue 中止跟蹤事件歷史,若是想從 Workqueue 中徹底移除事件,須要調用 Done() 函數。

如今咱們知道,Workqueue 能夠處理來自緩存的事件通知,但還有一個問題:控制器應該什麼時候啓用 workers 來處理 Workqueue 中的事件呢?

控制器須要等到緩存徹底同步到最新狀態才能開始處理 Workqueue 中的事件,主要有兩個緣由:

  1. 在緩存徹底同步以前,獲取的資源信息是不許確的。
  2. 對單個資源的屢次快速更新將由緩存合併到最新版本中,所以控制器必須等到緩存變爲空閒狀態才能開始處理事件,否則只會把時間浪費在等待上。

這種作法的僞代碼以下:

controller.informer = cache.NewSharedInformer(...)
controller.queue = workqueue.NewRateLimitingQueue(workqueue.DefaultControllerRateLimiter())

controller.informer.Run(stopCh)

if !cache.WaitForCacheSync(stopCh, controller.HasSynched)
{
	log.Errorf("Timed out waiting for caches to sync"))
}

// Now start processing
controller.runWorker()
複製代碼

全部處理流程以下所示:

控制器處理事件的流程

4. 參考資料

相關文章
相關標籤/搜索