乾貨 | 資料為王,攜程國際火車票的 ShardingSphere 之路

SphereEx發表於2021-08-25

作者簡介

 

瑞華,攜程高階後端開發工程師,關注系統架構、分庫分表、微服務、高可用等。


一、前言

隨著國際火車票業務的高速發展,訂單量快速增長,單資料庫瓶頸層面的問題逐漸顯露,常規的資料庫最佳化已無法達到期望的效果。同時,原先的底層資料庫設計,也存在一些歷史遺留問題,比如存在部分無用欄位、表透過自增主鍵關聯和各個應用直連資料庫等問題。

為此,經過討論後,我們決定對訂單庫進行分庫分表,同時對訂單表進行重構,進而從根本上解決這些問題。

二、問題挑戰

目標確定後,實踐起來可不輕鬆,出現了很多的問題和挑戰。這裡列舉一些典型問題,大致可以分為兩大類:分庫分表通用問題、具體業務關聯問題。
分庫分表通用問題

  • 如何切分,垂直分還是水平分?分片的鍵,如何選取?
  • 如何根據鍵值路由到對應庫、對應表?
  • 採用什麼中介軟體,代理方式還是中介軟體的方式?
  • 跨庫操作等問題,如跨庫事務和跨庫關聯?
  • 資料擴容問題,後續如何進行擴容?

具體業務關聯問題

  • 各個應用直連資料如何解決?
  • 如何進行平滑過渡?
  • 歷史資料如何恰當遷移?

三、方案選型

3.1 如何切分

切分方式,一般分為垂直分庫、垂直分表、水平分庫和水平分表四種,如何選擇,一般是根據自己的業務需求決定。
我們的目標是要從根本上解決資料量大、單機效能問題等問題,垂直方式並不能滿足需求,所以我們選取了水平分庫+水平分表的切分方式。

3.2 分片鍵選取

一般是根據自己的實際業務,來選擇欄位來作為分片的鍵,同時可以結合考慮資料的熱點問題 、分佈問題。比如訂單系統,不能根據國家欄位進行分片,否則可能會出現某些國家很多的訂單記錄,某些國家幾乎沒有訂單記錄,進而資料分佈不均。相對正確的方式,比如訂單類系統,可以選擇訂單 ID;會員系統,可以選擇會員 ID。

3.3 如何路由

選定了分片的鍵之後,接下來需要探討的問題,就是如何路由到具體的資料庫和具體的表。以分片鍵路由到具體某一個資料庫為例,常見的路由方式如下:

對映路由

對映路由,即新增一個庫,新建一個路由對映表,儲存分片鍵值和對應的庫之間的對映關係。比如,鍵值為 1001,對映到 db01 這個資料庫,如下圖所示:

對映方式,優點是對映方式可任意調整,擴容簡單,但是存在一個比較嚴重的不足,就是對映庫中的對映表的資料量異常巨大。我們本來的目標是要實現分庫分表的功能,可是現在,對映庫對映表相當於回到了分庫分表之前的狀態。所以,我們在實踐中,沒有采取這種方式。

分組路由

分組路由,即對分片的鍵值,進行分組,每組對應到一個具體的資料庫。比如,鍵值為 1000 到 2000,則儲存到 db01 這個資料庫,如下圖所示:

分組方式,優點是擴容簡單,實現簡單,但是也存在一個比較嚴重的不足,是資料分佈熱點問題,比如在某一個時間內,分片鍵值為 2001,則在將來一段時間內,所有的資料流量,全部打到某一個庫(db02)。這個問題,在網際網路環境下,也比較嚴重,比如在一些促銷活動中,訂單量會有一個明顯的飆升,這時候各個資料庫不能達到分攤流量的效果,只有一個庫在接收流量,會回到分庫分表之前的狀態。所以,我們也沒有采取這種方式。

雜湊路由

雜湊路由,即對分片的鍵值,進行雜湊,然後根據雜湊結果,對應到一個具體的資料庫。比如,鍵值為 1000,對其取雜湊的結果為 01,則儲存到 db01 這個資料庫,如下圖所示:

