IstioCon 回顧 | 網易數帆的 Istio 推送效能優化經驗

網易數帆發表於2022-05-24

在 IstioCon2022 上,網易數帆資深架構師方誌恆從企業生產落地實踐的視角分享了多年 Istio 實踐經驗,介紹了 Istio 資料模型,xDS 和 Istio 推送的關係,網易數帆遇到的效能問題和優化的經驗,以及一些相關的 Tips。

資料模型

從推送的角度,Istio 所做的事情,以做菜的過程類比,大致分為以下幾個部分:

首先是 “備菜”。 Istio 會對接、轉換、聚合各種服務註冊中心,將不同的服務模型的資料統一轉換為 Istio 內部的服務模型資料。早期的 Istio 實現裡面這是有定義的介面,做程式碼級實現,使用者可以去做註冊中心的實現和對接,但是這種方式對 Istio 的程式碼形成侵入,更便於開發者而不是使用者,所以 Istio 後續演進將其廢棄,取而代之的是定義了一個資料模型,ServiceEntry 這種 API 的資料結構,以及對應的一個 MCP 的協議,如果有擴充套件和外部整合的需求,可以單獨在外部元件裡面實現這個協議,將服務模型數值轉換之後再傳輸給 Istio,Istio 可以通過配置的方式對接到多個註冊中心、服務中心。因為服務資料可以認為是整個服務網格里面最基本的要素,所以我們把這一步比作是備菜的過程。

其次是 “加料、烹飪”。 這可能是使用者最為熟悉的部分,服務發現是服務網格的一個基本功能,但是 Istio 在整個服務網格中的優勢更多的是通過它所支援的豐富、靈活的治理的能力來體現的,所以這其實是將 Istio 定義的治理規則給 apply 進去,我們可以把它比作是加料、烹飪的過程。

再次是 “裝盤、擺盤”。 以上我們已經得到了最終要推送的 xDS 資料,但 Istio 裡面還有很多的程式碼是做各種部署、網路使用場景的適配,它會影響最終生成資料的一些特徵,這類似裝盤、擺盤的過程。

最後是 “出菜”, 我們將最終的資料以 xDS 配置的方式推送給資料面,這是資料面能夠理解的 “語言”,這個過程我們把它比作是出菜的過程。

一般來說,我們端上餐桌的 “菜”,相比之前準備好的那些 “食材” 已經面目全非了,你很難去了解它原來的樣子。xDS 的配置,看過的同學都知道,其實是非常的複雜的。

另外一個例子,我想把它比作 Kubernetes 模型的 reconcile 過程,基本上是一個 “watch-react-consistency” 這樣的迴圈,整個過程的複雜度,隨著輸入和輸出資源的數量和種類,幾乎是呈指數級別的增加的。

下面我整理了 Istio 上下游資源的關係,下游是指 xDS,上游是 Istio 自己定義出來的一些資料資源型別,用以支援豐富的治理能力以及場景適配。可以看到,每一種資源都會受到很多資源型別的影響,甚至是包括一些像 proxy 的特徵,這種東西我們認為是一個非常執行時的資料,這些資料都會影響到最終生成的 CDS,所以說最終的效果應該是 “千人千面”,也就是說從推送的視角來看,我們希望不同的特徵影響到能獲取哪些資源,而不是說獲取到的同一個資源內容還不一樣,後一種方式非常不利於推送和優化。

xDS 和 Istio 推送

簡單看一下 xDS 協議,從推送的角度看說分成三類,第一類是 StoW(state-of-the-world),也是目前 Istio 預設的和主要的一個模式,它支援最終一致、實時計算(實際上是 Istio 使用它的一個姿勢)和全量推送,特點是簡單健壯,比較好維護,因為它每次都會去重新計算所有資料推送,所以我們在做功能開發的時候,不用太關心它的資料一致性或資料丟失,但是它也有一個不菲的代價,就是效能很差。

