## 概述
2013 年 5 月,支付寶最后一臺小型機下線,去 “IOE” 取得里程碑進展。支付寶(以及后來的螞蟻金服)走的是一條跟傳統金融行業不同的分布式架構之路。要基于普通硬件資源實現金融級的性能和可靠性,有不少難題要解決。應用層是無狀態的,借助 SOA 架構還可以比較方便地擴展。而數據層就沒那么簡單了,螞蟻金服在探索的過程中,積累了一些有用的數據層架構設計經驗,還是非常模式化的,可以分享出來供參考。
傳統銀行使用的高端硬件資源和商業數據庫,單機的性能和穩定性肯定占有絕對的優勢。互聯網分布式架構,則需要從架構設計上做文章,提高系統整體的并發處理能力和容災能力,其中容災能力又主要有兩個指標:
RTO,Recovery Time Objective,恢復時間目標。表示能容忍的從故障發生到系統恢復正常運轉的時間,這個時間越短,容災要求越高。
RPO,Recovery Point Objective,數據恢復點目標。表示能容忍故障造成過去多長時間的數據丟失,RPO 為 0 表示不允許數據丟失。
分布式領域 CAP 理論告訴我們,一致性、可用性、分區容忍性三者無法同時滿足。我們不要奢望尋找能解決所有問題的萬能方案,而應該根據不同的場景作出取舍。雖然業務場景五花八門,但是根據實際經驗,往往可以歸到有限的幾種模式中,處理策略也是相對固定的。
我們抽象一個簡化的支付系統模型來幫助理解,為了敘述方便,不一定跟支付寶的實際業務情況完全一致。它采用 SOA 架構,主要劃分了交易、賬務、用戶、運營支撐這幾個子系統,各自有各自的數據庫。另外還有一個全局的配置庫,存放一些會被各處用到的配置數據。

這幾個子系統涵蓋了幾種常見的模式,先簡要介紹它們的主要業務:
賬務:金融/支付系統中最核心的業務,簡化后姑且認為只保存每個賬戶的余額,主要操作是增減余額。它的特點是要求數據強一致,每一次對余額的增減必須基于一個絕對正確的當前值,否則就會造成資損。
交易:負責記錄每筆交易的狀態和上下文。在電商系統中,它可能是商品訂單;在銀行系統中可能是轉賬流水。交易類的數據有生命周期,可能有創建、付款、發貨、確認收貨、退款等狀態變遷。這些都不重要,重要的是它的業務特點:每一筆交易的創建是獨立的,不需要依賴其他交易的數據;推進一筆交易狀態的時候,要求這條數據是強一致的,但跟其他交易數據無關。
用戶:維護用戶的用戶名、密碼、郵箱、手機等非賬務信息,提供注冊、登錄、查詢業務。在執行核心業務的時候,有多處需要讀用戶的基本信息,關鍵業務鏈路對其有讀強依賴。
運營支撐:供內部工作人員用的后臺系統,包括但不限于工作流、客服等功能。
配置數據:這里是個寬泛的說法,籠統地表示各類變更不頻繁,但是在主業務流程中需要頻繁讀取的數據,例如交易類目、機構代碼、匯率。它們實際可能是散在各個業務系統中的,為了方便描述,單獨用一個配置數據庫來表示。
把數據庫按業務模塊進行拆分,是典型的垂直擴展思路,突破了單庫的能力限制,使得系統可以支撐更多的業務量。當然這也引入了分布式事務的問題,另有專題介紹暫且不表。拆分開后,就方便不同的業務采取不同的架構設計了。
### 賬務系統
與垂直拆分對應的,自然就是水平拆分。分庫分表已經是一種非常成熟的數據水平拆分方法。例如可以將賬號對 10 取模,將數據分散到 10 個邏輯分表中。這 10 個分表又映射到 10 個物理數據庫。分庫分表中間件可以屏蔽掉底層部署結構和路由邏輯,應用層仍然像使用普通單庫一樣寫 SQL。

