很多年前,讀了子柳老師的《淘寶技術這十年》。這本書成為了我的架構啟蒙書,書中的一句話像種子一樣深埋在我的腦海里:「好的架構是進化來的,不是設計來的」。
2015年,我加入神州專車訂單研發團隊,親歷了專車數據層「架構進化」的過程。這次工作經歷對我而言非常有啟發性,也讓我經常感慨:「好的架構果然是一點點進化來的」。

1 單數據庫架構
產品初期,技術團隊的核心目標是:「快速實現產品需求,儘早對外提供服務」。
彼時的專車服務都連同一個 SQLServer 數據庫,服務層已經按照業務領域做了一定程度的拆分。

這種架構非常簡單,團隊可以分開協作,效率也極高。隨着專車訂單量的不斷增長,早晚高峰期,用戶需要打車的時候,點擊下單後經常無響應。
系統層面來看:
2 SQL優化和讀寫分離
為了緩解主數據庫的壓力,很容易就想到的策略:SQL優化。通過性能監控平台和 DBA 同學協作分析出業務慢 SQL ,整理出優化方案:
另外一個策略是:讀寫分離。
讀寫分離的基本原理是讓主數據庫處理事務性增、改、刪操作( INSERT、UPDATE、DELETE),而從數據庫處理 SELECT 查詢操作。
專車架構團隊提供的框架中,支持讀寫分離,於是數據層架構進化為如下圖:

讀寫分離可以減少主庫寫壓力,同時讀從庫可水平擴展。當然,讀寫分離依然有局限性:
3 業務領域分庫
雖然應用層面做了優化,數據層也做了讀寫分離,但主庫的壓力依然很大。接下來,大家不約而同的想到了業務領域分庫,也就是:將數據庫按業務領域拆分成不同的業務數據庫,每個系統僅訪問對應業務的數據庫。

業務領域分庫可以緩解核心訂單庫的性能壓力,同時也減少系統間的相互影響,提升了系統整體穩定性。
隨之而來的問題是:原來單一數據庫時,簡單的使用 JOIN 就可以滿足需求,但拆分後的業務數據庫在不同的實例上,就不能跨庫使用 JOIN了,因此需要對系統邊界重新梳理,業務系統也需要重構。
重構重點包含兩個部分:
4 緩存和MQ
專車服務中,訂單服務是並發量和請求量最高,也是業務中最核心的服務。雖然通過業務領域分庫,SQL 優化提升了不少系統性能,但訂單數據庫的寫壓力依然很大,系統的瓶頸依然很明顯。
於是,訂單服務引入了緩存和MQ。
乘客在用戶端點擊立即叫車,訂單服務創建訂單,首先保存到數據庫後,然後將訂單信息同步保存到緩存中。
在訂單的載客生命周期里,訂單的修改操作先修改緩存,然後發送消息到MetaQ,訂單落盤服務消費消息,並判斷訂單信息是否正常(比如有無亂序),若訂單數據無誤,則存儲到數據庫中。

核心邏輯有兩點:
這次優化提升了訂單服務的整體性能,也為後來訂單服務庫分庫分表以及異構打下了堅實的基礎。
5 從 SQLServer 到 MySQL
業務依然在爆炸增長,每天幾十萬訂單,訂單表數據量很快將過億,數據庫天花板遲早會觸及。
訂單分庫分表已成為技術團隊的共識。業界很多分庫分表方案都是基於 MySQL 數據庫,專車技術管理層決定先將訂單庫整體先從 SQLServer 遷移到 MySQL 。
遷移之前,準備工作很重要 :
當準備工作完成後,才開始遷移。
遷移過程分兩部分:歷史全量數據遷移和增量數據遷移。

歷史數據全量遷移主要是 DBA 同學通過工具將訂單庫同步到獨立的 MySQL 數據庫。
增量數據遷移:因為 SQLServer 無 binlog 日誌概念,不能使用 maxwell 和 canal 等類似解決方案。訂單團隊重構了訂單服務代碼,每次訂單寫操作的時候,會發送一條 MQ 消息到 MetaQ 。為了確保遷移的可靠性,還需要將新庫的數據同步到舊庫,也就是需要做到雙向同步。
遷移流程:
6 自研分庫分表組件
業界分庫分表一般有 proxy 和 client 兩種流派。
6.1 proxy模式
代理層分片方案在業界中有Mycat、cobar等 。
它的優點:應用零改動,和語言無關,可以通過連接共享減少連接數消耗。缺點:因為是代理層,存在額外的時延。
6.2 client模式
應用層分片方案業界有 sharding-jdbc ,TDDL 等。
它的優點:直連數據庫,額外開銷小,實現簡單,輕量級中間件。缺點:無法減少連接數消耗,有一定的侵入性,多數隻支持Java語言。
神州架構團隊選擇自研分庫分表組件,採用了client 模式,組件命名:SDDL。
訂單服務需要引入是 SDDL 的 jar 包,在配置中心配置數據源信息,sharding key ,路由規則等,訂單服務只需要配置一個 datasourceId 即可。
7 分庫分表策略7.1 乘客維度
專車訂單數據庫的查詢主維度是:乘客,乘客端按乘客 user_id 和 訂單 order_id 查詢頻率最高,我們選擇 user_id 作為 sharding key ,相同用戶的訂單數據存儲到同一個數據庫中。
分庫分表組件 SDDL 和阿里開源的數據庫中間件 cobar 路由算法非常類似的。
為了便于思維擴展,先簡單介紹下 cobar 的分片算法。
假設現在需要將訂單表平均拆分到 4 個分庫 shard0 ,shard1 ,shard2 ,shard3 。首先將 [0-1023] 平均分為 4 個區段:[0-255],[256-511],[512-767],[768-1023],然後對字符串(或子串,由用戶自定義)做 hash。hash 結果對 1024 取模,最終得出的結果 slot 落入哪個區段,便路由到哪個分庫。