第二類是 delta xDS。 我們熟知的 delta,一般有一些前提條件,這樣實現起來會比較方便一些。比方說將全域性的各種資源版本化(如 GVK + NamespacedName + version),推送的時候根據 offset 來推送新增的差異部分,這是一個典型的 delta 的實現思路。但是對目前 Istio 的資料模型來說,是無法生成這種比較徹底的 delta 模式的。目前我瞭解到的社群的 delta 的實現,實際上是做了一個降級,還是為每個 proxy 去做實時計算,對可列舉的特徵值進行快取,如果兩個 proxy 的特徵完全相同,那麼它們獲取的配置資料應該是一樣的。這個思路要求特徵值是可列舉的,目前實際只有少數的資源型別(CDS、EDS)和場景(只有 ServiceEntry 變更)能夠實現,否則會倒回到全量推送 ——delta xDS 在設計的時候,考慮到了這個場景,支援下發內容多於訂閱內容。

最後一類是 on demand xDS, 目前 Istio 還沒有開始使用。它大體的思路是,Envoy 在實際用到某個資源的時候才會去做請求,它有一個預設前提,就是 下發配置大部分 proxy 是不會用到的,這個前提是否能成立,我覺得不是特別的確定,而且它好像目前只支援 VHDS。

我們再回到 Istio 的視角,它的推送其實只有兩種,一種叫 non-full push,一種叫 full push。non-full push 是隻有 endpoint 發生變更的推送型別,只會做 EDS 推送,EDS 推送的特點,可以做到只推那些變更的 cluster,並且只推給那些對這些變更 cluster 做了 watch 的 proxy。從這個角度來說,non-full push,或者說 EDS push 比較接近於理想的推送。但是有一個場景是說我們的 cluster 非常大,比如說有一萬個 endpoint,這個時候我們只變了少數的 endpoint,我們還是會因為這些少數的 endpoint 的變更,去將這一萬個 endpoint 的 cluster 重新下發。

這個場景根據企業規模可能不是特別常見,而且它相比 full push 來說已經做得非常好了,因為後者所有型別的變更都會觸發 full push,不僅重新推送所有型別的資料,每種型別的資料都是全量推送,而且是推送給所有的 proxy。當然這是它的最初形態,Istio 在不斷的演進過程中也在不斷優化,引入了一些 scoping 的機制,大體的思路是,它會盡量做到只把每個 proxy 需要的內容全量推送,也只推送給那些當前變更影響到的 proxy。這句話可能說起來比較簡單,但實際上非常困難,因為前面我們也分析了,Istio 裡面非常複雜或者靈活的那些能力,決定了它的上下游的配置的關係是非常難以理清楚的。

接下來講 Istio 推送的 3W,第一個是 when,它其實是一個事件驅動型別,只有外部變更的場景它才會去做一些推送。思路是前面提到的最終一致、實時計算和全量推送,有少量的主動觸發場景。

第二個是 who,控制面會儘量判斷這個變更會影響到哪些 proxy,下面列出了在實現中 Istio 是通過哪些機制去判斷變更的 config 或者是 service 會影響到哪些 proxy,可以通過型別的判斷,可以通過具體的明確到資源的判斷,還有上下游的型別。通過型別去判斷是比較粗略的,而且需要一定的維護成本,因為某一個型別的推送,比如說 AuthorizationPolicy 這個 CR 的變更不會影響到 CDS。這是基於當前實現得出的結論,如果 Istio 在後續演進中引入新的特性,邏輯變更導致依賴關係發生變化,可能會漏掉,所以有一定的侷限性。

另外一種明確到資源的依賴關係是更明確的,目前它其實是通過 PushRequest.ConfigUpdated 去和 SidecarScope.configDependencies 作比較,再決定 sidecar 是不是受到這個 configure 的影響,但它目前也有一些侷限性,只支援 sidecar 型別的 proxy,而且只限於 sidecar 能夠約束的 service、vs、dr、sidecar 等四種資源。網易數帆目前在做的另外一個事情,是把 configDependencies 能做到 proxy 級別,這樣的話它幾乎可以覆蓋全部的資源型別。

最後一個是 what,就是控制面儘量判斷出需要更新的內容,以及 proxy 是不是需要它,或者說是 proxy 需要的內容,因為 proxy 需要的內容是一個下游配置 xDS,需要更新的內容也是這個。這影響到的是推送的資料量,先確定哪些 proxy 是需要被影響到的,同時是需要推送的,再確定需要推送哪些,這是通過一些像資源和 workload 的依賴關係來描述,有一些資源支援 workload selector,還有一些資源是通過 Sidecar 約束的。

遇到的效能問題和優化經驗

介紹完了背景,接下來主要分享網易數帆所經歷過的效能問題,和對應的優化的經驗。