雜湊方式,優點是分佈均勻,無熱點問題,但是反過來,資料擴容比較麻煩。因為在擴容過程中,需要調整雜湊函式,隨之帶出一個資料遷移問題。網際網路環境下,遷移過程中往往不能進行停服,所以就需要類似多庫雙寫等方式進行過渡,比較麻煩。所以,在實踐中也沒有采取這種方式。

分組雜湊路由

分組雜湊路由,即對分片的鍵值,先進行分組,後再進行雜湊。如下圖所示:

在實踐中,我們結合了前面的幾種方式,借鑑了他們的優點不足,而採用了此種方式。因為分組方式,能很方便的進行擴容,解決了資料擴容問題;雜湊方式,能解決分佈相對均勻,無單點資料庫熱點問題。

3.4 技術中介軟體

分庫分表的中介軟體選取,在行業內的方案還是比較多的,公司也有自己的實現。根據實現方式的不同,可以分為代理和非代理方式,下面列舉了一些業界常見的中介軟體,如下表(截至於 2021-04-08):

我們為什麼最終選擇了 ShardingSphere 呢?主要從這幾個因素考慮:

技術環境

  • 我們團隊是 Java 體系下的,對 Java 中介軟體有一些偏愛
  • 更偏向於輕量級元件,可以深入研究的元件
  • 可能會需要一些個性定製化

專業程度

  • 取決於中介軟體由哪個團隊進行維護,是否是名師打造,是否是行業標杆
  • 更新迭代頻率,最好是更新相對頻繁,維護較積極的
  • 流行度問題,偏向於流行度廣、社群活躍的中介軟體
  • 效能問題,效能能滿足我們的要求

使用成本

  • 學習成本、入門成本和定製改造成本
  • 弱浸入性,對業務能較少浸入
  • 現有技術棧下的遷移成本,我們當前技術棧是 SSM 體系下

運維成本

  • 高可用、高穩定性
  • 減少硬體資源,不希望再單獨引入一個代理中介軟體,還要考慮運維成本
  • 豐富的埋點、完善的監控

四、業務實踐

在業務實踐中,我們經歷了從新庫新表的設計,分庫分表自建代理、服務收口、上游訂單應用遷移,歷史資料遷移等過程。

4.1 新表模型

為了建立分庫分表下的關聯關係,和更加合理有效的結構,我們新申請了訂單分庫分表的幾個庫,設計了一套全新的表結構。表名以年份結尾、規範化表欄位、適當增刪了部分欄位、不使用自增主鍵關聯,採用業務唯一鍵進行關聯等。
表結構示例如下圖:

4.2 服務收口

自建了一個分庫分表資料庫的服務代理 Dal-Sharding。每一個需要操作訂單庫的服務,都要透過代理服務進行運算元據庫,達到服務的一個收口效果。同時,遮蔽了分庫分表的複雜性,規範資料庫的基本增刪改查方法。

4.3 平滑過渡

應用遷移過程中,為了保證應用的平滑過渡,我們新增了一些同步邏輯,來保證應用的順利遷移,在應用遷移前後,對應用沒有任何影響。未遷移的應用,可以讀取到遷移後應用寫入的訂單資料;遷移後的應用,能讀取到未遷移應用寫入的訂單資料。同時,統一實現了此邏輯,減少各個應用的遷移成本。

新老庫雙讀
顧名思義,就是在讀取的時候,兩個庫可能都要進行讀取,即優先讀取新庫,如果能讀到記錄,直接返回;否則,再次讀取老庫記錄,並返回結果。
雙讀的基本過程如下:

新老庫雙讀,保證了應用遷移過程中讀取的低成本,上游應用不需要關心資料來源於新的庫還是老的庫,只要關心資料的讀取即可,減少了切換新庫和分庫分表的邏輯,極大的減少了遷移的工作量。
實踐過程中,我們透過切面實現雙讀邏輯,將雙讀邏輯放入到切面中進行,減小新庫的讀取邏輯的侵入,方便後面實現對雙讀邏輯的移除調整。
同時,新增一些配置,比如可以控制到哪些表需要進行雙讀,哪些表不需要雙讀等。

