某店訂單系統水平分庫的實踐之路以及關鍵步驟


隨着大型互聯網應用的發展,海量數據的存儲和訪問成爲系統設計的瓶頸,分佈式處理成爲不二選擇。數據庫拆分,特別是水平分庫是個高難度的活,涉及一系列技術決策。
本人有幸負責某店訂單水平分庫的方案設計及實施落地,本人結合項目實踐,對水平分庫做一個系統地剖析,希望爲大家水平分庫(包括去IOE)改造提供總體思路,主要內容包括:
1.    水平分庫說明
2.    分庫維度-- 根據哪個字段分庫
3.    分庫策略-- 記錄如何分配到不同庫
4.    分庫數量-- 初始庫數量及庫數量如何增長
5.    路由透明-- 如何實現庫路由,支持應用透明
6.    分頁處理-- 跨多個庫的分頁case如何處理
7.    Lookup映射—非分庫字段映射到分庫字段,實現單庫訪問
8.    整體架構-- 分庫的整體技術架構
9.    上線步驟-- 分庫改造實施上線
10.    項目總結
水平分庫說明
數據庫拆分有兩種:
1)   垂直分庫
數據庫裏的表太多,拿出部分到新的庫裏,一般是根據業務劃分表,關係密切的表放同一數據庫,應用修改數據庫連接即可,比較簡單。
2)   水平分庫
某張表太大,單個數據庫存儲不下或訪問性能有壓力,把一張表拆成多張,每張表存放部分記錄,保存在不同的數據庫裏,水平分庫需要對系統做大的改造。

1號店核心的訂單表存儲在Oracle數據庫,記錄有上億條,字段有上百個,訪問的模式也是複雜多樣,隨着業務快速增長,無論存儲空間或訪問性能都面臨巨大挑戰,特別在大促時,訂單庫已成爲系統瓶頸。
通常有兩種解決辦法:
1.    Scale up,升級Oracle數據庫所在的物理機,提升內存/存儲/IO性能,但這種升級費用昂貴,並且只能滿足短期需要。
2.    Scale out,把訂單庫拆分爲多個庫,分散到多臺機器進行存儲和訪問,這種做法支持水平擴展,可以滿足長遠需要。
1號店採取後一種做法,它的訂單庫主要包括訂單主表/訂單明細表(記錄商品明細)/訂單擴展表,水平分庫即把這3張表的記錄分到多個數據庫中,

 原來一個Oracle庫被多個MySQL庫取代,支持1主多備和讀寫分離,主備之間通過MySQL自帶的數據同步機制(SLA<1秒),所有應用通過訂單服務訪問訂單數據。
分庫維度
水平分庫首先要考慮根據哪個字段作爲分庫維度,選擇標準是儘量避免應用代碼和SQL性能受影響,這就要求當前SQL在分庫後,訪問儘量落在單個庫裏,否則單庫訪問變成多庫掃描,讀寫性能和應用邏輯都會受較大影響。
對於訂單拆分,大家首先想到的是按照用戶Id拆分,結論沒錯,但最好還是數據說話,不能拍腦袋。好的做法是首先收集所有SQL,挑選where語句最常出現的過濾字段,比如用戶Id/訂單Id/商家Id,每個字段在SQL中有三種情況:
1.    單Id過濾,如用戶Id=?
2.    多Id過濾,如用戶Id IN (?,?,?)
3.    該Id不出現
然後進一步統計,假設共有500個SQL訪問訂單庫,3個過濾字段出現情況如下:

過濾字段

Id過濾

Id過濾

不出現

用戶Id

120

40

330

訂單Id

60

80

360

商家Id

15

0

485

結論明顯,應該選擇用戶Id進行分庫。
等一等,這只是靜態分析,每個SQL訪問的次數是不一樣的,因此還要分析每個SQL的訪問量。我們分析了Top15執行最多的SQL (它們佔總執行次數85%),如果按照用戶Id分庫,這些SQL 85%落到單個數據庫, 13%落到多個數據庫,只有2%需要遍歷所有數據庫,明顯優於使用其他Id進行分庫。
通過量化分析,我們知道按照用戶Id分庫是最優的,同時也大致知道分庫對現有系統的影響,比如這個例子中,85%的SQL會落到單個數據庫,這部分的訪問性能會優化,堅定了各方對分庫的信心。
分庫策略
分庫維度確定後,如何把記錄分到各個庫裏呢?一般有兩種方式:
    根據數值範圍,比如用戶Id爲1-9999的記錄分到第一個庫,10000-20000的分到第二個庫,以此類推。
    根據數值取模,比如用戶Id mod n,餘數爲0的記錄放到第一個庫,餘數爲1的放到第二個庫,以此類推。
兩種分法的優劣比較如下:

評價指標