MCP(over-xDS)效能問題

首先第一個問題是 MCP(over-xDS)效能問題,我們說 MCP 一般是指老版本的 MCP 協議,因為新版本的協議已經換成 xDS 了,只是保留了 MCP 協議裡面的資料結構,但是稱呼上一時半會兒改不過來。這個效能問題主要還是因為它的推送模式,因為它其實也是一個 xDS 協議,Istio 本身就實現了這個 MCP(over-xDS),也就意味著我們的一個 Istio Pilot,其實可以將另外一個 Istio Pilot 作為它的配置來源 。這可能是社群的一個有意的設計,它的模式是一致的,所以也會有 StoW 模式的問題 —— 任何型別的資源的變更,哪怕只是一個變更,都會導致同型別資源的全量推送。對於我們的場景,ServiceEntry 和 VirtualService 都是五位數量級的,傳輸或者寫放大也就是五位數量級的,所以開銷是過大的。

我們的優化方式有兩點,主要一點是支援增量推送,當然跟社群的思路也一樣的,我們沒有實現完全的 delta xDS,只是實現了一個對應的語義,但是最終的效果我們確實是做到了增量推送。它有兩個要點,一是 MCP Server 要⽀持 ResourceVersion 的註解,這個時候它才能夠把一些版本資訊告訴 MCP Client ;二是 MCP Client 要強化對該註解的支援,目前社群的主幹程式碼裡面已經有一部分支援,但不夠完整,我們後面做的一個事情就是當 ResourceVersion 沒有變化的時候,我們會跳過他的更新,這相當於是社群思路的一個延續。

另外一點是我們的 MCP Server 支援了 Istio Revision 的資源隔離,因為當前社群 Revision 的一個思路是 client side 的過濾,就是說我先收到了所有的資料,再根據我的 Revision 做一個過濾,這樣傳輸的資料量還是比較大的。我們在做的這個增強之後,MCP Server 會根據 client 的 Revision 來做一個過濾,從而降低傳輸的資料量。

ServiceEntryStore 的資料處理效能問題

第二個是 ServiceEntryStore 的資料處理效能問題。簡單地說,它裡面有一個步驟,會全量更新例項的索引,這意味著如果我的 service 有一個裝置發生變化了,它會去更新全部 service 的索引,這也是一個量級非常大的寫放大,優化的思路也比較接近,我們沒有去硬改 Istio 的主流程,而是在原來的 refreshIndexe 基礎上,對它的索引更新操作做了一個聚合,優化的效果非常明顯。這塊社群的程式碼其實是有一些重構的,具體的效果我們還沒有確認。

還有一個是 servicesDiff 沒有略過 CreateTime、Mutex 欄位,這兩個欄位的值是經常不一樣的,這就導致結果不準確,有些時候只有 endpoints 變更,但是這樣比較出來的結果,就會導致它升級為 service 變更,從⽽ non-full push 也會被升級為 full-push。這主要是一個演進的問題,社群最新程式碼 Service 中這兩個欄位要麼刪除要麼沒賦值,所以此問題可以忽略。

啟動時資料初始載入的 “雪崩效應”

第三個問題是一個比較大的問題,簡要的說是 Istio 在做大量的配置變更,尤其是做初始載入的時候,會載入所有的資料,每一個新的資料都會被認為是一個變更,可能導致雪崩的效應。雪崩效應一般都是由正反饋帶來的,這裡有兩個場景的正反饋,第一個是每個服務變更都會去重新整理全部服務的快取,我們實際的服務是一個一個地裝載進來的,快取重新整理量就是 O (n^2),計算量非常大。優化方式前面也有提到,就是做一個聚合。