新老庫雙寫
新老庫雙寫,就是在寫入新庫成功後,非同步寫入到老庫中。雙寫使得新老庫都同時存在這些訂單資料,尚未遷移透過代理服務運算元據庫的應用得以正常的運作。
雙寫的基本過程如下:


雙寫其實有較多的方案,比如基於資料庫的日誌,透過監聽解析資料庫日誌實現同步;也可以透過切面,實現雙寫;還可以透過定時任務進行同步;另外,結合到我們自己的訂單業務,我們還可以透過訂單事件(比如創單成功、出票成功、退票成功等),進行雙寫,同步資料到老庫中。
目前,我們經過考慮,沒有透過資料庫日誌來實現,因為這樣相當於把邏輯下沉到了資料庫層面,從實現上不夠靈活,同時,可能還會涉及到一些許可權、排期等問題。實踐中,我們採取其他三種方式,互補形式,進行雙寫。非同步切面雙寫,保證了最大的時效性;訂單事件,保證了核心節點的一致性;定時任務,保證了最終的一致性。
跟雙讀一樣,我們也支援配置控制到哪些表需要進行雙寫,那些表不需要雙寫等。

過渡遷移
有了前面的雙讀雙寫作為基礎,遷移相對容易實行,我們採取逐個遷移的方式,比如,按照服務、按照渠道和按照供應進行遷移,將遷移工作進行拆解,減少影響面,追求穩健。一般分為三步走方式:
1)第一階段,先在新對接的供應商中進行遷移新庫,因為新上線的供應商,訂單量最少,同時哪怕出現了問題,不至於影響到之前的業務。
2)再次遷移量比較少的線上業務,此類訂單,有一些量,但是追求穩定,不能因為切換新庫而產生影響。所以,將此類業務放到了第二階段中進行。
3)最後一步是,將量較大的業務,逐漸遷移到新庫中,此類業務,需要在在有前面的保證後,方能進行遷移,保證訂單的正常進行。

4.4 資料遷移

資料遷移,即將資料,從老庫遷移到新庫,是新老庫切換的一個必經過程。遷移的常規思路,一般是每個表一個個進行遷移,結合業務,我們沒有采取此做法,而是從訂單維度進行遷移。
舉個例子:假如訂單庫有 Order 表、OrderStation 表、OrderFare 表三個表,我們沒有采取一個一個表分別進行遷移,而是根據訂單號,以每一個訂單的資訊,進行同步。
大致過程如下:
1)開啟一個定時任務,查詢訂單列表,取得訂單號等基本訂單資訊。
2)根據這個訂單號,去分別查詢訂單的其他資訊,取得一個完整的訂單資訊。
3)校驗訂單是否已經完成同步,之前完成同步了則直接跳過,否則繼續執行下一個訂單號。
4)將老庫的完整的訂單資訊,對映成新庫的對應的模型。
5)將新的訂單資訊,同步寫入到新庫各個表中。
6)繼續執行下一個訂單號,直到所有的訂單號都完全同步結束。

4.5 完成效果

訂單庫經過一個全新的重構,目前已經線上上穩定執行,效果顯著,達到了我們想要的效果。

  • 服務收口,將分庫分表邏輯,收口到了一個服務中;
  • 介面統一管理,統一對敏感欄位進行加密;
  • 功能靈活,提供豐富的功能,支援定製化;
  • 分庫分表路由透明,且基於主流技術,易於上手;
  • 完善的監控,支援到表維度的監控;


五、 常見問題總結

5.1 分庫分表典型問題

問題1:如何進行跨庫操作,關聯查詢,跨庫事務?