拆分開后,“有數據庫出故障”的概率其實是大大增加的。假設其中一個賬務庫故障了,就意味著有至少 10% 的核心業務受影響了,實際還不止,因為一筆交易涉及雙方賬號。這種情況怎么辦,立即切換到備庫?不行的,前面說過賬務要求數據強一致,即 RPO=0。數據庫的主備復制一般有延時,不能保證數據無丟失。即使用 Oracle+ 共享存儲的方式保證不丟數據,回放 Redo Log、檢查數據一致性、切換備庫,通常要花費數十分鐘,足夠用戶在社交網絡炸鍋的了。怎么辦?早期其實沒什么好辦法,情愿犧牲一些 RTO,也要保證 RPO。當然可以做一些體驗上的優化,例如界面展示余額時,可以使用只讀備庫,減少用戶恐慌,但不允許基于此余額做實際業務,聊勝于無吧。
后來逐漸探索出了一套賬務容災方案,需要業務層參與,還挺復雜的。這個話題足夠單獨成文,本文先不詳細介紹,只說一下基本思路:主備庫數據不一致無法避免,但可以想辦法鎖定有哪些賬號的數據是最近剛剛在主庫有過變更的,我們沒法確定這個變更是否已經同步到備庫了,就把這些賬戶全部加入黑名單,數據庫恢復前不允許他們再做業務,避免發生資損。可以采取一些手段,讓黑名單范圍盡量小,并且確保黑名單以外的賬戶一定是主備庫一致的,實踐中可以縮小到幾十幾百個賬戶。這樣,不可用范圍就從庫粒度一下子降到賬號粒度,不在黑名單中的賬戶,就可以基于備庫余額正常開展業務。
這套基于黑名單的容災方案一直運行了好幾年,效果還不錯,缺點就是比較復雜,這是賬務類業務本身的特點決定的。直到自研數據庫 OceanBase 的誕生,情況有了改觀。OceanBase 是基于 Paxos 協議的分布式強一致數據庫,對于單節點故障,它提供 RPO=0,RTO<30 秒的容災能力,致力于從數據庫層屏蔽容災細節,為應用層提供簡單的使用方式。
### 交易系統
交易數據也是非常適合水平拆分的,可以將交易單據號取模,做分庫分表。除此之外,根據交易類業務的特點,還有更有意思的玩法。除了正常的交易主庫之外,另外再準備一組表結構完全相同的空庫,稱為 Failover 庫(注意不是備庫,跟主庫沒有數據同步關系)。交易系統在創建一筆交易的時候,首先要生成交易單據號,其中有一位叫做彈性位,正常情況下它的值是 1,代表這筆數據應該寫入主庫。后續根據交易單據號讀寫該條數據的時候,一看彈性位是 1,就知道到主庫找這條數據。

