尋夢新聞LINE@每日推播熱門推薦文章,趣聞不漏接❤️
出處 | 金融級分布式架構公眾號
螞蟻金服(當時還是支付寶)從 2013 年起就運行在單元化架構上,除了具備異地容災能力外,還能做到異地多活,可隨時在多城市、多數據中心調配流量。基於單元流量調配機制,可做到大規模集群的藍綠發布、灰度仿真環境,為充分驗證業務正確性、降低故障提供了基礎條件。相應地,微服務體系也必須具備單元內收斂、單元間可控路由等能力,來支撐單元化技術架構的落地。本文根據玄霄 2018 年上海 QCon 演講內容整理。
「異地多活」是互聯網系統的一種高可用部署架構,而「單元化」正是做到異地多活的一個解題思路。
說起這個話題,不得不提兩個事件:一件是三年多前的往事,另一件就發生今年的杭州雲棲大會上。
從「挖光纜」到「剪網線」
2015 年 5 月 27 日,因市政施工,支付寶杭州某數據中心的光纜被挖斷,造成對部分用戶服務不可用,時間長達數小時。其實支付寶的單元化架構容災很早就開始啟動了,2015 年也基本上成型了。當時由於事發突然,還是碰到很多實際問題,花費了數小時的時間,才在確保用戶數據完全正確的前提下,完成切換、恢復服務。雖然數據沒有出錯,但對於這樣體量的公司來說,服務不可用的社會輿論影響也是非常大的。
527 這個數字,成為螞蟻金服全體技術人心中懸著那顆苦膽。我們甚至把技術部門所在辦公樓的一個會議室命名為 527,把每年的 5 月 27 日定為技術日,來時刻警醒自己敬畏技術,不斷打磨技術。
經過幾年的臥薪嘗膽,時間來到 2018 年 9 月。雲棲大會上,螞蟻金服發布了「三地五中心金融級高可用方案」。現場部署了一個模擬轉帳系統,在場觀眾通過小程序互相不斷轉帳。服務端分布在三個城市的五個數據中心,為了感受更直觀,把杭州其中一個數據中心機櫃設置在了會場。工作人員當場把杭州兩個數據中心的網線剪斷,來模擬杭州的城市級災難。
網線剪斷之後,部分用戶服務不可用。經過 26 秒,容災切換完成,所有受影響的用戶全部恢復正常。這個 Demo 當然只是實際生產系統的一個簡化模型,但是其背後的技術是一致的。這幾年來,其實每隔幾周我們就會在生產環境做一次真實的數據中心斷網演習,來不斷打磨系統容災能力。
從大螢幕上可以看到,容災切換包含了「數據庫切換」「緩存容災切換」「多活規則切換」「中間件切換」「負載均衡切換」「域名解析切換」等多個環節。異地多活架構是一個複雜的系統工程,其包含的技術內涵非常豐富,單場分享實難面面俱到。本場是微服務話題專場,我們也將以應用層的微服務體系作為切入點,一窺異地多活單元化架構的真面目。
去單點之路
任何一個互聯網系統發展到一定規模時,都會不可避免地觸及到單點瓶頸。「單點」在系統的不同發展階段有不同的表現形式。提高系統伸縮能力和高可用能力的過程,就是不斷與各種層面的單點鬥爭的過程。
我們不妨以一個生活中最熟悉的場景作為貫穿始終的例子,來推演系統架構從簡單到複雜,所遇到的問題。
上圖展示的是用支付寶買早餐的情景,當然角色是虛構的。
最早支付寶只是從淘寶剝離的一個小工具系統,處於單體應用時代。這個時候移動支付當然還沒出現,我們的例子僅用於幫助分析問題,請忽略這個穿幫漏洞。
假設圖中的場景發生在北京,而支付寶系統是部署在杭州的機房。在小王按下「支付」按鈕的一瞬間,會發生什麼事情呢?
支付請求要從客戶端發送到服務端,服務端最終再把結果返回客戶端,必然會有一次異地網路往返,耗時大約在數十毫秒的數量級,我們用紅色線表示。應用進程內部會發生很多次業務邏輯運算,用綠色圈表示,不涉及網路開銷,耗時忽略不計。應用會訪問多次數據庫,由於都在部署在同一個機房內,每次耗時按一毫秒以下,一筆支付請求按 10 次數據庫訪問算(對於支付系統來說並不算多,一筆業務可能涉及到各種數據校驗、數據修改)。耗時大頭在無可避免的用戶到機房物理距離上,系統內部處理耗時很小。
到了服務化時代,一個好的 RPC 框架追求的是讓遠程服務調用像調本地方法一樣簡單。隨著服務的拆分、業務的發展,原本進程內部的調用變成了網路調用。由於應用都部署在同一個機房內,業務整體網路耗時仍然在可接受範圍內。開發人員一般也不會特別在意這個問題,RPC 服務被當成幾乎無開銷成本地使用,應用的數量也在逐漸膨脹。
服務化解決了應用層的瓶頸,緊接著數據庫就成為制約系統擴展的瓶頸。雖然我們本次重點討論的是服務層,但要講單元化,數據存儲是無論如何繞不開的話題。這里先插播一下分庫分表的介紹,作為一個鋪墊。
通過引入數據訪問中間件,可以做到對應用透明的分庫分表。一個比較好的實踐是:邏輯拆分先一步到位,物理拆分慢慢進行。以帳戶表為例,將用戶 ID 的末兩位作為分片維度,可以在邏輯上將數據分成 100 份,一次性拆到 100 個分表中。這 100 個分表可以先位於同一個物理庫中,隨著系統的發展,逐步拆成 2 個、5 個、10 個,乃至 100 個物理庫。數據訪問中間件會屏蔽表與庫的映射關係,應用層不必感知。
解決了應用層和數據庫層單點後,物理機房又成為制約系統伸縮能力和高可用能力的最大單點。
要突破單機房的容量限制,最直觀的解決辦法就是再建新的機房,機房之間通過專線連成同一個內部網路。應用可以部署一部分節點到第二個機房,數據庫也可以將主備庫交叉部署到不同的機房。
這一階段,只是解決了機房容量不足的問題,兩個機房邏輯上仍是一個整體。日常會存在兩部分跨機房調用:
- 服務層邏輯上是無差別的應用節點,每一次 RPC 調用都有一半的概率跨機房;
- 每個特定的數據庫主庫只能位於一個機房,所以宏觀上也一定有一半的數據庫訪問是跨機房的。
同城跨機房專線訪問的耗時在數毫秒級,圖中用黃色線表示。隨著微服務化演進如火如荼,這部分耗時積少成多也很可觀。
改進後的同城多機房架構,依靠不同服務註冊中心,將應用層邏輯隔離開。只要一筆請求進入一個機房,應用層就一定會在一個機房內處理完。當然,由於數據庫主庫只在其中一邊,所以這個架構仍然不解決一半數據訪問跨機房的問題。
這個架構下,只要在入口處調節進入兩個機房的請求比例,就可以精確控制兩個機房的負載比例。基於這個能力,可以做到全站藍綠發布。
「兩地三中心」是一種在金融系統中廣泛應用的跨數據中心擴展與跨地區容災部署模式,但也存在一些問題。異地災備機房距離數據庫主節點距離過遠、訪問耗時過長,異地備節點數據又不是強一致的,所以無法直接提供在線服務。
在擴展能力上,由於跨地區的備份中心不承載核心業務,不能解決核心業務跨地區擴展的問題;在成本上,災備系統僅在容災時使用,資源利用率低,成本較高;在容災能力上,由於災備系統冷備等待,容災時可用性低,切換風險較大。
小結一下前述幾種架構的特點。直到這時,微服務體系本身的變化並不大,無非是部署幾套、如何隔離的問題,每套微服務內部仍然是簡單的架構。
螞蟻金服單元化實踐
螞蟻金服發展單元化架構的原始驅動力,可以概括為兩句話:
- 異地多活容災需求帶來的數據訪問耗時問題,量變引起質變;
- 數據庫連接數瓶頸制約了整體水平擴展能力,危急存亡之秋。
第一條容易理解,正是前面討論的問題,傳統的兩地三中心架構在解決地區級單點問題上效果並不理想,需要有其他思路。但這畢竟也不是很急的事情,真正把單元化之路提到生死攸關的重要性的,是第二條。
到 2013 年,支付寶核心數據庫都已經完成了水平拆分,容量綽綽有餘,應用層無狀態,也可以隨意水平擴展。但是按照當年雙十一的業務指標做技術規劃的時候,卻碰到了一個棘手的問題:Oracle 數據庫的連接不夠用了。
雖然數據庫是按用戶維度水平拆分的,但是應用層流量是完全隨機的。以圖中的簡化業務鏈路為例,任意一個核心應用節點 C 可能訪問任意一個數據庫節點 D,都需要占用數據庫連接。連接是數據庫非常寶貴的資源,是有上限的。當時的支付寶,面臨的問題是不能再對應用集群擴容,因為每增加一台機器,就需要在每個數據分庫上新增若干連接,而此時幾個核心數據庫的連接數已經到達上限。應用不能擴容,意味著支付寶系統的容量定格了,不能再有任何業務量增長。別說大促,可能再過一段時間連日常業務也支撐不了了。
單元化架構基於這樣一種設想:如果應用層也能按照數據層相同的拆片維度,把整個請求鏈路收斂在一組服務器中,從應用層到數據層就可以組成一個封閉的單元。數據庫只需要承載本單元的應用節點的請求,大大節省了連接數。「單元」可以作為一個相對獨立整體來挪動,甚至可以把部分單元部署到異地去。
單元化有幾個重要的設計原則:
- 核心業務必須是可分片的
- 必須保證核心業務的分片是均衡的,比如支付寶用用戶 ID 作分片維度
- 核心業務要盡量自包含,調用要盡量封閉
- 整個系統都要面向邏輯分區設計,而不是物理部署
在實踐上,我們推薦先從邏輯上切分若干均等的單元,再根據實際物理條件,把單元分布到物理數據中心。單元均等的好處是更容易做容量規劃,可以根據一個單元的壓測結果方便換算成整站容量。
我們把單元叫做 Regional Zone。例如,數據按 100 份分片,邏輯上分為 5 個 Regional Zone,每個承載 20 份數據分片的業務。初期可能是部署成兩地三中心(允許多個單元位於同一個數據中心)。隨著架構的發展,再整單元搬遷,演化成三地五中心,應用層無需感知物理層面的變化。
回到前面買早餐的例子,小王的 ID 是 12345666,分片號是 66,應該屬於 Regional Zone 04;而張大媽 ID 是 54321233,分片號 33,應該屬於 Regional Zone 02。
應用層會自動識別業務參數上的分片位,將請求發到正確的單元。業務設計上,我們會保證流水號的分片位跟付款用戶的分片位保持一致,所以絕大部分微服務調用都會收斂在 Regional Zone 04 內部。
但是轉帳操作一定會涉及到兩個帳戶,很可能位於不同的單元。張大媽的帳號就剛好位於另一個城市的 Regional Zone 02。當支付系統調用帳務系統給張大媽的帳號加錢的時候,就必須跨單元調用 Regional Zone 02 的帳務服務。圖中用紅線表示耗時很長(幾十毫秒級)的異地訪問。
從宏觀耗時示意圖上就可以比較容易地理解單元化的思想了:單元內高內聚,單元間低耦合,跨單元調用無法避免,但應該盡量限定在少數的服務層調用,把整體耗時控制在可接受的範圍內(包括對直接用戶體驗和對整體吞吐量的影響)。
前面講的是正常情況下如何「多活」,機房故障情況下就要發揮單元之間的容災互備作用了。
一個城市整體故障的情況下,應用層流量通過規則的切換,由事先規劃好的其他單元接管。
數據層則是依靠自研的基於 Paxos 協議的分布式數據庫 OceanBase,自動把對應容災單元的從節點選舉為主節點,做到應用分片和數據分片繼續收斂在同一單元的效果。我們之所以規劃為「兩地三中心」「三地五中心」這樣的物理架構,實際上也是跟 OceanBase 的副本分布策略息息相關的。數據層異地多活,又是另一個宏大的課題了,以後可以專題分享,這里只簡略提過。
這樣,借助單元化異地多活架構,才能做到開頭展示的「26 秒完成城市級容災切換」能力。
關鍵技術組件
單元化是個複雜的系統工程,需要多個組件協同工作,從上到下涉及到 DNS 層、反向代理層、網關 /WEB 層、服務層、數據訪問層。
總體指導思想是「多層防線,迷途知返」。每層只要能獲取到足夠的信息,就盡早將請求轉到正確的單元去,如果實在拿不到足夠的信息,就靠下一層。
- DNS 層照理說感知不到任何業務層的信息,但我們做了一個優化叫「多域名技術」。比如 PC 端收銀台的域名是 cashier.alipay.com,在系統已知一個用戶數據屬於哪個單元的情況下,就讓其直接訪問一個單獨的域名,直接解析到對應的數據中心,避免了下層的跨機房轉發。例如上圖中的 cashiergtj.alipay.com,gtj 就是內部一個數據中心的編號。移動端也可以靠下發規則到客戶端來做到類似的效果。
- 反向代理層是基於 Nginx 二次開發的,後端系統在通過參數識別用戶所屬的單元之後,在 Cookie 中寫入特定的標識。下次請求,反向代理層就可以識別,直接轉發到對應的單元。
- 網關 /Web 層是應用上的第一道防線,是真正可以有業務邏輯的地方。在通用的 HTTP 攔截器中識別 Session 中的用戶 ID 字段,如果不是本單元的請求,就 forward 到正確的單元。並在 Cookie 中寫入標識,下次請求在反向代理層就可以正確轉發。
- 服務層 RPC 框架和註冊中心內置了對單元化能力的支持,可以根據請求參數,透明地找到正確單元的服務提供方。
- 數據訪問層是最後的兜底保障,即使前面所有的防線都失敗了,一筆請求進入了錯誤的單元,在訪問數據庫的時候也一定會去正確的庫表,最多耗時變長,但絕對不會訪問到錯誤的數據。
這麼多的組件要協同工作,必須共享同一份規則配置信息。必須有一個全局的單元化規則管控中心來管理,並通過一個高效的配置中心下發到分布式環境中的所有節點。
規則的內容比較豐富,描述了城市、機房、邏輯單元的拓撲結構,更重要的是描述了分片 ID 與邏輯單元之間的映射關係。
服務註冊中心內置了單元字段,所有的服務提供者節點都帶有「邏輯單元」屬性。不同機房的註冊中心之間互相同步數據,最終所有服務消費者都知道每個邏輯單元的服務提供者有哪些。RPC 框架就可以根據需要選擇調用目標。
RPC 框架本身是不理解業務邏輯的,要想知道應該調哪個單元的服務,信息只能從業務參數中來。如果是從頭設計的框架,可能直接約定某個固定的參數代表分片 ID,要求調用者必須傳這個參數。但是單元化是在業務已經跑了好多年的情況下的架構改造,不可能讓所有存量服務修改接口。要求調用者在調用遠程服務之前把分片 ID 放到 ThreadLocal 中?這樣也很不優雅,違背了 RPC 框架的透明原則。
於是我們的解決方案是框架定義一個接口,由服務提供方給出一個做到類,描述如何從業務參數中獲取分片 ID。服務提供方在接口上打註解,告訴框架做到類的路徑。框架就可以在執行 RPC 調用的時候,根據註解的做到,從參數中截出分片 ID。再結合全局路由規則中分片 ID 與邏輯單元之間的映射關係,就知道該選擇哪個單元的服務提供方了。
寫在最後
本文著重介紹了螞蟻金服異地多活單元化架構的原理,以及微服務體系在此架構下的關鍵技術做到。要在工程層面真正落地單元化,涉及的技術問題遠不止此。例如:數據層如何容災?無法水平拆分的業務如何處理?
螞蟻技術團隊會堅持走技術開放路線,後續還會以不同的形式分享相關話題,也歡迎各位讀者留言探討。