很多年前,讀了子柳老師的《淘寶技術這十年》。這本書成為了我的架構啟蒙書,書中的一句話像種子一樣深埋在我的腦海裡:“好的架構是進化來的,不是設計來的”。
2015年,我加入神州專車訂單研發團隊,親歷了專車資料層「架構進化」的過程。這次工作經歷對我而言非常有啟發性,也讓我經常感慨:“好的架構果然是一點點進化來的”。
1 單資料庫架構
產品初期,技術團隊的核心目標是:“快速實現產品需求,儘早對外提供服務”。
彼時的專車服務都連同一個 SQLServer 資料庫,服務層已經按照業務領域做了一定程度的拆分。
這種架構非常簡單,團隊可以分開協作,效率也極高。隨著專車訂單量的不斷增長,早晚高峰期,使用者需要叫車的時候,點選下單後經常無響應。
系統層面來看:
- 資料庫瓶頸顯現。頻繁的磁碟操作導致資料庫伺服器 IO 消耗增加,同時多表關聯,排序,分組,非索引欄位條件查詢也會讓 cpu 飆升,最終都會導致資料庫連線數激增;
- 閘道器大規模超時。在高併發場景下,大量請求直接運算元據庫,資料庫連線資源不夠用,大量請求處於阻塞狀態。
2 SQL優化和讀寫分離
為了緩解主資料庫的壓力,很容易就想到的策略:SQL優化。通過效能監控平臺和 DBA 同學協作分析出業務慢 SQL ,整理出優化方案:
- 合理新增索引;
- 減少多表 JOIN 關聯,通過程式組裝,減少資料庫讀壓力;
- 減少大事務,儘快釋放資料庫連線。
另外一個策略是:讀寫分離。
讀寫分離的基本原理是讓主資料庫處理事務性增、改、刪操作( INSERT、UPDATE、DELETE),而從資料庫處理 SELECT 查詢操作。
專車架構團隊提供的框架中,支援讀寫分離,於是資料層架構進化為如下圖:
讀寫分離可以減少主庫寫壓力,同時讀從庫可水平擴充套件。當然,讀寫分離依然有侷限性:
- 讀寫分離可能面臨主從延遲的問題,訂單服務載客流程中對實時性要求較高,因為擔心延遲問題,大量操作依然使用主庫查詢;
- 讀寫分離可以緩解讀壓力,但是寫操作的壓力隨著業務爆發式的增長並沒有很有效的緩解。
3 業務領域分庫
雖然應用層面做了優化,資料層也做了讀寫分離,但主庫的壓力依然很大。接下來,大家不約而同的想到了業務領域分庫,也就是:將資料庫按業務領域拆分成不同的業務資料庫,每個系統僅訪問對應業務的資料庫。
業務領域分庫可以緩解核心訂單庫的效能壓力,同時也減少系統間的相互影響,提升了系統整體穩定性。
隨之而來的問題是:原來單一資料庫時,簡單的使用 JOIN 就可以滿足需求,但拆分後的業務資料庫在不同的例項上,就不能跨庫使用 JOIN了,因此需要對系統邊界重新梳理,業務系統也需要重構 。
重構重點包含兩個部分:
- 原來需要 JOIN 關聯的查詢修改成 RPC 呼叫,程式中組裝資料 ;
- 業務表適當冗餘欄位,通過訊息佇列或者異構工具同步。
4 快取和MQ
專車服務中,訂單服務是併發量和請求量最高,也是業務中最核心的服務。雖然通過業務領域分庫,SQL 優化提升了不少系統效能,但訂單資料庫的寫壓力依然很大,系統的瓶頸依然很明顯。
於是,訂單服務引入了 快取 和 MQ 。
乘客在使用者端點選立即叫車,訂單服務建立訂單,首先儲存到資料庫後,然後將訂單資訊同步儲存到快取中。
在訂單的載客生命週期裡,訂單的修改操作先修改快取,然後傳送訊息到 MetaQ ,訂單落盤服務消費訊息,並判斷訂單資訊是否正常(比如有無亂序),若訂單資料無誤,則儲存到資料庫中。
核心邏輯有兩點:
- 快取叢集中儲存最近七天訂單詳情資訊,大量訂單讀請求直接從快取獲取;
- 在訂單的載客生命週期裡,寫操作先修改快取,通過訊息佇列非同步落盤,這樣訊息佇列可以起到消峰的作用,同樣可以降低資料庫的壓力。
這次優化提升了訂單服務的整體效能,也為後來訂單服務庫分庫分表以及異構打下了堅實的基礎。
5 從 SQLServer 到 MySQL
業務依然在爆炸增長,每天幾十萬訂單,訂單表資料量很快將過億,資料庫天花板遲早會觸及。
訂單分庫分表已成為技術團隊的共識。業界很多分庫分表方案都是基於 MySQL 資料庫,專車技術管理層決定先將訂單庫整體先從 SQLServer 遷移到 MySQL 。
遷移之前,準備工作很重要 :
- SQLServer 和 MySQL 兩種資料庫語法有一些差異,訂單服務必須要適配 MySQL 語法。
- 訂單 order_id 是主鍵自增,但在分散式場景中並不合適,需要將訂單 id 調整為分散式模式。
當準備工作完成後,才開始遷移。
遷移過程分兩部分:歷史全量資料遷移 和 增量資料遷移。
歷史資料全量遷移主要是 DBA 同學通過工具將訂單庫同步到獨立的 MySQL 資料庫。
增量資料遷移:因為 SQLServer 無 binlog 日誌概念,不能使用 maxwell 和 canal 等類似解決方案。訂單團隊重構了訂單服務程式碼,每次訂單寫操作的時候,會傳送一條 MQ 訊息到 MetaQ 。為了確保遷移的可靠性,還需要將新庫的資料同步到舊庫,也就是需要做到雙向同步 。
遷移流程:
- 首先訂單服務(SQLServer版)傳送訂單變更訊息到 MetaQ ,此時並不開啟「舊庫訊息消費」,讓訊息先堆積在 MetaQ 裡;
- 然後開始遷移歷史全量資料,當全量遷移完成後,再開啟「舊庫訊息消費」,這樣新訂單庫就可以和舊訂單庫資料保持同步了;
- 開啟「新庫訊息消費」,然後部署訂單服務( MySQL 版),此時訂單服務有兩個版本同時執行,檢測資料無誤後,逐步增加新訂單服務流量,直到老訂單服務完全下線。
6 自研分庫分表元件
業界分庫分表一般有 proxy 和 client 兩種流派。
▍ proxy模式
代理層分片方案業界有 Mycat ,cobar 等 。
它的優點:應用零改動,和語言無關,可以通過連線共享減少連線數消耗。缺點:因為是代理層,存在額外的時延。
▍ 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 , 就可以定位該使用者的訂單資料儲存在哪個分割槽裡。
Integer getWorkerId(Long orderId) {
Long workerId = (orderId >> 12) & 0x03ff;
return workerId.intValue();
}
專車 SDDL 分片演算法和 cobar 差異點在於:
- cobar 支援最大分片數是1024,而 SDDL 最大支援分庫數1024*8=8192,同樣分四個訂單庫,每個分片的 slot 區間範圍是2048 ;
- 因為要支援8192個分片,雪花演算法要做一點微調,雪花演算法的10位工作機器修改成13位工作機器,時間戳也調整為:38位時間戳(由某個時間點開始的毫秒數)。
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 非常類似。
整體遷移流程:
- DBA 同學準備乘客維度的四個分庫,司機維度的四個分庫 ,每個分庫都是最近某個時間點的全量資料;
- 八個分庫都是全量資料,需要按照分庫分表規則刪除八個分庫的冗餘資料 ;
- 開啟正向同步,舊訂單資料按照分庫分表策略落盤到乘客維度的分庫,通過 canal 將乘客維度分庫訂單資料異構複製到司機維度的分庫中;
- 開啟反向同步,修改訂單應用的資料來源配置,重啟訂單服務,訂單服務新建立的訂單會落盤到乘客維度的分庫,通過 canal 將乘客維度分庫訂單資料異構到全量訂單庫以及司機維度的資料庫;
- 驗證資料無誤後,逐步更新訂單服務的資料來源配置,完成整體遷移。
9 資料交換平臺
專車訂單已完成分庫分表 , 很多細節都值得覆盤:
- 全量歷史資料遷移需要 DBA 介入 ,技術團隊沒有成熟的工具或者產品輕鬆完成;
- 增量資料遷移通過 canal 來實現。隨著專車業務的爆發增長,資料庫映象,實時索引構建,分庫異構等需求越來越多,雖然canal 非常優秀,但它還是有瑕疵,比如缺失任務控制檯,資料來源管理能力,任務級別的監控和報警,操作審計等功能。
面對這些問題,架構團隊的目標是打造一個平臺,滿足各種異構資料來源之間的實時增量同步和離線全量同步,支撐公司業務的快速發展。
基於這個目標,架構團隊自研了 dataLink 用於增量資料同步,深度定製了阿里開源的 dataX 用於全量資料同步。
10 寫到最後
專車架構進化之路並非一帆風順,也有波折和起伏,但一步一個腳印,專車的技術儲備越來越深厚。
2017年,瑞幸咖啡在神州優車集團內部孵化,專車的這些技術儲備大大提升了瑞幸咖啡技術團隊的研發效率,並支撐業務的快速發展。 比如瑞幸咖啡的訂單資料庫最開始規劃的時候,就分別按照使用者維度,門店維度各拆分了8個資料庫例項,分庫分表元件 SDDL 和 資料交換平臺都起到了關鍵的作用 。
好了,這篇文字就寫到這裡了。 我們下期見。
如果我的文章對你有所幫助,還請幫忙點贊、在看、轉發一下,你的支援會激勵我輸出更高質量的文章,非常感謝!