資料量大讀寫緩慢如何最佳化(4)「分庫分表」

在資料庫表資料量大讀寫緩慢如何最佳化(2)「查詢分離」 文章中說到,查詢分離中存在三大不足,其中一個不足就是:當主資料量越來越大,寫操作緩慢,遇到這個問題我們該如何解決呢?

一、業務場景三

為了便於理解,我們透過一個業務場景來入手。

有一個電商系統架構最佳化工作,該系統中包含使用者和訂單2個主要實體,每個實體涵蓋資料量如下表所示:

資料量大讀寫緩慢如何最佳化(4)「分庫分表」

從上表中發現,目前訂單資料量已達上億,並且每日以百萬級速度增長,之後還可能是千萬級。

面對如此大的資料量,此時儲存訂單的資料庫竟然還是一個單庫單表。對於單庫單表而言,一旦資料量實現瘋狂增長,無論是IO還是CPU都會扛不住。

為了使系統抗住千萬級資料量的壓力,各種SQL最佳化都已經做完,最終確定下來的方式是將訂單表拆分,再進行分佈儲存,這也就是本章我們要討論的內容——分庫分表。

說到分庫分表解決方案,我們首先需要做的就是搞定拆分儲存的技術選型問題。

二、拆分儲存的技術選型

關於拆分儲存常用的技術解決方案,市面上目前主要分為4種:MySQL的分割槽技術、NoSql、NewSQL、基於MySQL的分庫分表。

1、MySQL的分割槽技術

資料量大讀寫緩慢如何最佳化(4)「分庫分表」

從上面的MySQL架構圖中,我們不難發現MySQL的分割槽主要在檔案儲存層做文章,它可以將一張表的不同存放在不同儲存檔案中,這對使用者來說比較透明。

在以往的實戰專案中,我們不使用它的原因主要有三點。

1、MySQL的例項只有一個,它僅僅分攤了儲存,無法分攤請求負載。

2、正是因為MySQL的分割槽對使用者透明,所以使用者在實際操作時往往不太注意,使得跨分割槽操作嚴重影響系統性能。

3、當然,MySQL還有一些其他限制,比如不支援query cache、位操作表示式等。

2、NoSQL(如MongoDB)

比較典型的NoSQL資料庫就是MongoDB啦。MongoDB的分片功能從併發性和資料量這兩個角度已經能滿足一版大資料量的需求,但是需要注意這三大要點。

1、約束考量

:MongoDB不是關係型資料庫而是文件型資料庫,它的每一行記錄都是一個結構靈活可變的JSON,比如儲存非常重要的訂單資料時,我們就不能使用MongoDB,因為訂單資料必須使用強約束的關係型資料庫進行儲存。

2、業務功能考量

:多年來,事務、鎖、SQL、表示式等千奇百怪的操作都在MySQL身上一一驗證過,MySQL可以說是久經考驗,因此在功能上MySQL能滿足我們所有的業務需求,MongoDB卻不能,且大部分的NoSQL也存在類似的問題。

3、穩定性考量

:我們對MySQL的運維已經很熟悉了,它的穩定性沒有問題,然而MongoDB的穩定性我們沒法保證,畢竟不熟悉,因此在之前的拆分儲存技術選型中,我們沒使用過NoSQL。

3、NewSQL(如TiDB)

NewSQL技術還比較新,我們曾經想在一些不重要的資料中使用NewSQL(比如TiDB),但從穩定性和功能擴充套件性兩方面來考量後,最終沒有使用,具體原因與MongoDB類似。

4、基於MySQL的分庫分表

什麼是分庫分表?分表是將一份大的表資料拆分存放至多個結構一樣的拆分表;分庫就是將一個大的資料庫拆分成多個結構一樣的小庫。

前面介紹的三種拆分儲存技術,在我們以往的專案中都沒有使用過,而是選擇了基於MySQL的分庫分表,主要是有一個重要考量:分庫分表對於第三方依賴較少,業務邏輯靈活可控,它本身並不需要非常複雜的底層處理,也不需要重新做資料庫,只是根據不同的邏輯使用不同的SQL語句和資料來源而已。

如果使用分庫分表方式,存在三個技術通用需求需要實現。

1、SQL組合

:因為我們關聯的表名是動態的,所以我們需要根據邏輯組裝動態的SQL。

2、資料庫路由

:因為資料庫名也是動態的,所以我們需要根據不同的邏輯使用不同的資料庫。

3、執行結果合併

:有些需求需要透過多個分庫執行,再合併歸集使用。

