攜程 Service Mesh 性能優化實踐

本文作者 Shirley 博、燒魚,來自攜程 Cloud Container 團隊,目前主要從事 Service Mesh 在攜程的落地,負責控制面的可用性及優化建設,以及推進各類基礎設施服務的雲原生化。

一、背景

攜程基於 SDK 模式已經有相對完善的微服務治理體系,但在業務全球化、混合多雲場景下,對基礎設施的標準化和解耦、可遷移性以及擁抱開源成爲新的訴求。以當前的業界實踐及趨勢來看,雲原生架構與體系是滿足上述訴求的最佳實踐。針對 Service Mesh 領域,Cloud Container&Service 團隊與框架團隊一同推進 Istio 在攜程的落地。

而在我們大規模推進 Service Mesh 的過程中,隨着接入的應用越來越多,Istio 控制面的性能遭遇了非常大的挑戰,包括下發數據的時延很長,推送的結果不透明,監控不完善,內存泄漏等等一系列問題,嚴重阻礙 Service Mesh 在攜程的落地。本文將着重分享解決 Istio 控制面性能與質量問題的一些方法、實踐和經驗,希望能給讀者以啓發和借鑑。

二、方法與目標

在介紹具體優化點之前,想先來聊一聊關於優化的方法論。我們認爲優化思路可以分爲三步走。第一步,梳理核心鏈路和場景,第二步,從需求側分析出 SLO,第三步,確立測試框架及度量框架驗證結果。這一思路幫助我們高效的完成了許多優化,同時也可以作爲基礎框架用在其他項目的優化中,下面對每一步的具體工作展開介紹。

2.1 梳理核心鏈路和場景

核心鏈路如上圖所示,對於 istio 控制面來說,它所關心的 k8s 裏的資源一旦被用戶們更改,就需要及時感知到這一事件,並快速的把變更下發到資源相關的 envoy。在這一過程中,我們總結出了需要關注的幾個場景。

1)隨着變更量的增加,istio 推送 xDS 延遲是否滿足要求

2)對於連接到不同控制面節點的每個 envoy 來說,istio 配置下發是否成功

3)當發佈或故障重啓時,istio 控制面啓動需要多長時間

2.2 從需求側分析出 SLO

經過對以上場景的驗證,再結合用戶需求,我們確立了以下目標:

1)在 3w serviceEntry 和 3w workloadEntry 的規模下,xDS 的推送時延 P95<3s,P99<5s

2)istio 配置下發成功率可度量

3)降低服務啓動的耗時到 5min

2.3 確立測試框架及度量框架

在制定了詳細目標之後,我們確立了詳細的測試體系,包括整理測試數據集,實現自動化測試工具,可以併發的增刪改多種資源,比如 workloadEntry、

serviceEntry 等等。

同時確立了詳細的度量框架,對於用戶來說,他們會關心自己的增刪改等操作是不是成功生效,如果沒生效還需要等多長時間,他們需要被告知一個確定性的結果。於是我們首先建立了較完善的監控體系,在必要路徑埋點,統計請求量、計算端到端推送時延、錯誤率等指標。除此之外提供查詢接口來獲取推送結果,來度量配置下發是否成功。

三. 優化方案及核心實現

3.1 優化 xDS 推送時延

(1)問題分析

在監控數據中,我們很快發現了 xDS 推送中的 CDS、RDS 的推送時延問題尤爲突出,在 3w + 的 envoyfilter 和 5000 + 的 virtualService 的規模下,CDS 的推送時延 P99 指標已接近分鐘級,RDS 的推送時延 P99 指標已超過一分鐘,這是令人難以接受的。經過初步分析發現,耗時長的主要因素是由於我們使用了 envoyfilter,istio 控制面需要進行大量的計算去匹配 envoyfilter 所關聯的對象,於是優化方向是要減少 envoyFilter patch 的耗時。

(2)CDS 中 envoyFilter 的 patch 時間複雜度從 O(n^2) 降低爲 O(n)

