這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。
以下文章為章駿原創,感謝供稿。
今天給大家介紹一下如何使用 client-go 來拓展 Kubernetes API,寫一個 Kubernetes 的控制器。
client-go 是 Kubernetes 官方推出的一個庫,方便我們來調用 Kubernetes 的 RESTful API。
控制流程
Overview
首先,控制器需要與 kubernetes apiserver 進行通訊,則需要一個 client, 這個 client 需要有以下的資訊:
apiserver 的地址以及串連 apiserver 的認證資訊,如使用者名稱密碼或者 token。
kubernetes 的 API resource 的 group 和 version,以及結構體的定義。
一個 serializer 來控制序列化與還原序列化 apiserver 的結果。
然後你就可以用這個 client 去 apiserver list/watch 特定的類型的資源。
一般是建議使用 client-go 中提供的 informer 來 watch 資源的變更而不是輪詢 apiserver,因為 informer 的方式效能更好。
Controller
下面這張圖很好的展示了一個 controller 的工作流程和原理,典型的 controller 一般會有一個或者多個 informer 來跟蹤 resource,把最新的狀態反映到本地的 cache 中。
Controller 使用 informer 來 list/watch apiserver,然後將資源儲存於本地的 cache 中。
如果 informer 監聽到了資源的變化(建立/更新/刪除),就會調用事先註冊的 ResourceEventHandler 回呼函數。
在 ResourceEventHandler 回呼函數中,其實只是做了一些很簡單的過濾,然後將關心變更的 Object 放到 workqueue 裡面。
Controller 從 workqueue 裡面取出 Object,啟動一個 worker 來執行自己的商務邏輯,商務邏輯通常是計算目前叢集的狀態和使用者希望達到的狀態有多大的區別,然後孜孜不倦地讓 apiserver 將狀態演化到使用者希望達到的狀態,比如為 deployment 建立新的 pods,或者是擴容/縮容 deployment。
在worker中就可以使用 lister 來擷取 resource,而不用頻繁的訪問 apiserver,因為 apiserver 中 resource 的變更都會反映到本地的 cache 中。
Clients
下面介紹 client-go 中的三種 client。
Clientset
Clientset 是我們最常用的 client,你可以在它裡面找到 kubernetes 目前所有原生資源對應的 client。 擷取方式一般是,指定 group 然後指定特定的 version,然後根據 resource 名字來擷取到對應的 client。
Dynamic Client
Dynamic client 是一種動態 client,它能同時處理 kubernetes 所有的資源。並且同時,它也不同於 clientset,dynamic client 返回的對象是一個 map[string]interface{},如果一個 controller 中需要控制所有的 API,可以使用dynamic client,目前它被用在了 garbage collector 和 namespace controller。
RESTClient
RESTClient 是 clientset 和 dynamic client 的基礎,前面這兩個 client 本質上都是 RESTClient,它提供了一些 RESTful 的函數如 Get(),Put(),Post(),Delete()。由 Codec 來提供序列化和還原序列化的功能。
如何選擇 Client 的類型呢?
如果你的 Controller 只是需要控制 Kubernetes 原生的資源,如 Pods,Nodes,Deployments等,那麼 clientset 就夠用了。
如果你需要使用 ThirdPartyResource 來拓展 Kubernetes 的 API,那麼需要使用 Dynamic Client 或 RESTClient。
需要注意的是,Dynamic Client 目前只支援 JSON 的序列化和還原序列化。
在1.7+版本需要將 ThirdPartyResource 遷移到 CustomResourceDefinition
Informer
最佳實務
等待所有的 cache 同步完成: 這是為了避免產生大量無用的資源,比如 replica set controller 需要watch replica sets 和 pods, 在 cache 還沒有同步完之前,controller 可能為一個 replica set 建立了大量重複的 pods,因為這個時候 controller 覺得目前還沒有任何的 pods。
修改 resource 對象前先 deepcopy 一份: 在 Informer 這個模型中,我們的 resource 一般是從本地 cache 中取出的,而本地的 cache 對於使用者來說應該是 read-only 的,因為它可能是與其他的 informer 共用的,如果你直接修改 cache 中的對象,可能會引起讀寫的競爭。
處理 DeletedFinalStateUnknown 類型對象: 當你的收到一個刪除事件時,這個對象有可能不是你想要的類型,即它可能是一個 DeletedFinalStateUnknown,你需要單獨處理它。
注意 informer 的 resync 行為, informer 會定期從 apiserver resync 資源,這時候會收到大量重複的更新事件,這個事件有一個特點就是更新的 Object 的 ResourceVersion 是一樣的,將這種不必要的更新過濾掉。
在建立事件中注意 Object 已經被刪除的情況: 在 Controller 重啟的過程中,可能會有一些對象被刪除了,重啟後,Controller 會收到這些已刪除對象的建立事件,請把這些對象正確地刪除。
SharedInformer: 建議使用 SharedInformer, 它會在多個 Informer 中共用一個本地 cache,這裡有一 個 factory 來方便你編寫一個新的 Informer。
Factory:
在 client-go 中提供了一個 SharedInformerFactory 來簡化 informer 的構建,具體代碼在:
https://github.com/kubernetes/client-go/blob/v3.0.0/informers/factory.go#L54
Lister
Lister 是用來協助我們訪問本地 cache 的一個組件。
Workqueue
Workqueue 是一個簡單的 queue 提供了以下的特性:
公平性:每個item 按順序處理。
嚴格性:一個 item 不會被並發地多次處理,而且一個相同的 item 被多次加入 queue 的話也只會處理一次。
支援多個生產者和消費者:它允許一個正在被處理的 item 再次排入佇列。
我們建議使用 RateLimitingQueue,它相比普通的 workqueue 多了以下的功能:
限流:可以限制一個 item 被 reenqueued 的次數。
防止 hot loop:它保證了一個 item 被 reenqueued 後,不會馬上被處理。
Workqueue helper:
這裡有一個 workqueue 的封裝,來簡化 queue 的操作,代碼在以下位置:
https://github.com/caicloud/loadbalancer-controller/blob/master/pkg/util/controller/helper.go
控制流程總結
我們來總結一個控制器的整體工作流程。
建立一個控制器
為控制器建立 workqueue
建立 informer, 為 informer 添加 callback 函數,建立 lister
啟動控制器
啟動 informer
等待本地 cache sync 完成後, 啟動 workers
當收到變更事件後,執行 callback
等待事件觸發
從事件中擷取變更的 Object
做一些必要的檢查
產生 object key,一般是 namespace/name 的形式
將 key 放入 workqueue 中
worker loop
等待從 workqueue 中擷取到 item,一般為 object key
用 object key 通過 lister 從本地 cache 中擷取到真正的 object 對象
做一些檢查
執行真正的商務邏輯
處理下一個 item
到這裡已經講完了一個完整的 Kubernetes 的 Controller 的構建過程。但是還想要多囉嗦幾句關於 kubernetes 的設計原則和 API 習俗,它們是指導我們寫出更加可靠的 Controller 的白皮書。
設計原則
功能設計基於 level_based,這意味系統應該在給定的 desired state 和 current/observed state 情況下也能正確運行,不管這中間有多少更新的資訊被丟失了。Edge-triggered 只能用來進行最佳化(應該有一個類似於 CAP 的理論去指導我們權衡應該輪詢還是使用事件驅動的方式去控制我們的流程,在高效能,可靠性和簡單些三者之間選其二)。
假定我們的系統是一個開放的環境:應該不斷的去驗證系統假設,優雅地接受外部的事件和修改。比如使用者可以隨意地刪除正在被 replica set 管理的 pods,而 replica set 發現了之後只是簡單的重新建立一個新的pod 而已。
不要為 object 建立大而全的狀態機器,從而把系統的行為和狀態機器的變遷關聯起來。
不要假設所有的組件都能正常運行,任何組件都有可能出錯或者拒絕你的請求。etcd 可能會拒絕寫入,kubelet 可能會拒絕 pod, scheduler 可能會拒絕調度,盡量進行重試或者有別的解決方案。
系統組件能夠自愈:比如說 cache 需要週期性進行同步,這樣如果有一些 object 被錯誤的修改或者儲存了, 刪除的事件被丟失等問題能夠在人類發現之前被自動修複。
優雅地進行降級和熔斷,優先滿足最重要的功能而忽略一些無關緊要的小錯誤。
Kubernetes API 習俗
Spec and status
Spec 表示系統希望到達的狀態,Status 表示系統目前觀測到的狀態。
PUT 和 POST 的請求中應該把 Status 段的資料忽略掉,Status 只能由系統組件來修改。
有一些對象可能跟 Spec 和 Status 模型相去甚遠,可以吧 Spec 改成更加適合的名字。
如果對象符合 Spec 和 Status 的標準的話,那麼除了 type,object metadata 之外不應該有其他頂級的欄位。
Status 中 phase 已經是 deprecated。因為 pahse 本質上是狀態機器的枚舉類型,它不太符合 Kubernetes 系統設計原則, 並且阻礙系統發展,因為每當你需要往裡面加一個新的 pahse 的時候你總是很難做到向後相容性,建議使用 Condition 來代替。
Primitive types
避免使用浮點數,永遠不要在 Spec 中使用它們,浮點數不好正常化,在不同的語言和電腦體繫結構中有 不同的精度和表示。
在 JavaScript 和其他的一部分語言中,所有的數字都會被轉換成 float,所以數字超過了一定的大小最好使 用 string。
不要使用 unsigned integers,因為不同的語言和庫對它的支援不一樣。
不要使用枚舉類型,建立一個 string 的別名資料型別。
API 中所有的 integer 都必須明確使用 Go(int32, int64), 不要使用 int,在32位和64位的作業系統中他們的位元不一樣。
謹慎地使用 bool 類型的欄位,很多時候剛開始做 API 的時候是 true or false,但是隨著系統的擴張,它可能 有多個可選值,多為未來打算。
對於可選的欄位,使用指標來表示,比如 *string *int32 , 這樣就可以用 nil 來判斷這個值是否設定了, 因為 Go 語言中string int 這些類型都有零值,你無法判斷他們是沒被設定還是被設定了零值。
總結
為 Kubernetes 拓展一個功能,實現一個 controller 是簡單的。
但是設計一個系統,抽象出其中的設計哲學,更加值得我們學習和深思。
下面這個項目可以視為 controller 的一個例子:
https://github.com/caicloud/loadbalancer-controller
References
Client-go:
https://github.com/kubernetes/client-go/tree/master/examples
Kubernetes controller:
https://github.com/kubernetes/kubernetes/tree/master/pkg/controller
Ingress controller task queue:
https://github.com/kubernetes/ingress/tree/master/core/pkg/task
Use client go to extend Kubernetes API — Xu Chao:
https://my.oschina.net/caicloud/blog/829365
Loadbalacner-controller:
https://github.com/caicloud/loadbalancer-controller