cobar 的默認路由算法 ,可以和雪花算法天然融合在一起, 訂單order_id使用雪花算法,我們可以將 slot 的值保存在 10位工作機器ID 里。

通過訂單 order_id 可以反查出 slot , 就可以定位該用戶的訂單數據存儲在哪個分區里。
專車 SDDL 分片算法和 cobar 差異點在於:


7.2 司機維度
雖然解決了主維度乘客分庫分表問題,但專車還有另外一個查詢維度,在司機客戶端,司機需要查詢分配給他的訂單信息。
我們已經按照乘客 user_id 作為 sharding key ,若按照司機 driver_id 查詢訂單的話,需要廣播到每一個分庫並聚合返回,基於此,技術團隊選擇將乘客維度的訂單數據異構到以司機維度的數據庫里。
司機維度的分庫分表策略和乘客維度邏輯是一樣的,只不過 sharding key 變成了司機 driver_id 。
異構神器 canal 解析乘客維度四個分庫的 binlog ,通過 SDDL 寫入到司機維度的四個分庫里。

這裡大家可能有個疑問:雖然可以異構將訂單同步到司機維度的分庫里,畢竟有些許延遲,如何保證司機在司機端查詢到最新的訂單數據呢 ?
在緩存和MQ這一小節里提到:緩存集群中存儲最近七天訂單詳情信息,大量訂單讀請求直接從緩存獲取。訂單服務會緩存司機和當前訂單的映射,這樣司機端的大量請求就可以直接緩存中獲取,而司機端查詢訂單列表的頻率沒有那麼高,異構複製延遲在10毫秒到30毫秒之間,在業務上是完全可以接受的。
7.3 運營維度
專車管理後台,運營人員經常需要查詢訂單信息,查詢條件會比較複雜,專車技術團隊採用的做法是:訂單數據落盤在乘客維度的訂單分庫之後,通過 canal 把數據同步到Elastic Search。

7.4 小表廣播
業務中有一些配置表,存儲重要的配置,讀多寫少。在實際業務查詢中,很多業務表會和配置表進行聯合數據查詢。但在數據庫水平拆分後,配置表是無法拆分的。
小表廣播的原理是:將小表的所有數據(包括增量更新)自動廣播(即複製)到大表的機器上。這樣,原來的分布式 JOIN 查詢就變成單機本地查詢,從而大大提高了效率。
專車場景下,小表廣播是非常實用的需求。比如:城市表是非常重要的配置表,數據量非常小,但訂單服務,派單服務,用戶服務都依賴這張表。
通過 canal 將基礎配置數據庫城市表同步到訂單數據庫,派單數據庫,用戶數據庫。

8 平滑遷移
分庫分表組件 SDDL 研發完成,並在生產環境得到一定程度的驗證後,訂單服務從單庫 MySQL 模式遷移到分庫分表模式條件已經成熟。
遷移思路其實和從 SQLServer 到 MySQL 非常類似。

整體遷移流程:
9 數據交換平台
專車訂單已完成分庫分表 , 很多細節都值得復盤:
面對這些問題,架構團隊的目標是打造一個平台,滿足各種異構數據源之間的實時增量同步和離線全量同步,支撐公司業務的快速發展。

基於這個目標,架構團隊自研了dataLink用於增量數據同步,深度定製了阿里開源的dataX用於全量數據同步。
10 寫到最後
專車架構進化之路並非一帆風順,也有波折和起伏,但一步一個腳印,專車的技術儲備越來越深厚。
2017年,瑞幸咖啡在神州優車集團內部孵化,專車的這些技術儲備大大提升了瑞幸咖啡技術團隊的研發效率,並支撐業務的快速發展。比如瑞幸咖啡的訂單數據庫最開始規劃的時候,就分別按照用戶維度,門店維度各拆分了 8 個數據庫實例,分庫分表組件SDDL和數據交換平台都起到了關鍵的作用 。
- EOF -
1、B站高可用架構實踐
2、[譯] 我做基礎架構學到的 42 件事
3、ElasticSearch讓人嘆為觀止的分布式系統架構設計
看完本文有收穫?請轉發分享給更多人
關注「ImportNew」,提升Java技能
點讚和在看就是最大的支持❤️