回答:對於跨庫操作,在訂單主流程應用中,我們目前是禁止了比如跨庫查詢、跨庫事務等操作的。對於跨庫事務,因為根據訂單號、建立年份路由,都是會路由到同一個資料庫中,也不會存在跨庫事務。同樣對於跨庫關聯查詢,也不會存在,往往都是根據訂單來進行查詢。同時,也可以適當進行冗餘,比如儲存車站編碼的同時,多儲存一個車站名稱欄位。

問題2:如何進行分頁查詢?

回答:目前在訂單主流程應用中的分頁查詢,我們直接採用了 Sharding-JDBC 提供的最原始的分頁方式,直接按照正常的分頁 SQL,來進行查詢分頁即可。理由:主流程訂單服務,比如出票系統,往往都是查詢前面幾頁的訂單,直接查詢即可,不會存在很深的翻頁。當然,對於要求較高的分頁查詢,可以去實現二次查詢,來實現更加高效的分頁查詢。

問題3:如何支援很複雜的統計查詢?
回答:專門增加了一個寬表,來滿足那些很複雜查詢的需求,將常用的查詢資訊,全部落到此表中,進而可以快速得到這些複雜查詢的結果。

5.2 API 方法問題

問題:服務收口後,如何滿足業務各種不同的查詢條件?
回答:我們的 API 方法,相對固定,一般查詢類只有兩個方法,根據訂單號查詢,和根據 Condition 查詢條件進行查詢。對於各種不同的查詢條件,則透過新增 Condition 的欄位屬性來實現,而不會新增各種查詢方法。

5.3 均勻問題

問題:在不同 group 中,資料會存在分佈不均勻,存在熱點問題?
回答:是的,比如執行 5 年後,我們擴充成了 3 個 group,每一個 group 中存在 3 個庫,那麼此時,讀寫最多的應該是第三個 group。不過這種分佈不均勻問題和熱點問題,是可接受的,相當於前面的兩個 group,可以作為歷史歸檔 group,目前主要使用的 group 為第三個 group。
隨著業務的發展,你可以進行調配,比如業務發展迅速,那麼相對合理的分配,往往不會是每個 group 是 3 個庫,更可能是應該是,越往後 group 內的庫越多。同時,因為每個 group 內是存在多個庫,與之前的某一個庫的熱點問題是存在本質差別,而不用擔心單資料庫瓶頸問題,可以透過加庫來實現擴充套件。

5.4 Group 內路由問題

問題:對於僅根據訂單號查詢,在 group 內的路由過程是讀取 group 內所有的表嗎?
回答:根據目前的設計,是的。目前是按年份分組,訂單號不會儲存其他資訊,採用攜程統一方式生成,也就是如果根據訂單號查詢,我們並不知道是存在於哪個表,則需要查詢 group 內所有的表。對於此類問題,通常推薦做法是,可以適當增加因子,在訂單號中,儲存建立年份資訊,這樣就可以知道對應那個表了;也可以年份適當進行延伸,比如每 5 年一次分表,那麼這樣調整後,一個 group 內的表應該相對很少,可以極大加快查詢效能。

5.5 非同步雙寫問題

問題:為什麼雙寫過程,採用了多種方式結合的方式?

回答:首先,切面方式,能最大限度滿足訂單同步的時效性。但是,在實踐過程中,我們發現,非同步切面雙寫,會存在多執行緒併發問題。因為在老庫中,表的關聯關係依賴於資料庫的自增 ID,依賴於表的插入順序,會存在關聯失敗的情況。所以,單純依靠切面同步還不夠,還需要更加穩健的方式,即定時任務(訂單事件是不可靠訊息事件,即可能會存在丟失情況)的方式,來保證資料庫的一致性。

關於作者
我們是攜程火車票研發團隊,負責火車票業務的開發以及創新。火車票研發在多種交通線路聯程聯運演算法、多種交通工具一站式預定、高併發方向不斷地深入探索和創新,持續最佳化使用者體驗,提高效率,致力於為全球人民買全球火車票。


歡迎掃碼關注我們

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/70001955/viewspace-2788659/,如需轉載,請註明出處,否則將追究法律責任。

相關文章