一個看似"沒用"的功能竟然涉及到這麽多的知識,需要做大量的工作。k8s 確實很復雜~
約定
k8s list 返回結果中的元素集合是按照字母順序從 a 到 z 升序排列的。
原因
這個約定存在的原因是為了保持開啟 WatchCache 功能前後 list 請求返回結果的一致性。在關閉 WatchCache 功能的情況下,請求直接透傳給 Etcd,此時返回的結果就是字母升序排列的。
實作
Etcd
在關閉 WatchCache 時 list 結果有序,這個能力是 Etcd 提供的。kube-apiserver 的實作中呼叫 Etcd API 執行 Range 操作的時候並沒有顯示指定 SortOrder 和 SortTarget,但最終卻返回了按照 key 排序的數據。如果直接使用 Etcdctl 去獲取指定 key collection 的話,不需要顯示指定順序,返回的結果也是有序的。
這就涉及到 Etcd Range 的實作,在不顯示設定排序順序和排序物件的時候,預設返回 key 按照字母升序排序後的結果,相關的程式碼如下
// 最終排序位置
func(ti *treeIndex)visit(key, end []byte, f func(ki *keyIndex)bool) {
keyi, endi := &keyIndex{key: key}, &keyIndex{key: end}
ti.RLock()
defer ti.RUnlock()
ti.tree.AscendGreaterOrEqual(keyi, func(item btree.Item)bool {
iflen(endi.key) > 0 && !item.Less(endi) {
returnfalse
}
if !f(item.(*keyIndex)) {
returnfalse
}
returntrue
})
}
如果是沿著呼叫鏈路一步步看的話,在前一步(預設線性一致性讀,需要等待 appliedIndex 趕上 confirmedIndex)返回數據時的程式碼中也有相關註釋。
func(a *applierV3backend)Range(ctx context.Context, txn mvcc.TxnRead, r *pb.RangeRequest)(*pb.RangeResponse, error) {
...
sortOrder := r.SortOrder
if r.SortTarget != pb.RangeRequest_KEY && sortOrder == pb.RangeRequest_NONE {
// Since current mvcc.Range implementation returns results
// sorted by keys in lexiographically ascending order,
// sort ASCEND by default only when target is not 'KEY'
sortOrder = pb.RangeRequest_ASCEND
}
...
}
註釋寫的很明顯了,預設情況下 mvcc.Range 就是返回按字母升序排序後的結果。
WatchCache
如果在開啟 WatchCache 的情況下呼叫 list 可能會發現結果並不是有序的,這是因為直到 v1.27 才開始加上有序的功能,換句話說就是從有了 WatchCache 開始直到 v1.27,list 都不是嚴格有序的,也就是沒有遵循之前的約定。如果你的客戶端依賴 list 結果的順序,那麽在使用 v1.27 之前的版本的時候可能就會遇到問題了,如果沒有遇到問題,說明客戶端對順序沒有依賴,而這也是大部份的場景,也正如此,這個問題才不容易被發現。
List 返回的是 WatchCache store 中的數據,而 store 中的數據是無序的。在 v1.27 Reuse generic GetList test for watchcache and fix inconsistency issues for both etcd3 and watchcache [1] 開始在從 store 返回最終數據時進行了主動排序的操作,原理很簡單,就是實作了 golang sort.Interface 介面,然後呼叫了 sort.Sort 方法對 slice 進行排序。
看似人畜無害的一個行為,引入了一個新的問題,sort.Sort 的執行是在加鎖的情況下執行的,而在 Reflector 處理每個從 Etcd 返回的 event 的時候也會進行加鎖操作,因此 list 操作就會對 event 的處理產生影響,導致一些不必要的處理延遲。而這個問題最終在 v1.30(尚未釋出) Don't sort in the critical p [2] 才得到修復,透過 defer 把排序操作放在了加鎖前面。
即使在沒有上述問題的情況下,針對每次請求都額外多一次排序操作是否會對 API 延遲或 kube-apiserver 記憶體有一些影響呢?或者影響有多大呢?這就和其使用的排序演算法有關了,上面提到使用 sort.Sort 進行排序,不同版本效能不一樣,最新的實作是 pdqsort [3] 演算法,是字節貢獻給社群的,其效能是之前的 2~60 倍,無需額外記憶體。所以對最終的記憶體沒有影響,對延遲的影響取決於數據量的大小,但相比於對數據進行序列化,網路傳輸等的耗時,此處排序的耗時顯得微乎其微。如果要較真的話,這裏還可以再修改一下,改成使用 slices.Sort 的方式,使用泛型 + pdqsort,耗時會更低。
那有沒有更好的辦法來實作返回有序的效果呢,能想到的一種方案是在處理 event 將資源物件保存到 WatchCache store 的時候就保持 store 有序,這樣可以避免每次 list 時的即時排序操作。但收益如何需要實作之後對比評估,理論上收益並不大。
WatchList
WatchCache 是將 store 中的數據排序後免洗的返回給客戶端,WatchList 為了最佳化 kube-apiserver 記憶體消耗,改用流的方式實作 list 的效果,參考 ,那麽他返回的結果也應該遵循規範做到按字母升序排列。
在最新的 v1.29.0 實作中 WatchList 還是 alpha 狀態,尚未做到嚴格有序,原因是 WatchList 的實作是用 watchCacheInterval 的數據(WatchCache store 或者 cyclic buffer 中的數據) + cacheWatcher input chan 中的數據作為 list 的結果,由於 cacheWatcher input chan 中的數據是按 resourceversion 排序的,而且必須是按 RV 排序,就會導致最終的數據無法嚴格字母升序。
社群也是在對此功能進行開發,目前存在兩個尚未合並的 PR, Ensure that initial events are sorted for WatchList [4] 和 storage/cacher: ensure the cache is at the Most Recent ResourceVersion when streaming was requested [5]。前者是對前半部份 watchCacheInterval 的數據進行排序,後者是用來保證 watchCacheInterval 中的數據已經是 list 所需的全量數據,也就是說不再需要從 cacheWatcher input chan 拼接數據了。
理論上透過上面兩個改動是可以實作 WatchList 下的 list 有序的,但同樣會引入新的問題,即當前的實作是伺服端收到 Watch 請求後會立馬開始往客戶端發送數據,而上述改動則需要伺服端等待 watchCacheInterval 是全量數據,排序後才開始往客戶端發送數據,這就引入了一個數據處理的延遲。也就是說處理方式變成了從當前的有一條數據就發一條的實作,變成了要先等到伺服端是全量數據後再開始一條一條的發送給客戶端。
那麽這個延遲會有多大呢,這個值跟 Etcd 的配置有關,當前的實作是依賴 ProgressNotify 的,透過 --experimental-watch-progress-notify-interval 控制 ProgressNotify event 發送的周期,預設是 10m,也就是說預設情況下會存在最大 10m + 1.25s 的延遲,這顯然是不可接受的。可以透過縮小此參數的值來縮短延遲,一般情況下設定為 5s,也就會有最大 5s + 1.25s 的延遲。這個延遲其實也很難讓人接受,其中的 1.25s 是來自 bookmarktimer 的 1s~1.25s 的定時周期導致的。社群計劃是利用 ConsistentRead 機制中已經使用到的 RequestProgressNotify 機制,由 kube-apiserver 周期性(100ms)主動請求 Etcd 發送 ProgessNotify 的原理把這個周期縮短到 100ms(1.25是否存在待定),此功能已經在 ConsistentRead FeatureGate 開啟後的 List 請求中使用到了,尚未在 WatchList 中支持。
這個延遲和 ConsistentRead/WatchList + ProgressNotify 機制有關,ConsistentRead/WatchList 都是先請求 Etcd 獲取當前最大的 RV,等待 Cache 數據追上 RV 之後才開始後續流程的,問題就在這個最大 RV,因為 Etcd 的 RV 是全域的,由於節點一直在上報 lease 狀態,就會導致 Etcd 中的最新 RV 一直在增加,但是資源物件的 RV 很可能是一直不變的。例如獲取 default namespace 下的所有 pods,即使在此 ns 下沒有任何 pod 的情況下使用 ConsistentRead 或者 WatchList 時也能感覺到明顯的延遲,延遲的大小和上面提到的 Etcd 參數有關,如果沒有顯示指定的話給人的感覺就是一直無法返回數據直到超時或者取消請求,避險問題,感興趣的話可以用最新版本(v1.29.0)測試下。
結束語
一個看似簡單的 list 有序,竟也會牽扯到這麽多的內容,而且還有很多尚未實作的功能。k8s 為了一個基本不會用到的 list 有序,需要做這麽多的工作,復雜度的提升可想而知,誰知道裏面還會有多少這種實際幾乎不會用到的功能呢?!
> > > >
參考資料
[1] pr#113730: https://github.com/kubernetes/kubernetes/pull/113730
[2] pr#122027: https://github.com/kubernetes/kubernetes/pull/122027
[3] pdqsort: https://blog.csdn.net/ByteDanceTech/article/details/124464192
[4] pr#120897: https://github.com/kubernetes/kubernetes/pull/120897/
[5] pr#122830: https://github.com/kubernetes/kubernetes/pull/122830
作者丨Kaku
來源丨公眾號:雲原生散修(ID:cloudnative_sanxiu)
dbaplus社群歡迎廣大技術人員投稿,投稿信箱: [email protected]