超複雜調用網下的服務治理新思路

超複雜調用網,在開始這個話題前,我們先對標題進行拆解。

什麼是調用網?下圖是一個常規的微服務架構,流量從客戶端過來後,會通過 Gateway 進入微服務層,這時微服務之間相互調用、相互依賴就形成了所謂的調用鏈。這些調用鏈相互交織,最終形成了調用網。

那麼什麼是超複雜呢?最開始的時候,很多團隊可能都採用單體架構,隨着業務演進、團隊擴充,我們需要對服務進行逐步拆分。因此隨着業務變得複雜,我們的調用鏈、調用網也會變得越來越複雜。當它們複雜到一定的程度時,很多難纏的問題就出現了。

當前很多團隊在進行微服務化的過程中,可能暫時僅看到微服務的優勢,未遇到服務管理上的問題,畢竟不是每一套系統都達到了超複雜的標準,但是提前關注這些問題並做好預案也非常重要。作爲企業的軟件架構師或是技術負責人,我們應當始終用發展的眼光看問題,軟件行業的發展變化非常巨大,如果企業當下的架構無法適應未來一到兩年的業務發展,那會對業務和技術進步形成巨大阻礙。如果架構師能吸取其他企業的教訓和經驗,提前佈局,那麼業務在擴張過程中遇到的技術問題會少很多。

超複雜調用網帶來的難題

我個人對超複雜調用網給出一個定義:

在內部技術實踐中,我們發現系統達到這個量級後,超複雜調用網就會產生許多棘手的問題。

第一個要點是微服務的數量。如果一個系統內的微服務數目只有幾百個,那麼繪製一張囊括所有微服務的調用圖是有利於管理的;但如果超過了 1000 個,再把它們塞到一張圖後整張圖變得不可讀,它的意義就不大了。

第二點,如果一個微服務的實例數只有幾十個,這時實例的管理是比較簡單的,如果實例數超過 300,那麼團隊不可避免地會需要使用一些分片策略或是長連接策略,它們都會帶來一些特殊問題。

第三點是單個 API 涉及的微服務數量。如果 API 需要普遍涉及 10 個以上的服務,這時監控會面臨更大的挑戰。以字節跳動的場景爲例,目前字節跳動內網的在線微服務數量在萬級,其中最大的微服務大約有 1-2 萬個實例,而單個 API 也普遍在後端關聯了幾十個甚至上百個微服務。面對這樣的複雜度,有三個問題最爲突出:

**一是難以做容量預估。**微服務已經達到了一定的複雜度,它們的調用關係是非常複雜的:一個核心服務的依賴鏈可能就有幾百個,對每個依賴方做調研或去細緻地跟進每個限流策略顯然非常困難。另外,不同業務會通過不同活動實現業務增長,對核心服務來說,追溯每個業務的增長也是一個非常艱鉅的任務。

**二是會大幅提高服務治理難度。**這裏的服務治理包含限流、ACL 白名單、超時配置等,因爲調用關係變得複雜,每個服務可能會調用幾十個甚至上百個依賴服務,一些核心服務也會被幾百個服務所依賴,這時如何梳理這些調用關係、配置多少限流、配置怎樣的白名單策略,就成了團隊需要深度探討的問題。

**三是容災複雜度增大。**在複雜的調用關係下,每個 API 會依賴大量的微服務,而每一個微服務都有一定概率產生故障。我們需要區分強依賴和弱依賴,並輔以特定的降級策略,才能夠在不穩定的服務環境下獲得儘可能穩定的對外效果。

業界嘗試

那麼對於這些複雜的治理難題,業界會有怎樣的嘗試呢?

**第一種方式是鴕鳥心態。**完全不做工作,這反而是業界最廣泛的嘗試。相信很多企業並不是沒有受到超大規模調用網的侵擾,也不是沒有對其做一些嘗試,而是解決問題所產生的成本和損失實在是難以量化。

舉個例子,一個核心服務有很多依賴方,其中一個依賴方的代碼中存在嚴重的重試漏洞,瞬間產生大量重試把核心服務給壓垮了,最終造成了系統級的災難。這時我們可以去追溯問題的直接原因——代碼質量問題,至於隔離沒做好、超複雜調用關係沒有梳理清楚等,這些會被歸結爲間接原因,往往可以不被追究。

**第二種方式是精細化的監測與限流。**業內一些開源組件在功能上確實做得比較出色。如左圖是一個知名開源組件,它會對整個服務鏈路進行精細化監控。在這個示例裏,每個三角形是一個 Gateway,中空圓形才真正的服務。它展示了從流量入口到每個微服務的整個鏈路,如果鏈路是綠色的,說明流量是健康的;鏈路是紅色的,就說明流量存在異常。有了這樣詳細的拓撲圖,開發者就可以看清它的依賴關係。

這看起來很美好,所以大概在兩年前,我選取了一箇中等規模的業務線,把所有依賴關係梳理出來,得到了上圖中右側這張圖。裏面每一個代號都是一個服務,每一條線都是這個服務的依賴關係——這實在是太複雜了。左圖由於只有 4 個服務,整體比較清晰,但如果是幾百個服務相互交織、相互依賴,用這種圖來進行測算無疑是不可行的。

