分散式系統數據層設計模式
原創聲明:本文系作者原創,謝絕個人、媒體、公眾號或網站未經授權轉載,違者追究其法律責任。
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 容災方案。
本文只討論了節點級的水平擴展以及容災能力,沒有提及訪問距離帶來的延時問題。金融系統往往要求機房級甚至城市級的擴展能力和容災能力,螞蟻金服就運行在異地多活架構上。這時候數據訪問延時就無法忽略了,會冒出很多原本不是問題的問題,架構設計將更加複雜。對數據類型的分類,其實是一個重要的基礎,不同類型的數據在異地架構下也有相應的處理模式,後續異地多活的系列文章還會深入討論。
公眾號:金融級分散式架構(Antfin_SOFA)
推薦閱讀:
※閱讀筆記:Scaling Memcache at Facebook
※用zookeeper來構建的一種一致性副本協議
※閱讀筆記:PowerGraph: Distributed Graph-Parallel Computation on Natural Graphs
※分散式系統設計:單點模式之挎斗模式