資料來源:火山引擎-開發者社區
什么是單元化
單元化的核心理念是將業務按照某種維度劃分成一個個單元, 理想情況下每個單元內部都是完成所有業務操作的自包含集合,能獨立處理業務流程,各個單元均有其中一部分數據,所有單元的數據組合起來是完整的數據(各企業實際落地過程中會結合實際業務和基建情況做一些折中)。 流量按照某種分區維度(例如流量所屬用戶)Sharding 到不同的單元,調度上按照流量攜帶的分區信息進行調度,保證同一時刻該分區的數據寫入都在同一個單元,簡化版示意圖如下:
為什么要做單元化
業界各企業演進到單元化一般主要都是出于下面幾個原因:
- 資源限制:單機房受物理資源上限限制,同城多機房受地區的能評和供電等限制,無法做到機房的無限擴展,隨著業務規模的擴大,長期一定會面臨多地數據中心的布局;
- 合規要求:全球化產品通常會面臨不同地區的合規要求(例如歐盟的 GDPR),會有當地用戶數據只能存儲在當地的要求,業務天然需要考慮圍繞不同的合規區域構建單元;
- 容災考慮:核心業務有城市級異地容災需求,通過單元化方式可以構建異地單元,每個單元都有常態真實流量,流量可以靈活地在單元間進行調度。
除了上述最關鍵的問題外,建設單元化還能有其他方面的收益:
- 業務體驗提升:通過結合就近調度,能夠將用戶流量調度到最近的單元,從而降低請求耗時,提升用戶體驗;
- 成本方面:相比于異地冷備,兩地三中心等傳統容災架構,各個單元都能直接承載流量,減少資源冗余;
- 隔離方面:在最小的單元范圍內去做各種技術演進,能夠有效控制風險半徑。
異地單元化的主要挑戰
機房延遲問題: 一般同城機房之間物理距離 <200KM,網絡延遲和機房內差別不大,大部分業務場景無需過多考慮。 但跨城異地機房之間的延遲會大很多,例如北京和 上海之間 RTT P99 達 40 毫秒,此時跨機房請求耗時需要慎重考慮,特別是單個請求如果出現多次跨機房,增加的請求耗時可能是幾百毫秒甚至更大的。
數據同步問題:
- 容災單元之間的數據需要互通,以支持一個單元故障時能夠切流到另一個單元進行容災,因此需要在單元間進行數據同步;
- 數據同步需要考慮不同的存儲引擎(數據庫、緩存、消息隊列等),不同引擎特性不一,實現成本和復雜度巨大;
- 跨城機房間的延遲大而且是弱網環境,網絡的質量很難保證,進一步導致數據同步質量保證難度大。
流量路由的問題:所謂流量路由也就是如何將流量調度到正確的單元問題,需要考慮在請求鏈路哪個環節(客戶端、流量入口、內網 RPC、存儲層等)、根據請求什么信息(用戶 ID、地理位置等)進行用戶歸屬單元的識別,以及如何進行走錯單元流量的糾偏。
數據正確性問題:
- 例如歸屬?
單元1
?的用戶 A 評論了歸屬?單元2
?的用戶 B 的抖音短視頻,系統在?單元1
?給 B 發了一個通知,但 B 查看評論的流量被按 B 的單元歸屬調度到了?單元2
?,由于數據同步延遲問題,B 打開抖音后看不到評論。業務上需要感知這類同步延遲帶來的正確性問題; - 另外兩個單元的數據庫構建了雙向數據同步后,如果同一個用戶短時間在兩個單元讀寫同一份數據,可能會出現數據沖突問題。
成本問題:
- 每個單元都需要有完整的業務資源以及支撐這些業務資源(計算、存儲、網絡)的基礎設施(運維平臺、觀測平臺等),需要綜合考慮對成本的影響;
- 異地單元化的改造落地涉及到包括基建、架構、業務多方的配合支持,需要考慮人力上的成本。
管理復雜度問題:隨著單元的增加,不同的單元都需要管理對應的服務和基建,管理和運維的復雜性會有較大增加。
字節跳動異地單元化架構
在本文中,我們僅對字節跳動在中國大陸的單元化架構做討論,目前我們的單元化部署架構如下圖所示:
我們圍繞客戶端選路、接入層糾偏、計算層糾偏、存儲訪問層管控四個維度構建了單元化流量調度和管控能力,通過技術手段確保單元化流量調度的正確性(將流量調度到正確的單元)和數據訪問的正確性(數據不寫臟),具體來說:
- 客戶端:在客戶端通過調度組件將用戶流量從第一跳開始就調度到正確的單元,減少在內網的跨單元流量;
- 接入層:在網關通過網關的開放能力實現調度插件,按需對客戶端調度出錯的流量進行路由信息計算和糾偏;
- 計算層:計算層通過研發框架和 Mesh 的開放能力實現流量切面,確保對未經過網關的內部 RPC 流量的路由信息計算和糾偏;
- 存儲層:通過存儲訪問中間件和 Mesh 的開放能力實現流量切面,確保對存儲層路由出錯 / 異常單元化流量進行審計和攔截。
目前包括抖音、抖音電商、抖音支付、抖音直播、抖音本地生活等業務均啟動了異地單元化改造落地,生產環境已接入數千個核心微服務、超過 100 萬實例。
單元化架構落地的關鍵問題
如何選擇單元的維度
單元維度的選擇很大程度上決定了單元化架構的水平擴展能力、運維成本和容災方式,需要結合業務推進單元化架構想要解決的核心問題(資源、合規、容災)和業務特性來選型。業界實踐大部分是圍繞物理維度(例如 Region、機房)構建單元,也有少部分是邏輯維度的。
結合我們面臨的業務特性:
- 同時有包括社交、電商、本地生活、直播、支付等多種類型的業務,不同業務在單元化架構上有不同考慮;
- 存在大量中臺,各中臺同時支持不同類型的業務,這些中臺需要適配好不同上游業務的單元化流量和數據;
- 業務之間有比較復雜的依賴,例如電商和本地生活有從抖音入口進來的流量,也有自己獨立入口的流量。
綜合考慮,我們認為字節跳動的單元應該是物理維度的,單元的建設隨著基建的規劃和建設去演進,而不是業務自行構建,這種物理意義上的唯一調度依據能夠保證不同的業務長期演進中能夠在一個單元內閉環,避免調度混亂。
同時考慮我們目前最核心要解決的問題是資源和容災問題,因此我們最終選擇是 Region 維度構建單元,形成當前的?同城容災+異地多活
?容災架構。
如何選擇分區維度
分區維度決定了數據和流量在單元間的劃分標準,選擇分區維度的時候需要重點考慮:
- 每個分區對應的數據互相不能重疊,以保證同一時刻同一個分區維度的數據寫只在一個單元;
- 分區的粒度需要能支撐流量調度的靈活性要求,確保流量能按預期分布到各個單元;
- 路由信息的計算需要足夠輕量;
- 確保單元內調用邏輯盡可能閉環,避免產生跨單元調用。
一般 ToC 類業務最常見的就是以用戶作為分區維度,我們目前大部分業務選擇用戶(UserID)作為分區維度(抖音、電商等業務),少部分選擇 Region 維度(搜索等業務)。
如何進行流量的單元化調度
管理分區和單元的映射
分區和單元的映射也就是對于一個請求我們找到它應該調度到哪個單元的依據,需要綜合業務上對管理成本和靈活性的要求來考慮,我們通過以下兩種方式結合來管理:
- 映射表:通過?
UserID -> Set
?的映射表管理,面向 QA 測試、高價值用戶群灰度等明確指定歸屬單元場景; - 表達式:通過流量分片的方式,按比例將流量在單元間分配,以支持比較低的管理和計算成本,例如?
0 <= Hash(UserID) % 100000 < 70000 -> Set1、70000 <= Hash(UserID) % 100000 < 100000 -> Set2
?。
計算路由信息和糾偏
路由信息的計算邏輯一般不復雜,更關鍵的是決策需要在哪些環節計算以及怎么糾偏(把走錯單元的流量調度到正確單元),一般來說,整個請求鏈路包括客戶端、接入層、計算層和存儲層四層,需要結合公司基建情況和業務要求來決策落地方式,以下是我們對于各層實現糾偏能力的必要性和實現方式上的思考和建議。
客戶端:
- 必要性分析:在客戶端直接計算流量歸屬單元并調度,可以減少在內網出現跨單元訪問的情況,減少跨單元導致的耗時影響;
- 實現方式:可以在客戶端集成調度組件來實現,具體調度邏輯需要結合單元的維度來設計,例如最簡單可以不同單元對應不同域名,按 UserID 映射到對應域名。
接入層(負載均衡、網關):
- 必要性分析:客戶端由于改造依賴用戶升級、同時網絡環境復雜導致配置變更時生效時間無法保證,因此存在無法 100% 保證流量調度正確的情況,需要在接入層兜底計算路由信息并對走錯單元的流量進行糾偏;
- 實現方式:在流量入口,結合 LB / 網關的開放能力,實現路由信息的計算和流量糾偏能力。
計算層:
- 必要性分析:實際業務上,存在很多內部的 RPC 流量,例如消息隊列的 Consumer 發起請求、后臺定時任務發起的請求等,這種流量不經過接入層,需要在計算層 RPC 接口兜底進行路由信息計算并對走錯單元的流量進行糾偏;
- 實現方式:可以結合研發框架或者 Service Mesh 的開放能力實現。
存儲層:
- 必要性分析:業務上會存在例如一個用戶去讀寫另一個用戶的數據(例如社交場景的點贊、評論)這類不經過 RPC 接口調度的場景,需要在存儲訪問的時候兜底計算路由信息并對走錯單元的流量進行糾偏;
- 實現方式:一般可以結合存儲訪問中間件或者 ServiceMesh 開放能力實現。實際落地的時候,考慮到存儲訪問層實現糾偏成本和風險偏高(例如連接數風險),實際業務落地上可以考慮只做攔截,推動業務進行改造。
一個比較完整的分層示例:
如何適配復雜的業務調度場景
理想的單元化架構是所有服務都能在單元內閉環,不出現跨單元的流量,但是在實際業務場景下很難滿足理想的單元化架構。以電商場景為例,如果用買家作為分區維度,那商家的數據必然會被多單元讀寫,因此一定存在部分服務的流量不能單元化調度的情況,單元化架構需要能做好適配。和業界基本思路類似,我們對服務類型做了區分:
- 本地服務/LocalService:單元化拆分后能夠本地閉環讀寫的業務服務,比如在電商場景的買家業務相關服務。業務的大部分微服務應當都是 LocalService 類似,否則傾向于部署單 vRegion 部署;
- 中心服務/CentralService:無法進行單元化拆分的業務,固定在某個單元部署,通常是一些對于數據一致性要求非常高的服務,例如電商的商家、商品庫存等服務。
服務類型決定了流量調度方式和服務訪問的數據的同步方式。基于此,研發核心需要關注業務上服務類型的定義即可,其他包括數據同步管理、流量調度的細節都可以由框架/平臺內部收斂解決,極大降低業務上的理解和管理復雜度:
實際業務落地過程中,甚至會出現同一個服務不同接口的訪問行為不一致的情況,需要細化到接口維度區分類型,和服務類型類似設計即可,這里不贅述。
如何進行多單元數據管理
單元間數據同步
單元化架構下的數據同步有兩種場景:
- 中心服務訪問的數據在單元間單向同步,支持延遲敏感且接受一定程度數據不一致的業務場景本地只讀;
- 本地服務訪問的數據在容災單元間的雙向同步,支持一個單元出現故障的時候能夠將流量切到容災單元進行容災,數據同步需要考慮好防回環和唯一 Key 沖突處理。
數據同步一致性比對
數據對賬一般是全增結合,實時增量比對確保 Diff 發現的實時性,但寫入 TPS 高時會有誤差,周期全量比對兜底保證整體一致率,常見的一致性比對流程如下:
設計增量數據一致性比對能力的時候,需要重點考慮對熱鍵的檢測和處理,部分熱鍵一直在 Update 的話需要能識別出來,否則會導致持續有不一致誤報警,一個簡單的基于重試比對的實現如下:
如何保證數據多活的正確性
日常態:異常單元化流量的識別和攔截
以 UserID 作為單元化分區維度的話,在實際業務場景可能出現兩種異常情況:
- 從請求里面無法解析到正常的 UserID 或者未接入流量調度組件導致未正常計算路由信息;
- 一個用戶的流量在代碼邏輯層直接訪問數據庫,寫了另一個非歸屬本單元的用戶的數據。
這兩種情況我們都認為是異常單元化流量,在存儲訪問層通過管控中間件支持了識別和攔截能力,兜底保障數據不被寫臟:
切流態:避免數據同步延遲導致臟寫
在做單元間切流的時候,由于下面兩個問題,可能導致數據出現臟寫:
- 跨城異地單元之間的數據同步延遲一般接近或達到秒級,用戶流量從一個單元切換到另一個單元的時候可能由于數據還未同步完成從而出現臟寫;
- 單元間切流本質上是分區和單元映射配置的變化和重新下發的過程,在大規模分布式架構下,這份配置需要下發到多個獨立的實例上去生效,這些不同的實例生效時間無法完全一致,就導致同一個用戶的流量短時間可能進入不同單元從而導致數據臟寫。
我們對切流過程引入?兩階段配置變更+存儲訪問禁寫
?來解決上述數據臟寫問題,整體切流流程如下:
得益于我們整體單元化調度和存儲訪問中間件設計,我們的禁寫是 UserID 維度的,能夠控制到每次切流僅禁寫切流中的用戶,將切流的影響做的盡可能小。
如何確保切流的可靠性和風險控制
優化配置下發時效
結合前面關于切流期間通過?兩階段配置變更+存儲訪問禁寫
?來避免數據出現臟寫的說明,可以看到配置下發生效的時間對于禁寫時長是一個非常大的影響因素,而禁寫生效時長直接影響切流那部分用戶的使用體驗,因此我們需要盡可能提升配置下發時效。
目前字節接入單元化的 Pod 數已超過 100萬,在這么大規模的實例數下快速下發調度配置的挑戰是非常大的,我們通過?長連接主動推送+定時輪詢拉取
?的方式進行配置分發,提高配置收斂速度,減少配置不一致中間態的時間:
防止路由死循環
配置發布過程中,同一個 PSM 不同單元實例的生效時間不一樣,可能導致流量糾偏到目標單元后由于目標單元重新計算糾偏回來,出現路由死循環的情況。我們通過單元化調度組件在流量上下文中記錄并傳遞糾偏次數,攔截重復糾偏的流量,及時阻斷回環流量:
切流過程業務架構各層狀態檢查
- 多實例配置生效版本觀測檢查:每次配置發布會分配一個遞增的版本號,數據面組件通過長連接上報 Pod 當前配置版本號給配置中心,從而支持單元化控制面查詢和觀測配置收斂情況:
- 數據同步延遲觀測檢查:由于我們的切流禁寫是用戶維度的,不是整個數據庫禁寫,禁寫過程中數據庫并不是完全沒有新的寫流量,因此我們是根據?
數據實時的同步延遲+禁寫等待時間
?結合來判斷切流范圍的用戶禁寫后的數據是否已經同步:
- 業務核心指標觀測檢查:切流過程中對業務成功率、負載、延遲等指標的觀測。
如何保證跨地區 RPC 質量
單元之間通常物理距離較遠,網絡專線建設難度大成本高,網絡質量(延遲、丟包、可用帶寬)也差于同城機房間。前面介紹的各種單元路由糾偏都會產生跨單元 RPC,相比單元內 RPC 其成功率/耗時指標有所劣化。為了提升跨單元 RPC 質量,除了網絡專線本身的穩定性建設(屬于網絡基建范疇,此處不展開),也可以在架構設計和帶寬使用策略上進行針對性的優化。
跨單元 RPC 通道收斂:在單元化流量路由場景,某個流量很大的 RPC 服務可能僅小比例命中跨單元糾偏,此時由于下游實例數量很多可能導致連接復用效果較差。此時如果讓所有跨單元 RPC 先統一經過本地邊界網關,然后傳輸到其他單元的邊界網關,最后再轉發給實際下游,則建連過程拆分成了 3 段,中間段(網關之間)被所有跨單元 RPC 共用,可以保持較高的鏈接復用率; 另外 2 段為本地建連,即使連接復用率低,帶來的耗時增加也較少。此外收斂到網關通道也更容易集中對跨單元 RPC 進行其他優化,比如按接口等級進行 QoS 管控、在專線完全故障情況下對重要的控制信令做加密后 Fallback 到公網傳輸等:
跨單元網絡分級 QoS 管控:長距離專線建設成本高,帶寬有限,也更容易因為各類異常導致可用帶寬變少,無法滿足所有場景的跨單元網絡傳輸的需求,因此需要對不同類型的跨單元流量進行分級 QoS 管控。跨機房 RPC 失敗要么直接影響用戶體驗,要么導致故障無法操作止損,應在跨單元網絡傳輸中給予更高的優先級; 離線數據傳輸容易短時間大量占用帶寬,異常對用戶感知相對不明顯,應給以較低優先級,嚴格限制上限,必要時還應讓出帶寬:
未來演進思考
多單元研發成本和效率優化
字節跳動從原本的單 Region 內同城容災架構演進到多 Region 異地單元化架構周期比較短(一年半左右),基礎設施對多 Region 視角的支持還比較不足,對業務的整體研發和業務管理成本偏高,需要將多 Region 的研發和業務管理成本打平到單 Region。
極致的成本優化
從計算資源成本視角:在原來三機房同城容災模式下,每個機房需要預留 50% 的 Buffer 用于機房故障容災,演進到異地單元化架構后,基于兩個容災單元間的六個機房,部分業務機房故障可以將流量分攤到其他五個機房,此時各機房僅需 20% 的 Buffer。
從存儲資源成本視角:我們現在是?同城容災+異地多活
?的容災模式,各單元都支持同城容災,因此部分業務可以直接進行數據的單元化拆分,單元內各自只有一部分數據(加起來是全量數據),理想情況下存儲成本減少一半。
更復雜的單元化架構演進
未來字節跳動在國內會有更多的區域,不同業務在各單元的排布模型會越來越復雜,結合我們復雜的業務依賴關系,這里的流量調度模型、數據單元化和同步模型都需要演進。
未來區域增多后,業務隨著發展機房排布會調整,可能會需要在非容災單元之間調整流量,此時存在數據單元化拆分和用戶維度數據單元間搬遷能力,需要解決用戶維度數據的識別和低成本搬遷問題。
更完善的數據多活能力
字節跳動目前的存儲對 AP 場景更友好(側重抖音這種社交類場景),主要圍繞單 Region 構建,在多單元場景下對于電商、支付類(對數據一致要求非常高)的業務支持較弱,在異地單元化架構下強依賴數據同步能力來支持多單元數據多活能力,業務上的限制偏大(例如寫只能統一在一個單元),有跨 Region 強一致數據庫的需求。