第二個場景比較複雜,背景是前面提到的,服務變更配置變更都會觸發 full-push,全稱是 full-config-update,包含 full-update 和 full-push 兩個階段,full-update 做 Istio 內部的資料更新, full-push 拿所有的被影響到的 proxy,然後對每個 proxy 做資料生成,再推送給每一個 proxy,這兩步的開銷都是非常大的。Istio 對這個場景其實是做了一個抑制抖動,如果發現有高頻的變更,它會對這些變更做一個抑制,效果是比較不錯的,但是有一個問題,抑制抖動主要是抑制的是瞬時和集中的大量變更,而這個大量變更如果因為某種原因被拖慢了,持續時間更長了,就會導致抑制抖動的設計失效。我們如果有這樣的資料裝載的時候,以及有其他的雪崩邏輯導致計算量放大的時候,會出現非常嚴重的 CPU 爭用,因為 Istio 的內容變更觸發的更新和推送都是一個強 CPU 性加上強 I/O 性,如果它有 proxy 需要推的話,CPU 爭用會使流程變得更慢。這兩點是互為正反饋的,會導致更多的 full update,就會更慢。整個流程還不止於此,這個時候如果有 proxy 連上來,我們還要去疊加一個正比於 proxy 數量的 full-push 開銷,整個情況會更加惡化,配置裝載就會持續更長時間。另外嚴重瓶頸的時候對 proxy 的 push 會超時,proxy 又會做斷鏈重連,這個過程會繼續惡化。最後一個是業務效果,載入時間特別長意味著整個資料載入完畢的時間比較晚,而 proxy 如果在這之前連線上來,會拿到不完整的配置,這樣會造成業務有損,因為這個 proxy 可能是重連,它之前是有完整的配置的。

以網易數帆的資料量級來說,如果上述的優化都沒有做,我們需要一二十分鐘甚至更長的時間才能達到穩定,但在這期間可以認為系統是不可用的。優化的思路大概有幾點:一是前面提到過的優化 endpoint index 更新的寫放大;二是優化整個系統對 ready 的判斷的邏輯,因為 Istio 本身有一個邏輯,只有當它認為元件都已經 ready 的時候,才會把自己標記為 ready,但這其實是有問題的;三是最重要的一點,我們是引入了一個對變更的管理,原先單純的抑制已經不足以去覆蓋這種場景了,所以我們做了一個類似於推送狀態的管理器,在抑制的基礎上再加上了一些啟停的控制。最終的效果是,我們優化後同等規模的啟動時間大概是 14 秒,並且中間是沒有狀態誤判,也沒有業務受損的。

執行時的大量服務 / 配置變更

還有一個關聯問題,就是如果執行時有大量的服務變更會怎麼樣?執行時雖然沒有這麼大的概率會出現,但是它有一些條件也不一樣,比如此時我們的 Pilot 就是 ready 的,Envoy 就是連上來了,這個時候挑戰也是比較大的,它的大規模變更一般來說會有三種場景,一種場景是業務有一些非規範的釋出,比如短時間內新增大量的服務,或者新增大量的配置,此時我們需要保證自己的健壯性;第二是上游的 bug,比如說配置來源的一些 bug 導致的頻繁變更、推送,比較少見;第三個是上游的 MCP Server 的重啟,對於 Kubernetes 裡面的資源來說,它是帶版本號的,可以基於版本的檢查只推送增量部分,但是對於轉換出來的 ServiceEntry,它沒有持久化的版本號,可能會導致大量的假更新。

對應的思路,是一條一條地針對性地處理。網易數帆引入了幾種優化方式,一是做有條件的批處理,場景比如 MCP,它的互動方式是一次性推送同一個型別的所有的資料,這就意味著變更量是很大的,我們就模擬一個事務提交的機制,我們會在事務週期內禁用推送。再一個場景,如果本身是一個非批處理,我們會加入一個防抖動機制,把連續的變更轉換成批處理。另外一點就是把資源版本化,生成的資源通過某種方式去引入一個持久化版本號,這樣可以減少不必要的變更。

proxy 接入先於系統 ready

還有一個場景,前面提到在我們系統沒有 ready 的時候 sidecar 會接入進來,這是因為目前 Istio 內部判斷元件是否 ready 的設計,它可能判斷得不夠精確,在大規模的資料和高壓的情況下會被放大出來。用一句話來概括,就是中間有一個非同步處理的過程,這個過程在正常情況下是比較快的,時序上的漏洞不會體現出來,但是高壓力的情況下,CPU 爭用嚴重,這個非同步過程的時間 gap 就會被放大,導致 Istio 錯誤地提前認為整個系統已經 ready。

優化的方式,一是優化效能,減少這種高壓力的場景,另外就是引入更多的元件 ready check 的機制。比如我們上游的 MCP,它之前也有類似的場景,沒有嚴謹地判斷是否 ready 就提前做資料下發,這個時候我們也是做額外的檢查。

密集變更問題

