1. 程式人生 > 其它 >kubernetes client-go pitfall

kubernetes client-go pitfall

作為雲原生開發人員難免會給 kubernetes client-go 打交道,但是有許多坑總是一遍又一遍的被開發者踩到,下面梳理常見的坑,希望大家注意避免:

informer cache中的資料是隻讀的, 任何修改都先deepcopy

informer cache中的資料是隻讀的, 任何修改都應該先deepcopy出來,然後提交apiserver, 利用apiserver informer event重新同步回cache中。 如果直接修改cache中的資料,就會出現資料不一致, 程式表現異常, 更嚴重的是, cache中底層實現是一個非執行緒安全的map, 如果多個groutine 併發讀寫map的現象, 程式直接fatal, 且不可recover。

 

更新資源需要RetryOnConflict

kubernetes 中的資源併發更新模型是樂觀鎖的機制, 每個資源都有一個單調遞增的resourceVersion。 當客戶端傳送一個修改操作時,會攜帶resoruceVersion, apiserver收到請求進行修改之前會判斷該version 是否小於最新的resourceVersion,如果小於就意味著這個資源已經被其他人修改,返回409 錯誤碼,並提示需要衝突重試。 客戶端收到該錯誤後需要進行重試, 目前kubernetes已經封裝好對應的庫, retry.RetryOnConflict。具體使用案例可以參見; Golang retry.RetryOnConflict函式程式碼示例

 

client-go 中訪問apiserver 預設QPS 為5

kubernetes client-go 會預設進行客戶端限流, 訪問apiserver的預設併發為5。 對於一些併發比較大的服務來說,顯然是不可接受的, 經常會出現一個服務上線時表現正常, 但是突然某一天出現超時,反應變慢。這時候就需要考慮是不是因為訪問apiserver被限流了。

建立client時傳遞的 rest.Config可以進行配置客戶端併發度。客戶端限流底層實現是一個令牌桶, 通過兩個引數來調節: 1). QPS: 表示每秒加入桶的token數量,用於控制併發 2). Burst:表示桶的大小,控制允許的激增請求量。 其中Burst需要設定的比QPS 大,Client-go不會校驗該引數。 具體的數值根據服務負載和apiserver的負載進行權衡。

當發生客戶端限流時,其實klog是會列印throttling字樣,注意觀察日誌定位改問題。

 

Informer中cache 可以自定義索引

informer 底層是預設是根據namespace/name為key儲存在map裡的, 雖然Infomer中內嵌的Lister提供了List(selector labels.Selector) 方法, 但是該方法只是list所有元素之後再進行過濾, 如果元素較多會導致處理時間較長。

可以通過構建索引來優化訪問速度:

1). 新增索引: 需要傳入一個生成索引key的函式

2). 使用索引:

這樣我們就可以根據某個label value構建索引,快速地從Informer cache中獲取結果。

 

List 資源時指定resourceVersion=0將從apiserver cache中獲取

當前我們叢集裡有大量的資源,如果有list請求, 可以顯式指定metav1.ListOptions{ResourceVersion: 0} , 這樣可以請求直接從apiserver的cache中返回, 不會穿透到etcd中。 不僅可以保護etcd,而且能夠更快返回。k8s informer在啟動的時候resourceVersion即為0。

 

Informer ResyncPeriod

我們在編寫controller的時候,會註冊informer event handler, 在產生event的時候進行一些reconcile操作,這是基於事件觸發。 除了事件觸發外,健壯的程式也需要基於時間的定期觸發,定期全量reconcile, 防止丟失某些event, 或者其他錯誤,相當於一種定期補償。

在建立informer的時候可以指定resyncPeriod, 該引數就是用於控制informer多久全量resync一次,resync時會將informer cache中的所有資源呼叫OnUpdateEventHandler, 不會請求apiserver。此時傳遞進來的引數 oldObjectResourceVersion==newObj.ResourceVersion, 使用者可以根據resourceVersion來判斷是真正的update event還是由於resync觸發的update event。

kubernetes 中controlle-manager中的cnotroller預設是: 12h~24h的一個隨機數。

 

controller workqueue