**第三種方式是單元化,或稱 SET 化,**比較有代表性的是螞蟻和美團。他們採用的主要方式是把每一個服務部署多份:set 1、set 2、set 3,流量通過單一的 shard key 進行 set 的選擇。這樣,set 之間就可以進行有效的資源隔離,在單個 set 產生問題時可以通過切流的方式容災。

但它也有三方面的侷限性。第一方面,SET 化需要有合適的分片鍵,如用地域或賬號去切分,這需要和業務屬性有匹配,並不是所有的業務都能找到這種合適的分片鍵。第二方面,這種方式需要的非全局數據比較多,譬如本地生活訂單,用戶在北京下單酒店的數據沒必要經過深圳。但在抖音、今日頭條這些綜合信息服務場景中,非全局數據非常少,那些看似本地的數據如用戶名、用戶的粉絲數、近期的點贊列表,其實也是全局數據。最後一個方面,SET 化需要冗餘,需要備份成本,大體量的公司不一定能夠支撐。

**第四種方式是 DOMA。**它的英文全稱是 Domain-Oriented Microservice Architecture。2020 年,Uber 提出了這個架構。下圖是一個簡單示例,其中綠色是 public interface,紅色的是 private interface。如果有流量想訪問域內的一個微服務,它必須要經過 Gateway Service 進行轉發,然後才能訪問。

如果用戶想要在域外訪問這個數據庫,我們需要通過左下角的 Query、ETL 把它轉化成一個離線數據庫。整個大框是一個 domain,它不同於 DDD 的 domain,它被稱爲服務域,可以理解成是一組服務的集合。字節跳動內部也參考了這種 domain 的思想,把一些服務聚合起來,產生特殊的化學反應。

但 DOMA 架構也存在一些問題,比如它過了一層 Gateway Service。我們在外層其實已經有一個從外網到內網的 Gateway,如果內網再放置過多 Gateway(尤其是中心化的),肯定會帶來額外的性能消耗,並造成一定的延遲上漲,這也是字節跳動沒有采取這種方式的原因。

字節跳動的探索和實踐

對於超複雜調用網,字節跳動探索出了一些最佳實踐,其中第一個核心叫做服務分層原則。

正如前文的微服務架構圖所示,服務在經歷從上到下的調用後出現了很複雜的調用關係,對此,我們可以依據康威定律對它做一些橫向切分,對調用關係進行分層。

康威定律是馬爾文 · 康威於 1967 年提出的,指的是**設計系統的架構受制於產生這些設計組織的溝通結構。**舉個例子,假設某家公司內部有四個團隊,如上圖所示,左側團隊和上方團隊溝通較密切,上方團隊和下方團隊溝通較少,把這種關係映射到微服務架構中後也是類似的,上方微服務和左側微服務的通信耦合性會大一些,和下方微服務的聯繫就會弱一些。

我們之前討論過一個悖論:爲什麼企業的組織架構非常清晰,但是微服務設計就非常複雜?最終得出的結論是沒有做好映射。字節跳動內部有很多團隊分別負責業務、中臺、基礎架構等技術領域,在真實的微服務架構下,我們應該把它清晰地切分成不同層次。

如下圖所示,首先是網關層。外網到內網之間需要有一個 Gateway 來處理一些基本事項,如參數基礎校驗、session 機制、協議轉換等。

第二層是 BFF 層。BFF 是近幾年日趨流行的一個概念,全稱是 Backend For Frontend(服務於前端的後端)。如過一個接口的對外主體業務邏輯是一致的,但在 iOS、Android、Web 等不同客戶端的可能有一些細微差別,那麼這些差別可以放在 BFF 層處理。

第三層是業務層。字節跳動有很多業務,如短視頻、資訊、遊戲、公益等,與特異業務功能直接相關的功能應當由這一層來實現。

第四層是中臺層,這一層應用了 DDD 的思想,我們抽取了一些通用的特殊能力,對它們進行專業化的建模和封裝,以實現大量基礎能力的複用。

第五層是數據服務層,通過合理的封裝,用戶無需直接訪問數據庫的表即可更方便、更安全地使用數據。

最後一層是基礎架構層,這層主要提供基礎架構領域的各種能力,比如微服務基礎組件、微服務基礎依賴以及數據庫或是消息隊列等。

字節跳動之所以可以快速孵化新產品,業務層和中臺層的建設是一個重要原因。比如新做一個教育應用,我們可以直接調用成熟的賬號系統、支付系統、直播模塊等,也可以通過向學員推送他可能感興趣的視頻,將他們轉化成付費會員。由於存在這類專業領域的建模,在對微服務進行歸類處理時,分層變得尤爲重要。

這裏有幾個指導思想供大家參考:首先是分層原則需要結合業務靈活調整,DDD 只是一種指導思想,不能按照它的每一條規範去做;其次是在分層原則中,建議從上到下去進行訪問,業務層的請求可以訪問數據服務層,但數據服務層的請求不能訪問中臺層,逆向訪問可能會產生循環依賴等嚴重問題;第三,對於調用關係異常複雜的業務層、中臺層,我們給出了一種點線面結合的方法