在 CDS 的 cluster patch 中,原先 istio 在對 envoyFilter 的 patch cluster 時用到了循環嵌套,循環一遍所有 envoyFilter,再嵌套循環一遍所有目標 cluster 對象,根據每一個 envoyFilter 指定的 service、subset、port 等信息計算出其匹配的 cluster 對象的範圍,然後把內容寫入對應對象,這一過程使得時間複雜度爲 O(n^2)。

梳理我們的使用場景發現,envoyfilter 的用途是針對特定的 cluster 對象進行 patch,大部分 envoyFilter 和 cluster 是一對一的。因此就可以用空間換時間的方式,提前將 envoyFilter 構建成 map,在循環全部 cluster 進行 patch 時,就能以 O(1) 的方式獲取到匹配的 envoyFilter,從而減少了循環嵌套。

於是我們根據不同 key 構建多個 map 形成多個桶,比如 service 有一個桶,subset 有一個桶,port 有一個桶,對於每一個 envoyFilter,指定了 service 的值就放入 service 的桶裏並記錄 service 值,指定了 port 的值就放入 port 的桶裏並記錄 port 值,如果沒指定值就是任意值都匹配,比如沒指定 port 值就把他放入 port 桶裏並記錄值爲 Any,其餘同理,最終每個桶裏記錄的特定值的數量加上 Any 的數量一定等於全部 envoyFilter 的數量。

循環完一遍 envoyFilter 之後,所有的桶都構建好了。然後循環一遍 cluster,對於每個 cluster 根據它自身的 service、subset、port 的值,在每個桶中尋找匹配值的數量,再加上 Any 值的數量,可以尋找到匹配的最小集,然後再循環最小集進行精確匹配。

在我們的使用場景中,時間複雜度從 O(n^2) 降低到了 O(n)。只有當所有 envoyFilter 都沒有指定 cluster,envoyFilter 和 cluster 是多對多的關係時,我們的優化效果最不明顯,與優化前保持一致,時間複雜度都是 O(n^2)。

(3)其他 xDS 的 envoyFilter patch 優化

RDS 推送的主要性能瓶頸也在於 envoyfilter 的 patch,CDS 優化的思想邏輯也適用於 RDS 的優化,不過 RDS 的 patch 更爲複雜,由於 key 更多所以分的桶更多,但核心思想一致,這裏就不再贅述。LDS、EDS 若是遇到了同樣的性能瓶頸,也可以考慮這一方案。

(4)其他優化

實現按需下發,這樣可以大大減少 CDS 推送時所需要計算的數據。另外,拆分了 gateway 數據,也減少了 VirtualService 的數量,使得 build 路由的時候更快速。

3.2 基於 merkle 樹建立成功率度量體系

(1) 成功率度量體系建設方案

隨着數據量接入越來越多,控制面管理的 envoy 節點也越來越多,用戶會關心他的操作成功生效的有多少,未完成的有多少。控制面下發一次配置變更後,一定時間內所有的 envoy 都接收到此次變更則認爲成功率爲 100%。爲了獲取某一變更已經被多少 envoy 接收到,我們設計了一個方案來查詢推送的狀態。

如上圖所示,istio 控制麪包含多個節點,每個節點又連接到不同的 envoy。需要先改造 istio,使它能夠正確記錄不同 envoy acked 配置版本,並暴露接口以供查詢,然後用 Istio Status Api 來聚合 istio 控制面返回的 acked 版本信息,最終通過 Istio Status Api,我們可以獲取某一變更推送未完成的 envoy 節點列表,推送成功率 = 推送成功的 envoy 節點數 / 全部需要推送的 envoy 節點數。基於此方案,統計每次變更的推送成功率,最後形成監控指標,可以很好的度量 istio 控制面的性能表現。

除此之外,這一方案還可以幫助我們提升可靠性,在刪除 pod 時,先利用 finalizer 卡住刪除操作,然後查詢接口直到這一變更已經被所有 envoy 接收到,再摘除 finalizer 實現真正刪除,防止變更推送未完成時直接刪除造成的流量損失。

(2)方案實現過程

爲了 istio 控制面能夠獲取不同 envoy acked 版本信息,我們分析了其內部實現邏輯。