按照範圍分庫

按照Mod分庫

庫數量

前期數目比較小,可以隨用戶/業務按需增長

前期即根據mode因子確定庫數量,數目一般比較大

訪問性能

前期庫數量小,全庫查詢消耗資源少,單庫查詢性能略差

前期庫數量大,全庫查詢消耗資源多,單庫查詢性能略好

調整庫數量

比較容易,一般只需爲新用戶增加庫,老庫拆分也隻影響單個庫

困難,改變mod因子導致數據在所有庫之間遷移

數據熱點

新舊用戶購物頻率有差異,有數據熱點問題

新舊用戶均勻到分佈到各個庫,無熱點

實踐中,爲了處理簡單,選擇mod分庫的比較多。同時二次分庫時,爲了數據遷移方便,一般是按倍數增加,比如初始4個庫,二次分裂爲8個,再16個。這樣對於某個庫的數據,一半數據移到新庫,剩餘不動,對比每次只增加一個庫,所有數據都要大規模變動。
補充下,mod分庫一般每個庫記錄數比較均勻,但也有些數據庫,存在超級Id,這些Id的記錄遠遠超過其他Id,比如在廣告場景下,某個大廣告主的廣告數可能佔總體很大比例。如果按照廣告主Id取模分庫,某些庫的記錄數會特別多,對於這些超級Id,需要提供單獨庫來存儲記錄。
分庫數量
分庫數量首先和單庫能處理的記錄數有關,一般來說,Mysql 單庫超過5000萬條記錄,Oracle單庫超過1億條記錄,DB壓力就很大(當然處理能力和字段數量/訪問模式/記錄長度有進一步關係)。
在滿足上述前提下,如果分庫數量少,達不到分散存儲和減輕DB性能壓力的目的;如果分庫的數量多,好處是每個庫記錄少,單庫訪問性能好,但對於跨多個庫的訪問,應用程序需要訪問多個庫,如果是併發模式,要消耗寶貴的線程資源;如果是串行模式,執行時間會急劇增加。
最後分庫數量還直接影響硬件的投入,一般每個分庫跑在單獨物理機上,多一個庫意味多一臺設備。所以具體分多少個庫,要綜合評估,一般初次分庫建議分4-8個庫。
路由透明
分庫從某種意義上來說,意味着DB schema改變了,必然影響應用,但這種改變和業務無關,所以要儘量保證分庫對應用代碼透明,分庫邏輯儘量在數據訪問層處理。當然完全做到這一點很困難,具體哪些應該由DAL負責,哪些由應用負責,這裏有一些建議:
1.    對於單庫訪問,比如查詢條件指定用戶Id,則該SQL只需訪問特定庫。此時應該由DAL層自動路由到特定庫,當庫二次分裂時,也只要修改mod 因子,應用代碼不受影響。
2.    對於簡單的多庫查詢,DAL負責彙總各個數據庫返回的記錄,此時仍對上層應用透明。
3.    對於帶聚合運算的多庫查詢,如帶groupBy/orderby/min/max/avg等關鍵字,建議DAL彙總單個庫返回的結果,上層應用做進一步處理。一方面DAL全面支持各種case,實現很複雜;另一方面,從1號店實踐來看,這樣的例子不多,在上層應用作針對性處理,更加靈活。
DAL可進一步細分爲JDBC和DAL兩層,基於JDBC層面實現分庫路由,系統開發難度大,靈活性低,目前也沒有很好的成功案例;一般是基於持久層框架進一步封裝成DDAL(分佈式數據訪問層),實現分庫路由,1號店DAL即基於iBatis進行上層封裝而來。
分頁處理
分庫後,有些分頁查詢需要遍歷所有庫,這些case是分庫最大的受害者L。
舉個分頁的例子,比如要求按時間順序展示某個商家的訂單,每頁100條記錄,由於是按商家查詢,需要遍歷所有數據庫,假設庫數量是8,我們來看下分頁處理邏輯:
1.    如果取第1頁數據,則需要從每個庫裏按時間順序取前100條記錄,8個庫彙總後有800條,然後對這800條記錄在應用裏進行二次排序,最後取前100條。
2.    如果取第10頁數據,則需要從每個庫裏取前1000(100*10)條記錄,彙總後有8000條記錄,然後對這8000條記錄二次排序後取(900,1000)條記錄。
分庫情況下,對於第k頁記錄,每個庫要多取100*(k-1)條記錄,所有庫加起來,多取的記錄更多,所以越是靠後的分頁,系統要耗費更多內存和執行時間。
對比沒分庫的情況,無論取那一頁,都只要從單個DB裏取100條記錄,而且無需在應用內部做二次排序,非常簡單。
那如何解決分庫情況下的分頁問題呢?有以下幾種辦法:
1.    如果是在前臺應用提供分頁,則限定用戶只能看前面n頁,這個限制在業務上也是合理的,一般看後面的分頁意義不大(如果一定要看,可以要求用戶縮小範圍重新查詢)。
2.    如果是後臺批處理任務要求分批獲取數據,則可以加大page size,比如每次獲取5000條記錄,有效減少分頁數(當然離線訪問一般走備庫,避免衝擊主庫)。
3.    分庫設計時,一般還有配套大數據平臺彙總所有分庫的記錄,有些分頁查詢可以考慮走大數據平臺。
Lookup映射
分庫字段只有一個,比如這裏是用戶Id,但訂單表還有其他字段可唯一區分記錄,比如訂單Id,給定一個訂單Id,相應記錄一定在某個庫裏。如果盲目地查詢所有分庫,則帶來不必要的開銷,Lookup映射可根據訂單Id,找到相應的用戶Id,從而實現單庫定位。
 可以事先檢索所有訂單Id和用戶Id,保存在Lookup表裏,Lookup表的記錄數和訂單庫記錄總數相等,但它只有2個字段,所以存儲和查詢性能都不是問題。實際使用時,一般通過分佈式緩存來優化Lookup性能。對於新增的訂單,除了寫訂單表,同時要寫Lookup表。
