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

語言: CN / TW / HK

在 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 開源地址:https://github.com/slime-io/slime

作者簡介

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


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