最後一個 case,就是我們會遇到一個密集的變更,比方說初始化載入,這時候 CPU 的高水位會使得 https 的 health check 失敗,從而 liveness probe 失敗乃至 pod 重啟,它的預設值只有 1 秒,https 的協商階段對 CPU 其實是有一定要求的,就會導致持續搶不到時間片,或者是排程不上,就會出現這種狀況。優化方式比較簡單粗暴,我們直接把超時改成 10 秒以上。

“⾃動” 服務依賴管理

後面講一下場景無關的優化,是一個老生常談的自動化服務依賴管理。前面也提到縮小下發給 proxy 的資料量是非常重要的事情,一個是少推一點資料給它,另外一個是無關的資料發生變更的時候不要推送給它,所以依賴關係非常重要。

Istio 目前提供了一個 sidecar 的 API,這個 API 人工維護是不太現實的,我認為這是把基礎設施上的能力不足轉換成使用者的操作風險,所以就有一個對自動化的剛性需求,我認為也是網格的一個必備元件。它核心的思路是通過實際的呼叫資料來生成和更新依賴的關係,同時在依賴關係完備之前我們要正確地兜底流量處理。

這方面大家也可以去了解一下我們開源的 Slime lazyload,現在我們在它的動態依賴關係維護的基礎上,也支援了比較靈活的半靜態的依賴關係描述,類似於條件匹配,可以用來實現一些高階的特性,所以我們現在更願意把它叫做 servicefence,在這個元件裡面,我們也沉澱了比較多的生產實踐的經驗。

一些 Tips

最後講幾個 Tips,不一定每個場景大家都會遇得到,但是會有一些思路可以借鑑。比方說我們有一些場景是連線不均衡,原生的是有一個用於自我保護的限流,如果有大量的連線或者請求發往單個節點,單個 pod 的控制面的時候,它就會去做一個限流的保護,但是這並不能使得不均衡的情況重新均衡,所以我們有一個元件會去做均衡的調配,也已經開源。

另外一個場景比較特殊,如果我們有海量的 endpoint,它的記憶體使用是一個比較大的問題,這個時候我們可以考慮對可列舉的內容用字串池優化,尤其像 label 這種重複度比較高的。

還有前面提到的,如果我有超大的 cluster,這時候 EDS 推送甚至都會成為問題啊,解決思路是大資源拆小資源,曾經有一個方案叫 EGDS,在社群有有過一些討論,雖然說最終是沒有進到社群主幹,但是可以作為一個解決問題的思路參考,Kubernetes 其實也是有類似的思路。

最後一個是說我們如果是有超大規模的服務註冊中心,我們的控制面已經無法承擔所有的配置資料或者服務資料了。這時候我們可以考慮去做控制面的分組,讓 sidecar 去基於依賴關係的親和性,來讓每一個控制面只處理一部分的配置。

結語

我今天要分享的內容就是這些。最後分享一下我個人做了幾年服務網格優化的感受,Istio 的很多設計確實比較符合當下軟體工程的一個務實路徑,先採用一個高可靠的方式來做快速演進,當然曾經欠下的東西,在後面的實踐中還是都要補回來的。謝謝!

相關連結

網易數帆如何基於 Istio 實現微服務架構演進

網易的 Service Mesh 之路:Istio 會是下一個 K8s 嗎?

網易開源 Slime:讓 Istio 服務網格變得更加高效與智慧

Slime 2022 展望:把 Istio 的複雜性塞入智慧的黑盒

Slime 開源地址:https://github.com/slime-io/s...

作者簡介

方誌恆,網易數帆資深架構師,負責輕舟 Service Mesh,先後參與多家科技公司 Service Mesh 建設及相關產品演進。從事多年基礎架構、中介軟體研發,有較豐富的 Istio 管理維護、功能擴充和效能優化經驗。

2022 年 5 月 13 日至 6 月 15 日,Loggie 社群面向雲原生、可觀測性及日誌技術愛好者發起 Loggie Geek Camp 開源協作活動,以 “效能之巔,觀測由我” 為主題,讓參與者感受開源文化的精髓與開源社群的創造力,共創雲原生可觀測性的未來。包括提供 user case、捕捉 bug、完善和提交 feature 等四類任務,提交內容通過社群稽核即為成功,表現優異者將可獲得網易數帆及 Loggie 社群表彰。歡迎瞭解和參與:https://sf.163.com/loggie

相關文章