整體架構
1號店訂單水平分庫的總體技術架構如下圖所示:

 
1.    上層應用通過訂單服務/分庫代理和DAL訪問數據庫。
2.    代理對訂單服務實現功能透明,包括聚合運算,非用戶Id到用戶Id的映射。
3.    Lookup表用於訂單Id/用戶Id映射,保證按訂單Id訪問時,可以直接落到單個庫,Cache是Lookup的內存數據映像,提升性能,cache故障時,直接訪問Lookup表。
4.    DAL提供庫的路由,根據用戶Id定位到某個庫,對於多庫訪問,DAL支持可選的併發訪問模式,並支持簡單記錄彙總。
5.    Lookup表初始化數據來自於現有分庫數據,新增記錄時,直接由代理異步寫入。
上線步驟
訂單表是核心業務表,它的水平拆分影響很多業務,本身的技術改造也很大,很容易出紕漏,上線時,必須謹慎考慮,1號店整個方案實施過程如下:
首先實現Oracle和MySQL兩套庫並行,所有數據訪問指向Oracle庫,通過數據同步程序把數據從Oracle拆分到多個MySQL分庫,比如3分鐘增量同步一次。
1.    按照上述架構圖搭建整個體系,選擇幾個對數據實時性不高的訪問例子(如訪問歷史訂單),轉向MySQL分庫訪問,然後逐漸增加更多非實時case,以檢驗整套體系可行性。
2.    如果性能和功能都沒問題,再一次性把所有實時讀寫訪問轉向MySQL,廢棄Oracle。
這個上線步驟多了數據同步程序的開發(大約1人周工作量,風險很低),但分散了風險,把第一步的技術風險(Lookup/DAL等基礎設施改造)和第二步的業務功能風險(Oracle改MySQL語法)分開。1號店兩階段上線都是一次性成功,特別是第二階段上線,100多個依賴方應用簡單重啓即完成升級,中間沒有出現一例較大問題。
項目總結
1號店完成訂單水平分庫的同時,把訂單庫從Oralce遷到MySQL,設備從小型機換成X86服務器,通過水平分庫和去IOE,不但支持訂單量未來增長,並且總體成本也大幅下降。
由於去IOE和訂單分庫一起實施,帶來雙重的性能影響,我們花了很大精力做性能測試,爲了模擬真實場景,大家通過Tcpcopy把線上實際的查詢流量引到測試環境,先後經過13輪的性能測試,最終6個MySQL庫相對一個Oracle,平均SQL執行時間基本持平,性能不降低的情況下,優化了架構,節省了成本。
對核心表做水平分庫之前,必須先做好服務化,即外部系統通過統一的訂單服務訪問相關表,不然很容易遺漏一些SQL。
1號店最終是根據用戶Id後三位取模,初始分6個庫,理論上支持多達768個庫,並且對訂單Id生成規則做了改造,使其包括用戶Id後三位,這樣新訂單Id本身包含庫定位所需信息,無需走Lookup機制,隨着老訂單歸檔到歷史庫,上述架構中lookup部分可廢棄。
水平分庫是一項系統性工作,首先需要在理論模式指導下,結合實際情況,每個方面做出最優選擇。其次對於特殊場景,如跨庫分頁,沒有銀彈,可以靈活處理,不走常規路。最後控制好節奏,系統改造、數據遷移、上線實施等各個環節做好銜接,全局一盤棋。
大膽設計,小心求證,謹慎實施,分庫並不難。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章