而市面上能解決以上問題的中介軟體分為2類:Proxy模式、Client模式。

(1)Proxy模式:直接拿ShardingSphere官方文件裡的圖進行說明,我們重點看看中間Sharding-Proxy層,如下圖所示:

資料量大讀寫緩慢如何最佳化(4)「分庫分表」

以上這種設計模式,把SQL組合、資料庫路由、執行結果合併等功能全部存放在一個代理服務中,而與分庫分表相關的處理邏輯全部存放在另外的服務中,這種設計模式的優點是對業務程式碼無侵入,業務只需要關注自身的業務邏輯即可。

(2)Client模式:還是借用shardingSphere官方文件的圖來說明,如下圖所示:

資料量大讀寫緩慢如何最佳化(4)「分庫分表」

以上這種設計模式,把分庫分表相關邏輯存放在客戶端,一版客戶端的應用會引用一個jar,然後再jar中處理SQL組合、資料庫路由、執行結果合併等相關功能。

市面上,關於這兩種模式的中介軟體有如下選擇:

資料量大讀寫緩慢如何最佳化(4)「分庫分表」

看到這裡,我們已經知道市面上開源中介軟體的設計模式,那麼我們到底該選擇哪種模式呢?簡單對比下這2個模式的優缺點,你就知道答案了。

資料量大讀寫緩慢如何最佳化(4)「分庫分表」

因為看重程式碼靈活可控這個優勢,所以我們選擇了Client模式裡的Sharding-JDBC來實現分庫分表,如下圖所示:

資料量大讀寫緩慢如何最佳化(4)「分庫分表」

當然,關於拆分儲存選擇哪種技術,在實際工作中我們需要根據各自的實際情況來定。

三、分庫分表實現思路

技術選型這一大難題解決後,具體如何落地分庫分表解決方案成了我們亟待解決的問題。

在落實分庫分表解決方案時,我們需要考慮5個要點。

1、使用什麼欄位作為分片鍵?

我們先來回顧下業務場景中舉例的資料庫:

資料量大讀寫緩慢如何最佳化(4)「分庫分表」

下面我們把上表中的資料拆分成一個訂單表,表中主要資料結構如下:

資料量大讀寫緩慢如何最佳化(4)「分庫分表」

從上面表中可知,我們是使用user_id作為分片主鍵,為什麼這樣分呢,來聊聊當時的實現思路。

在選擇分片欄位之前,我們首先了解了下目前存在的一些常見業務需求:

使用者需要查詢所有訂單,訂單資料中肯定包含不同的merchant_id、order_time;

後臺需要根據城市查詢當地訂單;

後臺需要統計每個時間段的訂單趨勢;

根據這些常見業務需求,我們判斷了下優先順序,使用者操作也就是第一個需求必須優先滿足。

此時,如果我們使用user_id作為訂單分片欄位,就能保證每次使用者查詢資料時(第一個需求),在一個分庫的一個分表裡即可獲取資料。

因此,在我們的方案裡,最終還是使用user_id作為分片主鍵,這樣在分庫分表查詢時,首先會把user_id作為引數傳過來。

這裡需要特殊說明下,選擇欄位作為分片鍵時,我們一般要考慮三個因素:資料儘量均勻分佈在不同的庫或表、跨庫查詢儘可能少、這個欄位值會不會變(這點尤為重要)。

2、分片的策略是什麼?

決定使用user_id作為訂單分片欄位後,我們就要開始考慮分片的策略問題了。

目前,市面上通用的分片策略分為根據範圍分片、根據hash值分片,根據hash值及範圍混合分片這三種。

根據範圍分片:比如使用者id是自增型數字,我們把使用者id按照每100萬份分為一個庫,每10萬份分為一個表的形式進行分片,如下表所示:

資料量大讀寫緩慢如何最佳化(4)「分庫分表」

特殊說明:這裡我們只說分表,至於分庫則是把分表分組存放在一個庫即可,就不另行說明了。

根據hash值分片:指的是根據使用者id的hash值mod一個特定的數進行分片。(避免方便後續擴充套件,一版是2的幾次方)

根據hash值及範圍混合分片:先按照範圍分片,再根據hash值取模分片。比如:表名=order

#user_id%10#_

#hash(user_id)%8,即被分成了10*8=80個表。為了方便理解,我們畫個圖來說明,如圖所示:

資料量大讀寫緩慢如何最佳化(4)「分庫分表」

以上三大分片策略我們到底應該選擇哪個?我們只需要考慮一點:假設之後資料量變大了,需要我們把表分的更細,此時保證遷移的資料量儘量少即可。