在編寫controller時, 一種約定俗稱的規範是在Informer的event handler中將需要處理的resource 加入一個workqueue中, 而不是直接在event handler中處理,這樣做的好處很多:

  1. 佇列最重要的作用是防止同一個資源被多個groutine同時處理,防止衝突
  2. 佇列提供了很多高階的功能, 例如 rate limit, delay retry等功能
  3. 佇列具體削峰填谷的作用, 在變更較為頻繁時, event較多, 利用佇列可以將同一個資源的多次event 緩衝合併成一次reconcile。 而且可以建立多個消費者處理。

 

儘量使用shardInformer

每種資源型別都有獨立的informer, 但是建議還是使用shardInformer, shardInformer的多個使用方底層複用一個informer, 可以節省很多資源,例如可以降低apiserver壓力: 對apiserver的連線數, apiserver 資源序列化開銷, 客戶端的cache記憶體佔用開銷。

ContentType 設定為 PB

可以設定如下:

1 conf := &rest.Config{
2         Host:        "https://9.134.163.198:6443",
3         QPS:         10000,
4         Burst:       100000,
5         ContentConfig: rest.ContentConfig{
6             ContentType: "application/vnd.kubernetes.protobuf",
7         },
8 }

這樣可以極大的加快請求的耗時, 尤其是用Informer list 很多資源的時候, PB相對於json 更快的壓縮速度, 和更大的壓縮比。當前預設設定為json, 需要手動設定 contentType為 protobuf。 注意CRD 不能設定CotentType=PB , 因為CRD 沒有實現對應protobuf marshal interface。建立的時候會報錯如下: “object xxxx does not implement the protobuf marshaling interrace and cannot be encoded to a protobuf message"

 

ClientConfig設定Timeout

顯式設定ClientConfig timeout,目前建立的k8s client 底層用的是http2協議, 會複用connection, 如果網路抖動, 會導致connection hang死, 需要等很久底層作業系統將連結重置之後應用層才能恢復,如果是http1協議的話, 因為每個請求都是傳送一個新的出去, 如果網路問題會直接返回失敗。 為了避免底層過長的超時時間,我們可以手動設定一個短一點的超時時間。

SharedInformer WaitForCacheSync 返回並不代表 eventHandler 執行完畢

我們使用informer的典型的一個順序是 1).註冊各種 eventHandler 2). 啟動sharedInformer 3). 等待Informer 同步完成

往往我們認為 informer 同步完成對應的eventHander 也被呼叫成功, 但實際上並不是如此。ShardInformer 中的WaitForCacheSync 返回只是代表對應的事件已經分發下去了, eventHandler 是非同步執行的, 並不保證執行時間。 sharedInfomer 往往會註冊多個eventHandler , 如果是同步執行的話, 某個Handler 處理的特別慢的話會影響其他handler 的及時被呼叫, 所以每個handler 都是非同步執行的。對於一些嚴重依賴handler 執行的邏輯需要特別小心, 例如我們維護了一個cache, 在每次有event 的時候更新這個cache, 當WaitForCacheSync 返回的時候我們不能直接開始執行業務邏輯,去讀取這個cache, 很可能這個cache 還沒有完全執行完畢。但是對於單個informer, WaitForCacheSync 執行完畢後,eventHandler 也執行完畢了。

 

併發更新 CRD 造成全量更新relist

高併發更新/增加很容易導致apiserver的watchCache的快取被穿透,從而導致所有連線其上的informer 在rewatch時出現“too old resource version”,從而引發relist動作,加劇Apiserver的記憶體劇增。

 

Informer EventHandler 中Ondelete的時候 需要判斷DeletedFinalStateUnknown

在Informer EventHandler 中OnDelete 被呼叫的時候, 需要判斷 DeletedFinalStateUnknown 狀態, 典型的使用場景如下:

DeletedFinalStateUnknown 一般出現在由於網路中斷等原因導致informer 本地cache 和apiserver 不一致, 等informer relist的時候發現informer cache中存在這個Object, 但是apiserver 沒有, 就會觸發一個DeletedFinalStateUnknown。 使用者需要顯式的轉換會原來的物件。

Reference:

Writing Controllers