點在字節跳動內部被稱爲流量身份標記 TIM(Traffic Identity Mark)。流量從客戶端進來後,我們會在 Gateway 層對 request 的各種參數進行檢測,驗證之後,一些需要在鏈路中傳遞的核心參數會被記錄下來,供後續分流、核心服務調用使用。

這種做法有助於一些特殊鏈路數據保護策略的實現,如未成年人數據保護。未成年人發出的請求從一開始就帶有相關參數,隨着調用鏈向下傳遞,通過透傳機制,核心的中臺層和數據服務層依然能讀到這些信息,並執行特殊的邏輯,以便對未成年人做好保護。

有了點之後,如果想在下游核心業務中使用這些關鍵信息,就必須要求信息會向下透傳。舉個例子,假設抖音的一個請求帶有流量身份標記 TIM1,那麼該流量觸達下游服務時仍應攜帶標記 TIM1;如果流量來自西瓜視頻且攜帶了 TIM2,那麼由這個請求觸發下一個在線請求時,它也一定要攜帶這個 TIM2。這使得整個調用鏈可以完成串聯,類似 Log ID、Trace ID。

所以這個地方有兩個依賴,我們最好把 TIM 放在 Header 中,讓它能更好地傳遞信息,並且使下游服務在不解析它的請求 Body 時,就能拿到 Header 中的信息來做流量調度等操作。在一個微服務內部,我們要通過 Context 機制,把入流量和出流量結合起來,把真正的標記傳遞過去,形成鏈路。

在字節跳動,“面” 是指高內聚的服務要聚合成服務域。上文介紹過康威定律,即軟件架構受制於組織溝通架構:如果有一組服務,它們的合作和聯繫非常緊密,相互調度非常多,但是共同對外暴露的功能點又比較少,那麼我們就可以把它們聚合爲一個服務域。

通過自動搜索流量的緊密、鬆散程度,結合組織架構關係,我們可以爲內部開發者提供服務域自動推薦,但最終設計還是需要服務維護人員進行確認。確定服務域後,服務之間的關係也真正確定下來。緊耦合的服務也需要採用同樣的治理策略。

“線 2” 有兩層含義,一是域管理員自行決定部署策略,二是要根據目標服務域按條件分流。

如上圖所示,服務域 A 是一個業務,它的域管理員希望按地域進行切流,把南方的服務調度到左邊,把北方的服務調度到右邊,他可以自由選擇調度的策略。

服務域 C 是一個核心中臺服務,比如評論服務,它不應當按照地域進行劃分,而是按照 User ID 進行流量劃分。基於這個目標,域管理員希望服務域可以按照 ID 取模進行切分,這也是可以的。在服務域內,它就可以形成這樣一條泳道,流量可以在泳道中向下傳遞。

對於服務域之間的流量,在域管理員確定部署策略之後,它會根據目標服務域的調度策略進行分流。舉個例子,如果服務域 A 想去訪問服務域 C 中的某個服務,流量從 A 出來後,它會根據 C 的切流方式進行切流。字節跳動的絕大多數在線流量已經接入 Service Mesh,我們能夠動態分析目標服務的部署策略、切流策略,並反饋給 Client 所在的 mesh proxy,Client mesh proxy 會動態修改目標服務的集羣,把流量打到目標集羣上去。

當然 Mesh 只是一種方法,開發者也可以用框架或業務代碼實現同樣的效果,但如果有企業和組織正在內部推廣 Service Mesh,上述提到的流量透傳、流量注入、根據目標部署情況動態按條件分流等都可以提前放在系統和框架中進行考慮。

在 2021 年抖音央視春晚紅包活動中,這套超複雜調用網服務治理思路也有充分應用。活動往往意味着流量激增,容災測試、全鏈路壓測、容量預估,我們遇到了不少難題。有了這個切流方案後,我們最終較理想地把服務域都找了出來,最終在活動上線後保障了流量的穩定分發,且沒有對其他業務造成影響。

結   語

目前,字節跳動正面臨超複雜調用網治理的嚴峻挑戰,它帶來的問題是實實在在的。我也相信,隨着國內企業的不斷髮展,很多公司未來也會發展到調用網極其複雜的境地,需要直面同樣的問題。爲了幫助業務實現健康過渡,大家最好能夠做兩個佈局:

第一個佈局是把服務分層做得足夠好。可以參考字節跳動的方案,按照分層原則排布服務,使各個組件能夠充分發揮優勢。第二個佈局是梳理調用鏈。這一點同樣可以參考我們點線面的實踐,根據可信的流量標記動態調配流量。如果這兩個佈局都能夠做好,那麼開發者既可以享受微服務的優勢,同時也能儘量規避微服務帶來的複雜度。最後做一個簡單的小廣告,最近我們開源了雲原生中間件集 CloudWeGo,專注於微服務的通信與治理,歡迎大家點擊 “閱讀原文” 瞭解詳情。

項目地址:https://github.com/cloudwego

項目官網:www.cloudwego.io

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