ConfigController 把配置變更傳給 DiscoveryServer 後,DiscoveryServer 經過一系列處理, 會生成 DiscoveryResponse 下發給 envoy,其中包含了 ledgerVersion 這一信息。與此同時,若是 envoy 向 istio 控制面發起 DiscoveryRequest 請求時,也會帶着它自己當前 acked ledgerVersion 信息。LedgerVersion 這一信息是基於 merkle 樹實現的,它其實是這棵樹的 root hash 值,通過 root hash 值,我們可以解析出整棵樹存儲的數據。

基於這一特性,我們在某一變更事件進入推送隊列時,將這一次的配置變更的資源和其版本存入 merkle 樹,即與 ledgerVersion 這一信息關聯起來,並暴露接口來查詢所有 envoy 當前 acked ledgerVersion,並解析成對應的配置,就能夠確定性的知道每一個 envoy 節點中某一配置的某一版本是否真正生效。

(3)優化接口查詢時延,減少內存使用

在改造 istio 實現接口的過程中,我們發現查詢時間變得越來越慢,內存也在緩慢增長,甚至到達臨界點會 oom。通過 pprof 抓取內存發現,merkle 樹的佔比非常之高,爲了優化這一問題,我們進一步分析了 ledger 中對樹的實現。

它實現了一棵高度爲 64 的哈希樹,葉子節點存儲了數據塊的哈希值,父節點的 hash 值是左右子節點的 hash 值組合,且 0 代表左孩子,1 代表右孩子,如下圖所示。

爲了加快存取速度,沒有把每個點的 hash 值都存入數據庫,而是當高度爲 4 的倍數時,把這一層的點的 hash 值存入數據庫,它既是上面一棵小樹的葉子節點,又是下面一棵小樹的 root hash,每一棵小樹有 31 個節點。當每次要更新某一個 key 的值時,根據 key 的 8 個字節化成二進制,按照 01 去尋找左或右子節點,最多隻需要加載 16 次數據庫,就可以找到該 key 對應的位置,把它的 value 更新好之後,生成一個新的 hash 值,再由下往上依次更新父節點的 hash 值,由於只保存高度爲 4 的倍數的節點的 hash,也只需要更新 16 次數據庫,就可以更新到整棵樹的根節點的 root hash,這樣最終生成的 root hash 就代表更新之後的完整數據信息。

然而在樹的實現中,之所以內存一直在增長是因爲 gc 存在問題,每次更新之後,舊的 root hash 其實已經無用了,但是它並沒有被刪除,導致越來越多的無用數據堆積佔用內存,隨着更新越多,數據殘留越多。爲了解決這個問題,我們利用了它的 TTL cache,它會週期性的檢測數據生存時間,對於過期的數據立即進行清理。於是在每次更新時,如果任意一棵小樹的 root hash 改變,我們就將它的舊的 root hash 設置爲過期,這樣下一次被檢測到時,內存就成功被釋放了。

3.3 啓動耗時優化

istio 控制面啓動時,會加載 kubernetes 中所有關注的資源到內存裏,並且還要進行很多資源匹配的計算。隨着接入的資源越來越多,控制面的啓動時間越來越長,這對於每次發佈和故障重啓都是個問題。我們通過改變內存中的數據結構,改造一些 map 的映射,可以很好的加快啓動的速度。關於啓動的分析和改造,是一個複雜的過程,我們做的工作還解決了其他的啓動問題,超出了性能這個議題,這裏不具體展開。

四. 結果與展望

4.1 優化結果對比

基於 istio 1.7.5 版本

4.2 未來展望

關於性能優化,需要做的工作還有很多,通過監控體系,我們不斷地在完善 istio 控制面,未來會在以下幾方面繼續努力:

1)繼續完善度量體系,可以利用 merkle 樹的實現,幫助我們存取更多信息,比如存入時間,幫助計算下發生效時延;

2)增加隊列併發度,提升隊列處理速度,從而提升性能;

3)積極和開源社區溝通交流,從自身使用場景出發,給社區做出更多貢獻。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/W6T3oMFR4rccv4fvt3Rsmw