假設 3 號主庫突然故障了,這時就需要自動或手動給交易系統推送一個指令,告訴它以后第 3 分片的新數據應該插入 Failover 庫。以后生成的第 3 分片的交易單據號,彈性位就是 2,代表 Failover 庫,后續讀寫這條數據,也可以根據這一位自動找到 Failover 庫。這時候主庫的存量數據是無法修改的,已創建未付款的交易,用戶可以放棄,重新創建一筆,就會落到 Failover 庫正常處理。已經付款的交易,就暫時不能做發貨、確認收貨等狀態推進了,但這不是關鍵業務,遲一點做也問題不大。當主備庫數據一致性檢查通過,主備切換完成,落在主庫的老數據又可以繼續處理了。這時再推送指令給交易系統:3 號庫恢復正常狀態,以后新數據落主庫。Failover 機制讓主業務(創建交易、付款)在很短的時間內恢復可用,放棄非關鍵業務(存量數據的狀態推進),為主備切換爭取了時間。分庫分表、Failover 的邏輯,都可以由數據訪問層封裝,業務層并不用感知。
這期間在 Failover 3 號庫創建的、彈性位為 2 的數據怎么處理?答案是不用特殊處理,根據彈性位 2,以后仍然可以在 Failover 庫訪問到這條數據,經過一段時間后,主庫、彈性庫的數據最終都會遷移到歷史庫去。Failover 庫主要用于臨時接管主庫的新增數據,只要保持表結構一致即可,容量可以低于主庫。當然彈性位也可以啟用 3、4、5 更多編號,來靈活切換更多存儲,這也是“彈性”的含義所在。
### 配置數據
配置數據很好理解,讀多寫少,讀可靠性要求高,非常適合采用讀寫分離方案。根據具體業務,可以采用讀從庫、分布式緩存、內存緩存等方式。
### 用戶系統
用戶數據跟賬務數據有緊密的對應關系,直觀地想,也應該跟賬務數據采用同樣的處理策略,甚至合并到賬務庫中。這的確也是可行的,但在實踐中,我們根據它的業務特性,采取的卻是跟配置數據類似的處理策略,沒有做水平拆分,而是做全量復制、讀寫分離。理由有如下這些: 用戶數據更新較少,寫操作不在關鍵路徑,讀操作在關鍵路徑,跟配置數據的特性非常相似 對數據一致性要求沒那么高,可以接受少量延遲同步,沒有必要用賬務數據那么強的一致性保障,賬戶余額不可信時,希望至少不影響登錄 不全是按賬號精確查找,可能有郵箱、手機號等維度的查詢,按賬號水平拆分后,難以路由
所以用戶系統實際采用的方案是:一個寫庫,全量異步復制到多個讀庫,再加上分布式緩存。如果寫庫故障,則不能注冊新用戶、更新個人信息;個別讀庫故障,不影響業務。
### 運營支撐系統
這里用運營支撐系統舉例子,實際上是想代表這么一類業務數據:讀寫比差不多,業務流程依賴寫操作,也不適合做水平拆分。這種稱之為全局狀態型數據。全局狀態型數據一般是輔助型的非關鍵業務,一旦數據庫故障,“要么等,要么忍”——犧牲 RTO 等待數據庫主備切換,或者犧牲 RPO 立即強切備庫。在做架構設計時,需要盡量避免關鍵業務強依賴全局狀態型數據。如果真的有關鍵業務是全局狀態型的,只能依靠 OceanBase 這樣的多副本強一致數據庫產品了。
### 歸納總結
業務數據主要可以歸為三大類:
狀態型:讀寫比相當,必須保證可寫才有意義,每一次寫操作必須基于前一個正確的狀態。這是最棘手的一種數據,難以完美兼顧 PTO 和 RPO 。關鍵業務的狀態型數據,應盡量想辦法把維度拆細,一是提高并發處理能力,二是方便隔離故障影響。
流水型:不斷產生新的數據,各條數據間是獨立的,可以隨時切換新數據的存儲位置,每條數據的主鍵自包含存儲位置信息。單條數據的更新需要保證強一致性。流水型數據很方便做水平擴展。
配置型:讀寫比大,強依賴讀,弱依賴寫,不要求嚴格的讀一致性。可以采用讀寫分離、一寫多讀的方式保證讀操作的性能和高可靠。
在做架構設計的時候,如果能準確地識別出業務數據的“模式”,可以幫助更合理地劃分業務模塊,更方便套用特定模式的性能擴展和可靠性保障策略,乃至將公共邏輯抽象成通用組件。一個沒有經過數據類型拆分的系統,可以先當成最壞的情況:全是全局狀態型數據。然后識別出其中的流水型數據、配置型數據,逐漸分離出去,狀態型數據盡量做水平拆分。最后肯定還是會存在無法規避的全局狀態型數據,則要想辦法盡量降低它的重要性,避免關鍵鏈路對它的強依賴。
金融/支付系統中,最重要的往往就是類似賬務的這種狀態型數據,是必須要面對的難題。螞蟻金服的經驗是將所有“類賬務”的業務(例如余額、余額寶、花唄)做抽象化、平臺化,封裝黑名單容災等固定的業務邏輯,減少重復開發。當然,OceanBase 數據庫上線后,把復雜度封裝在數據庫層內部,在性能和容災能力(RTO/RPO)上達到了業務期望的平衡,對業務開發是一個不小的福音。目前支付寶的核心系統已經 100% 運行在 OceanBase 數據庫上。
本文介紹的幾種數據模式,基本可以覆蓋常見的業務,可以作為架構設計的參考。但也不必過于拘泥,業務本身是復雜多樣的,不一定是單純的某種模式。舉兩個例子:
我們日常在支付寶界面上看到的消費記錄,并不是直接查詢交易系統,而是從一個專門的消費記錄系統查詢的。交易系統會通過異步消息把數據復制到消費記錄系統。雙 11 高峰期消費記錄展示可能會有少許延遲,就是這個道理。交易是典型的流水型業務,但交易系統和消費記錄系統組成的體系,用的卻是讀寫分離思想。
賬務系統的余額是狀態型數據,但每個賬戶的變更明細,卻是流水型數據,可以適用流水型 Failover 容災方案。
本文只討論了節點級的水平擴展以及容災能力,沒有提及訪問距離帶來的延時問題。金融系統往往要求機房級甚至城市級的擴展能力和容災能力,螞蟻金服就運行在異地多活架構上。這時候數據訪問延時就無法忽略了,會冒出很多原本不是問題的問題,架構設計將更加復雜。對數據類型的分類,其實是一個重要的基礎,不同類型的數據在異地架構下也有相應的處理模式