因此,根據hash值分片時我們一般建議拆分成2的N次方表,比如分成8張表,資料遷移時把原來的每張表拆一半出來組成新表,這樣資料遷移量就小了。

當初的方案中,我們就是根據使用者id的hash值取模32,把資料分成32個數據庫,每個資料庫再拆分成16張表。

我們簡單算了下,假設每天訂單1000萬,每個庫日增1000萬/16=31。25萬,每個表日新增1000萬/32/16=1。95萬。而如果每天千萬訂單量,3年後每個表的資料量就是2000萬左右,也還在可控範圍內。

因此,如果業務增長特別快,且運維還扛得住,為避免以後出現擴容問題,我們建議庫分的越少越好。

3、業務程式碼如何修改?

分片策略定完以後,我們就要考慮業務程式碼如何修改了。因修改業務程式碼部分與業務強關聯,所以我們的方案並不具備參考性。

這裡分享些個人觀點。近年來,分庫分表操作愈發簡單,不過我們需要注意幾個要點:

我們已經習慣微服務了,對於特定表的分庫分表,其影響面只在該表所在的服務中,如果是一個單體架構的應用做分庫分表,那真是傷腦筋。

在網際網路架構中,我們基本不使用外來鍵約束。

隨著查詢分離的流行,後臺系統中有很多操作需要跨庫查詢,導致系統性能非常差,這時分庫分表一般會結合查詢分離一起操作:先將所有的資料在ES中索引一份,再使用ES在後臺直接查詢資料。如果訂單詳情資料量很大,還有個常見的做法,即先在ES中儲存索引欄位(作為查詢條件的欄位),再將詳情資料存在HBASE中(這個方案這裡就不展開了)。

一般來說,業務程式碼的修改不會很複雜,最麻煩的是歷史資料的遷移。

4、歷史資料的遷移?

歷史資料的遷移非常耗時,有時遷移幾天幾夜都很正常。在網際網路行業中,別說幾天幾夜了,就連停機幾分鐘業務都無法接受,這就要求我們給出一個無縫遷移的解決方案。

還記得在聊查詢分離時,討論過的解決方案嗎?我們來回顧下,如下圖所示:資料庫表資料量大讀寫緩慢如何最佳化(2)「查詢分離」

資料量大讀寫緩慢如何最佳化(4)「分庫分表」

歷史資料遷移時,我們就是採用類似的方案進行歷史資料遷移,如下圖所示:

資料量大讀寫緩慢如何最佳化(4)「分庫分表」

此資料遷移方案的基本思路:存量資料直接遷移,增量資料監聽binglog,然後透過canal通知遷移程式搬運資料,新的資料庫擁有全量資料,且校驗通過後逐步切換流量。

資料遷移解決方案詳細的步驟如下:

上線canal,透過canal觸發增量資料的遷移;

遷移資料指令碼測試通過後,將老資料遷移到新的分庫分表中;

注意遷移增量資料與遷移老資料的時間差,確保全部資料都被遷移過去,無遺漏;

第二步、第三步都執行完後,新的分庫分表中已經擁有了全量資料了,這時我們可以執行資料驗證的程式,確保所有資料都存放在新資料庫中;

到這步資料遷移就算完成了,之後就是新版本程式碼上線了,至於是灰度上還是直接上,需要根據實際情況決定,回滾方案也是一樣。

5、未來的擴容方案是什麼?

隨著業務的發展,如果原來的分片設計已經無法滿足日益增長的資料需求,我們就需要考慮擴容了,擴容方案主要依賴以下兩點:

分片策略是否可以讓新表資料的遷移源只是一箇舊表,而不是多箇舊表,這就是前面我們建議使用2的N次方分表的原因;

資料遷移:我們需要把舊分片資料遷移到新的分片上,這個方案與上面提及的歷史資料遷移一樣,就不過多贅述了;

四、分庫分表的不足

分庫分表的解決方案聊完了,以上就是業界常用的一些做法,不過此方案仍存在不足之處。

ES+Hbase做資料查詢分離的方案:前面我們說了單獨使用ES做查詢分離解決方案,這裡就不再單獨展開了。

增量資料遷移:如何保證資料的一致性及高可用性?這個問題我們在後面的文章中會單獨展開來說。(感興趣的小夥伴可以關注一下)

短時訂單量大爆發:分庫分表仍然扛不住時解決方案是什麼?這個在快取和秒殺架構文章中我們再單獨展開來說。

歡迎關注公眾號:服務端技術精選,還有更多內容等你喲