1. 程式人生 > >螞蟻金服異地多活的微服務體系

螞蟻金服異地多活的微服務體系

螞蟻金服(當時還是支付寶)從 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 個物理庫。資料訪問中介軟體會遮蔽表與庫的對映關係,應用層不必感知。

解決了應用層和資料庫層單點後,物理機房又成為制約系統伸縮能力和高可用能力的最大單點。

要突破單機房的容量限制,最直觀的解決辦法就是再建新的機房,機房之間通過專線連成同一個內部網路。應用可以部署一部分節點到第二個機房,資料庫也可以將主備庫交叉部署到不同的機房。

這一階段,只是解決了機房容量不足的問題,兩個機房邏輯上仍是一個整體。日常會存在兩部分跨機房呼叫:

  1. 服務層邏輯上是無差別的應用節點,每一次 RPC 呼叫都有一半的概率跨機房;

  2. 每個特定的資料庫主庫只能位於一個機房,所以巨集觀上也一定有一半的資料庫訪問是跨機房的。

同城跨機房專線訪問的耗時在數毫秒級,圖中用黃色線表示。隨著微服務化演進如火如荼,這部分耗時積少成多也很可觀。

改進後的同城多機房架構,依靠不同服務註冊中心,將應用層邏輯隔離開。只要一筆請求進入一個機房,應用層就一定會在一個機房內處理完。當然,由於資料庫主庫只在其中一邊,所以這個架構仍然不解決一半資料訪問跨機房的問題。

這個架構下,只要在入口處調節進入兩個機房的請求比例,就可以精確控制兩個機房的負載比例。基於這個能力,可以實現全站藍綠髮布。

“兩地三中心”是一種在金融系統中廣泛應用的跨資料中心擴充套件與跨地區容災部署模式,但也存在一些問題。異地災備機房距離資料庫主節點距離過遠、訪問耗時過長,異地備節點資料又不是強一致的,所以無法直接提供線上服務。

在擴充套件能力上,由於跨地區的備份中心不承載核心業務,不能解決核心業務跨地區擴充套件的問題;在成本上,災備系統僅在容災時使用,資源利用率低,成本較高;在容災能力上,由於災備系統冷備等待,容災時可用性低,切換風險較大。

小結一下前述幾種架構的特點。直到這時,微服務體系本身的變化並不大,無非是部署幾套、如何隔離的問題,每套微服務內部仍然是簡單的架構。

架構型別 優勢 問題
單體應用 網路開銷小 擴充套件性差,維護困難
單機房服務化 解耦,可擴充套件 容量受限,機房級單點
同城多機房階段一 突破單機房容量瓶頸 非必要的跨機房網路開銷大
同城多機房階段二 非必要的跨機房網路開銷小;機房級容災能力 城市級單點
兩地三中心 異地容災能力 網路耗時與資料一致性難兩全

螞蟻金服單元化實踐

螞蟻金服發展單元化架構的原始驅動力,可以概括為兩句話:

  1. 異地多活容災需求帶來的資料訪問耗時問題,量變引起質變;

  2. 資料庫連線數瓶頸制約了整體水平擴充套件能力,危急存亡之秋。

第一條容易理解,正是前面討論的問題,傳統的兩地三中心架構在解決地區級單點問題上效果並不理想,需要有其他思路。但這畢竟也不是很急的事情,真正把單元化之路提到生死攸關的重要性的,是第二條。

到 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 與邏輯單元之間的對映關係,就知道該選擇哪個單元的服務提供方了。

寫在最後

本文著重介紹了螞蟻金服異地多活單元化架構的原理,以及微服務體系在此架構下的關鍵技術實現。要在工程層面真正落地單元化,涉及的技術問題遠不止此。例如:資料層如何容災?無法水平拆分的業務如何處理?