1. 程式人生 > 其它 >kubernetes設計精髓List/Watch機制和Informer模組詳解

kubernetes設計精髓List/Watch機制和Informer模組詳解

1.list-watch是什麼

List-watchK8S 統一的非同步訊息處理機制,保證了訊息的實時性,可靠性,順序性,效能等等,為宣告式風格的API 奠定了良好的基礎,它是優雅的通訊方式,是 K8S 架構的精髓。

2. List-Watch 機制具體是什麼樣的

Etcd儲存叢集的資料資訊,apiserver作為統一入口,任何對資料的操作都必須經過 apiserver。客戶端(kubelet/scheduler/controller-manager)通過 list-watch 監聽 apiserver 中資源(pod/rs/rc等等)的 create, update 和 delete 事件,並針對事件型別呼叫相應的事件處理函式。

那麼list-watch 具體是什麼呢,顧名思義,list-watch有兩部分組成,分別是list和 watch。list 非常好理解,就是呼叫資源的list API羅列資源,基於HTTP短連結實現;watch則是呼叫資源的watch API監聽資源變更事件,基於HTTP 長連結實現,也是本文重點分析的物件。以 pod 資源為例,它的 list 和watch API 分別為: [List API](https://v1-10.docs.kubernetes.io/docs/reference/generated/kubernetes-api/v1.10/#list-all-namespaces-63),返回值為 [PodList](https://v1-10.docs.kubernetes.io/docs/reference/generated/kubernetes-api/v1.10/#podlist-v1-core),即一組 pod`。

GET /api/v1/pods

Watch API,往往帶上 watch=true,表示採用 HTTP 長連線持續監聽 pod 相關事件,每當有事件來臨,返回一個WatchEvent

GET /api/v1/watch/pods

K8S 的informer 模組封裝 list-watch API,使用者只需要指定資源,編寫事件處理函式,AddFunc, UpdateFunc和 DeleteFunc等。如下圖所示,informer首先通過list API 羅列資源,然後呼叫 watch API監聽資源的變更事件,並將結果放入到一個 FIFO 佇列,佇列的另一頭有協程從中取出事件,並呼叫對應的註冊函式處理事件。Informer還維護了一個只讀的Map Store 快取,主要為了提升查詢的效率,降低apiserver 的負載。

3.Watch 是如何實現的

List的實現容易理解,那麼 Watch 是如何實現的呢?Watch是如何通過 HTTP 長連結接收apiserver發來的資源變更事件呢?

祕訣就是Chunked transfer encoding(分塊傳輸編碼),它首次出現在HTTP/1.1。正如維基百科所說:

HTTP 分塊傳輸編碼允許伺服器為動態生成的內容維持 HTTP 持久連結。通常,持久連結需要伺服器在開始傳送訊息體前傳送Content-Length訊息頭欄位,但是對於動態生成的內容來說,在內容建立完之前是不可知的。使用分塊傳輸編碼,資料分解成一系列資料塊,並以一個或多個塊傳送,這樣伺服器可以傳送資料而不需要預先知道傳送內容的總大小。

當客戶端呼叫 watch API 時,apiserver 在responseHTTP Header 中設定 Transfer-Encoding的值為chunked,表示採用分塊傳輸編碼,客戶端收到該資訊後,便和服務端該連結,並等待下一個資料塊,即資源的事件資訊。例如:

 1 $ curl -i http://{kube-api-server-ip}:8080/api/v1/watch/pods?watch=yes
 2 HTTP/1.1 200 OK
 3 Content-Type: application/json
 4 Transfer-Encoding: chunked
 5 Date: Thu, 02 Jan 2019 20:22:59 GMT
 6 Transfer-Encoding: chunked
 7 
 8 {"type":"ADDED", "object":{"kind":"Pod","apiVersion":"v1",...}}
 9 {"type":"ADDED", "object":{"kind":"Pod","apiVersion":"v1",...}}
10 {"type":"MODIFIED", "object":{"kind":"Pod","apiVersion":"v1",...}}

4. 談談 List-Watch 的設計理念

當設計優秀的一個非同步訊息的系統時,對訊息機制有至少如下四點要求:

  • 訊息可靠性
  • 訊息實時性
  • 訊息順序性
  • 高效能

    首先訊息必須是可靠的,list 和 watch 一起保證了訊息的可靠性,避免因訊息丟失而造成狀態不一致場景。具體而言,list API可以查詢當前的資源及其對應的狀態(即期望的狀態),客戶端通過拿期望的狀態和實際的狀態進行對比,糾正狀態不一致的資源。Watch API 和 apiserver保持一個長連結,接收資源的狀態變更事件並做相應處理。如果僅呼叫 watch API,若某個時間點連線中斷,就有可能導致訊息丟失,所以需要通過list API解決訊息丟失的問題。從另一個角度出發,我們可以認為list API獲取全量資料,watch API獲取增量資料。雖然僅僅通過輪詢 list API,也能達到同步資源狀態的效果,但是存在開銷大,實時性不足的問題。

    訊息必須是實時的,list-watch 機制下,每當apiserver 的資源產生狀態變更事件,都會將事件及時的推送給客戶端,從而保證了訊息的實時性。

    訊息的順序性也是非常重要的,在併發的場景下,客戶端在短時間內可能會收到同一個資源的多個事件,對於關注最終一致性的 K8S 來說,它需要知道哪個是最近發生的事件,並保證資源的最終狀態如同最近事件所表述的狀態一樣。K8S 在每個資源的事件中都帶一個 resourceVersion的標籤,這個標籤是遞增的數字,所以當客戶端併發處理同一個資源的事件時,它就可以對比 resourceVersion來保證最終的狀態和最新的事件所期望的狀態保持一致。

    List-watch 還具有高效能的特點,雖然僅通過週期性呼叫list API也能達到資源最終一致性的效果,但是週期性頻繁的輪詢大大的增大了開銷,增加apiserver的壓力。而watch 作為非同步訊息通知機制,複用一條長連結,保證實時性的同時也保證了效能。

5. Informer介紹

Informer 是 Client-go 中的一個核心工具包。在Kubernetes原始碼中,如果 Kubernetes 的某個元件,需要 List/Get Kubernetes 中的 Object,在絕大多 數情況下,會直接使用Informer例項中的Lister()方法(該方法包含 了 Get 和 List 方法),而很少直接請求Kubernetes API。Informer 最基本 的功能就是List/Get Kubernetes中的 Object。

6. Informer 設計思路

6.1 Informer 設計中的關鍵點

為了讓Client-go 更快地返回List/Get請求的結果、減少對 Kubenetes API的直接呼叫,Informer 被設計實現為一個依賴Kubernetes List/Watch API、可監聽事件並觸發回撥函式的二級快取工具包。

6.2 更快地返回 List/Get 請求,減少對 Kubenetes API 的直接呼叫

使用Informer例項的Lister()方法,List/Get Kubernetes 中的 Object時,Informer不會去請求Kubernetes API,而是直接查詢快取在本地記憶體中的資料(這份資料由Informer自己維護)。通過這種方式,Informer既可以更快地返回結果,又能減少對 Kubernetes API 的直接呼叫。

6.3 依賴 Kubernetes List/Watch API

Informer 只會呼叫Kubernetes List 和 Watch兩種型別的 API。Informer在初始化的時,先呼叫Kubernetes List API 獲得某種 resource的全部Object,快取在記憶體中; 然後,呼叫 Watch API 去watch這種resource,去維護這份快取; 最後,Informer就不再呼叫Kubernetes的任何 API。

用List/Watch去維護快取、保持一致性是非常典型的做法,但令人費解的是,Informer 只在初始化時呼叫一次List API,之後完全依賴 Watch API去維護快取,沒有任何resync機制。

筆者在閱讀Informer程式碼時候,對這種做法十分不解。按照多數人思路,通過 resync機制,重新List一遍 resource下的所有Object,可以更好的保證 Informer 快取和 Kubernetes 中資料的一致性。

諮詢過Google 內部 Kubernetes開發人員之後,得到的回覆是:

在 Informer 設計之初,確實存在一個relist無法去執 resync操作, 但後來被取消了。原因是現有的這種 List/Watch 機制,完全能夠保證永遠不會漏掉任何事件,因此完全沒有必要再新增relist方法去resync informer的快取。這種做法也說明了Kubernetes完全信任etcd。

6.4 可監聽事件並觸發回撥函式

Informer通過Kubernetes Watch API監聽某種 resource下的所有事件。而且,Informer可以新增自定義的回撥函式,這個回撥函式例項(即 ResourceEventHandler 例項)只需實現 OnAdd(obj interface{}) OnUpdate(oldObj, newObj interface{}) 和OnDelete(obj interface{}) 三個方法,這三個方法分別對應informer監聽到建立、更新和刪除這三種事件型別。

在Controller的設計實現中,會經常用到 informer的這個功能。

6.5 二級快取

二級快取屬於 Informer的底層快取機制,這兩級快取分別是DeltaFIFO和 LocalStore。

這兩級快取的用途各不相同。DeltaFIFO用來儲存Watch API返回的各種事件 ,LocalStore 只會被Lister的List/Get方法訪問 。

雖然Informer和 Kubernetes 之間沒有resync機制,但Informer內部的這兩級快取之間存在resync 機制。

6.6 關鍵邏輯介紹

1.Informer 在初始化時,Reflector 會先 List API 獲得所有的 Pod

2.Reflect 拿到全部 Pod 後,會將全部 Pod 放到 Store 中

3.如果有人呼叫 Lister 的 List/Get 方法獲取 Pod, 那麼 Lister 會直接從 Store 中拿資料

4.Informer 初始化完成之後,Reflector 開始 Watch Pod,監聽 Pod 相關 的所有事件;如果此時 pod_1 被刪除,那麼 Reflector 會監聽到這個事件

5.Reflector 將 pod_1 被刪除 的這個事件傳送到 DeltaFIFO

6.DeltaFIFO 首先會將這個事件儲存在自己的資料結構中(實際上是一個 queue),然後會直接操作 Store 中的資料,刪除 Store 中的 pod_1

7.DeltaFIFO 再 Pop 這個事件到 Controller 中

8.Controller 收到這個事件,會觸發 Processor 的回撥函式

9.LocalStore 會週期性地把所有的 Pod 資訊重新放到 DeltaFIFO 中

關鍵設計

Informer依賴Kubernetes的List/Watch API。 通過Lister()物件來List/Get物件時,Informer不會去請求Kubernetes API,而是直接查詢本地快取,減少對Kubernetes API的直接呼叫。

Informer 只會呼叫 Kubernetes List 和 Watch 兩種型別的 API。Informer 在初始化的時,先呼叫 Kubernetes List API 獲得某種 resource 的全部 Object,快取在記憶體中; 然後,呼叫 Watch API 去 watch 這種 resource,去維護這份快取; 最後,Informer 就不再呼叫 Kubernetes 的任何 API。

Informer元件:

  • Controller
  • Reflector:通過Kubernetes Watch API監聽resource下的所有事件
  • Lister:用來被呼叫List/Get方法
  • Processor:記錄並觸發回撥函式
  • DeltaFIFO
  • LocalStore

DeltaFIFO和LocalStore是Informer的兩級快取。 DeltaFIFO:用來儲存Watch API返回的各種事件。 LocalStore:Lister的List/Get方法訪問。

我們以 Pod 為例,詳細說明一下 Informer 的關鍵邏輯:

  1. Informer 在初始化時,Reflector 會先 List API 獲得所有的 Pod
  2. Reflect 拿到全部 Pod 後,會將全部 Pod 放到 Store 中
  3. 如果有人呼叫 Lister 的 List/Get 方法獲取 Pod, 那麼 Lister 會直接從 Store 中拿資料
  4. Informer 初始化完成之後,Reflector 開始 Watch Pod,監聽 Pod 相關 的所有事件;如果此時 pod_1 被刪除,那麼 Reflector 會監聽到這個事件
  5. Reflector 將 pod_1 被刪除 的這個事件傳送到 DeltaFIFO
  6. DeltaFIFO 首先會將這個事件儲存在自己的資料結構中(實際上是一個 queue),然後會直接操作 Store 中的資料,刪除 Store 中的 pod_1
  7. DeltaFIFO 再 Pop 這個事件到 Controller 中
  8. Controller 收到這個事件,會觸發 Processor 的回撥函式

之前說到kubernetes裡面的apiserver的只負責資料的CRUD介面實現,並不負責業務邏輯的處理,所以k8s中就通過外掛controller通過對應資源的控制器來負責事件的處理,controller如何感知事件呢?答案就是informer

基於chunk的訊息通知

watcher的設計在之前的文章中已經介紹,服務端是如何將watcher感知到的事件傳送給informer呢?我們提到過apiserver本質上就是一個http的rest介面實現,watch機制則也是基於http協議,不過不同於一般的get其通過chunk機制,來實現訊息的通知

本地快取

通過listwatch介面主要分為兩部分,list介面我們可以獲取到對應資源當前版本的全量資源,watch介面可以獲取到後續變更的資源,通過全量加增量的資料,就構成了在client端一份完整的資料(基於當前版本的),那後續如果要獲取對應的資料,就直接可以通過本地的快取來進行獲取,為此informer抽象了cache這個元件,並且實現了store介面,如果後續要獲取資源,則就可以通過本地的快取來進行獲取

無界佇列

為了協調資料生產與消費的不一致狀態,在cleint-go中通過實現了一個無界佇列來進行資料的緩衝,當reflector獲取到資料之後,只需要將資料寫入到無界佇列中,則就可以繼續watch後續事件,從而減少阻塞時間, 下面的事件去重也是在該佇列中實現的

為了協調資料生產與消費的不一致狀態,在cleint-go中通過實現了一個無界佇列來進行資料的緩衝,當reflector獲取到資料之後,只需要將資料寫入到無界佇列中,則就可以繼續watch後續事件,從而減少阻塞時間, 下面的事件去重也是在該佇列中實現的

事件去重

事件去重是指的,在上面的無界佇列中,如果針對某個資源的事件重複被觸發,則就只會保留相同事件最後一個事件作為後續處理

到此對於事件接收和資料快取相關優化就結束了,接下就是處理層的優化

複用連線

在k8s中一些控制器可能會關注多種資源,比如Deployment可能會關注Pod和replicaset,replicaSet可能還會關注Pod,為了避免每個控制器都獨立的去與apiserver建立連結,k8s中抽象了sharedInformer的概念,即共享的informer, 針對同一資源只建立一個連結

基於觀察者模式的註冊

因為彼此共用informer,但是每個元件的處理邏輯可能各部相同,在informer中通過觀察者模式,各個元件可以註冊一個EventHandler來實現業務邏輯的注入

設計總結


————————————————
原文連結:https://blog.csdn.net/weixin_43116910/article/details/88653263