原文地址:淺析 Kubernetes 控制器的工做原理html
Kubernetes 中運行了一系列控制器來確保集羣的當前狀態與指望狀態保持一致,它們就是 Kubernetes 的大腦。例如,ReplicaSet 控制器負責維護集羣中運行的 Pod 數量;Node 控制器負責監控節點的狀態,並在節點出現故障時及時作出響應。總而言之,在 Kubernetes 中,每一個控制器只負責某種類型的特定資源。對於集羣管理員來講,瞭解每一個控制器的角色分工相當重要,若有必要,你還須要深刻了解控制器的工做原理。git
本文我將會帶你深刻了解 Kubernetes 控制器的內部結構、基本組件以及它的工做原理。本文使用的全部代碼都是從 Kubernetes 控制器的當前實現代碼中提取的,基於 Go 語言的 client-go 庫。github
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)
}
複製代碼
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 的內容,調整內存限制,從新開始回滾。在水平觸發系統中,控制器會當即中止上一次回滾動做,開始根據最新值進行回滾。而在邊緣觸發系統中,控制器必須等上一次回滾操做完成才能進行下一次回滾。
每一個控制器內部都有兩個核心組件:Informer/SharedInformer
和 Workqueue
。其中 Informer/SharedInformer
負責 watch Kubernetes 資源對象的狀態變化,而後將相關事件(evenets)發送到 Workqueue
中,最後再由控制器的 worker
從 Workqueue
中取出事件交給控制器處理程序進行處理。
事件 = 動做(create, update 或 delete) + 資源的 key(以
namespace/name
的形式表示)
控制器的主要做用是 watch 資源對象的當前狀態和指望狀態,而後發送指令來調整當前狀態,使之更接近指望狀態。爲了得到資源對象當前狀態的詳細信息,控制器須要向 API Server 發送請求。
但頻繁地調用 API Server 很是消耗集羣資源,所以爲了可以屢次 get
和 list
對象,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
是對某個特定命名空間中某個特定資源的 list
和 watch
函數的集合。這樣作有助於控制器只專一於某種特定資源。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
用來處理相關資源發生變化的事件:
type ResourceEventHandlerFuncs struct {
AddFunc func(obj interface{}) UpdateFunc func(oldObj, newObj interface{}) DeleteFunc func(obj interface{}) } 複製代碼
UpdateFunc
。oldObj
表示資源的最近一次已知狀態。若是 Informer 向 API Server 從新同步,則無論資源有沒有發生更改,都會調用 UpdateFunc
。DeleteFunc
。該函數會獲取資源的最近一次已知狀態,若是沒法獲取,就會獲得一個類型爲 DeletedFinalStateUnknown
的對象。ResyncPeriod
用來設置控制器遍歷緩存中的資源以及執行 UpdateFunc
的頻率。這樣作能夠週期性地驗證資源的當前狀態是否與指望狀態匹配。
若是控制器錯過了 update 操做或者上一次操做失敗了,ResyncPeriod
將會起到很大的彌補做用。若是你想編寫自定義控制器,不要把週期設置過短,不然系統負載會很是高。
經過上文咱們已經瞭解到,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)
複製代碼
因爲 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 的狀況。
Workqueue
在 client-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
中的事件,主要有兩個緣由:
這種作法的僞代碼以下:
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()
複製代碼
全部處理流程以下所示:
控制器處理事件的流程