分散式系統「伸縮性」大招之——「水平&垂直切分」詳解

huorongbj發表於2019-03-08

如果第二次看到我的文章, 歡迎 訂閱我的公眾號(跨界架構師)喲~ 

本文長度為 5389字 ,建議閱讀 14 分鐘。

堅持原創,每一篇都是用心之作~

 

 

沒想到這篇文章寫了這麼長,一時半會沒消化完的話,可以收藏一下先

 

 

這是「伸縮性」章節的第四篇,先給新來的小夥伴們簡單回顧下前三篇的內容。

 

做「伸縮性」最重要的就是先做好「無狀態」,如此才可以隨心所欲的進行橫向“擴充套件”,而不用擔心在多個副本之間切換會產生錯亂。《 分散式系統關注點——「無狀態」詳解 》聊的就是這個。

 

不過,就算做好了橫向擴充套件,本質上還是一個“大程式”,只是變得「可複製」了而已。

 

如果要消滅“大程式”,那就得“切分”,做好切分必然離不開「高內聚低耦合」的核心思想。《 分散式系統關注點——「高內聚低耦合」詳解 》這篇聊的就是這個。

 

題外話 當你遇到單點單應用支撐不住使用的時候,Z哥給你的普適性建議是:先考慮“擴”,再考慮“切”。這個和寫程式碼一樣,“增加”新功能往往比在老功能上改容易。

 

“擴”的話先考慮「垂直擴」(加硬體,錢能解決的都不是問題),再考慮「水平擴」(無狀態改造+多節點部署,這是小手術)。

 

“切”的話一般就是「垂直切」(根據業務切分,這是大手術),偶爾會用到「水平切」(其實就是單個應用裡的分層,比如前後端分離)。

 

 

第三篇《 分散式系統關注點——彈性架構 》我們聊了常見的兩種「松耦合」架構模式,為的是讓應用程式的「伸縮性」更上一層樓。

 

 

以上這些呢都是應用程式層面的工作。一般情況下,在應用程式層面做做手術,再配合以快取的充分運用,就可以支撐系統發展很長時間了。特別是資料量不大,只是請求量大的「CPU密集型」場景。

 

但是,如果所處的工作場景是一個非常成熟且具有一定規模的專案,越發展到後面瓶頸總是出現在資料庫這裡。甚至會出現cpu長期高負荷、當機等現象。

 

在如此場景下,就不得不對資料庫開刀了。這次Z哥就來和你聊聊做資料庫的「伸縮性」有哪些好方法。

 

 

核心訴求

面臨資料庫需要開刀的時候,整個系統往往已經長成這個樣子了。

 

 

正如前面所說,這時候的瓶頸往往會體現在「CPU」上。

 

因為對資料庫來說,硬碟和記憶體的擴容相對容易,因為它們都可以直接用“增加”的方式進行。

 

CPU就不同了,一旦CPU飆高,最多檢查下索引有沒有做好,完了之後基本就只能幹看著。

 

所以解決這個問題的思路自然就變成了:如何將一個資料庫的CPU壓力分攤到多個CPU上去。甚至可以做到按需隨時增加。

 

那這不就是和應用程式一樣做「切分」嘛。也是分散式系統的「分治」思想體現。

 

既然是切分,本質上就和應用程式一樣,也分為「垂直切分」和「水平切分」。

 

 

垂直切分

垂直切分有時候也會被稱作「縱向切分」。

 

同應用程式一樣,它是以「業務」為維度的切分方式,在不同的資料庫伺服器上跑不同業務的資料庫,各司其職。

 

 

一般情況下,Z哥建議你 優先考慮「垂直切分」而不是「水平切分」 ,為什麼呢?你可以隨意開啟手頭專案中的SQL語句看看,我想必然存在著大量的「join」和「transaction」關鍵字,這種關聯查詢和事務操作, 本質上是一種「關係捆綁」,一旦面臨資料庫拆分之後,就沒法玩了

 

此時你只有2個選擇。

  1. 要麼將不必要的「關係捆綁」邏輯捨棄掉,這需要在業務上作出調整,去除不必要的“批次操作”業務,或者去除不必要的強一致性事務。不過你也知道,肯定有一些場景是去不完的。

  2. 要麼將「合併」,「關聯」等邏輯上浮,體現到業務邏輯層甚至是應用層的程式碼中。

 

最終,不管怎麼選擇,改動起來都是一個大工程。

 

 

為了讓這個工程儘可能的動作小一些,追求更好的價效比,需要堅持一個原則——“ 避免拆分緊密關聯的表 ”。

 

因為兩個表之間關聯越緊密,意味著對「join」和「transaction」的需求越多,所以堅持這個原則可以使得相同的模組,緊密相關的業務都落在同一個庫中,這樣它們可以繼續使用「join」和「transaction」來工作。

 

因此,我們應當優先採用「垂直切分」的方式。

 

 

做「垂直切分」思路很簡單,一般情況下, 建議是與切分後的應用程式一一對應就好,不用多也不用少

 

 

實際工作中,要做好「垂直切分」主要體現在「業務」的熟悉度上,所以這裡就不繼續展開了。

 

 

「垂直切分」的優點是:

  1. 高內聚,拆分規則清晰。相比「水平切分」資料冗餘度更低。

  2. 與應用程式是1:1的關係,方便維護和定位問題。一旦某個資料庫中發現異常資料,排查這個資料庫的關聯程式就行了。

 

但是這並不是一個「一勞永逸」的方案,因為沒人能預料到未來業務會發展的怎麼樣,所以最明顯的缺點就是: 對於訪問極其頻繁或者資料量超大的表仍然存在效能瓶頸

 

確實需要解決這個問題的話,就需要搬出「水平切分」了。

 

題外話 :不到迫不得己,儘量避免進行「水平切分」。看完接下去的內容你就知道原因了。

 

 

下面Z哥就給你好好聊聊「水平切分」,這才是本文的重點。

 

 

水平切分

想象一下,在你做了「垂直切分」之後,還是在某個資料庫中發現了一張資料量超過10億條的表。

 

這個時候要對這個表做「水平切分」,你會怎麼思考這個事情?

 

 

Z哥教給你的思路是:

  1. 先找到“最高頻“的「讀」欄位

  2. 再看這個欄位的實際使用中有什麼特點(批次查詢多還是單個查詢多,是否同時是其它表的關聯欄位等等)。

  3. 再根據這個特點選擇合適的切分方案。

 

為什麼要先找到高頻的「讀」欄位呢?

 

因為在實際的使用中,「讀」操作往往是遠大於「寫」操作的。一般進行「寫」之前都得透過「讀」來做先行校驗,然而「讀」還有自己單獨的使用場景。所以 針對更高頻的「讀」場景去考慮,產生的價值必然也更大

 

比如,現在那張10億資料量的表是一張訂單表,結構是這樣:

order (orderId long, createTime datetime, userId long)

 

下面我們先來看看有哪幾種「水平切分」的方式,完了才能明白什麼樣的場景適合哪種方式。

 

 

範圍切分

這是一種「連續式」的切分方式。

 

比如根據時間(createTime)切分的話,我們可以按年月來分,order_201901一個庫,order_201902一個庫,以此類推。

 

根據順序數(orderId)切分的話,可以100000~199999一個庫,200000~299999一個庫,以此類推。

 

這種切分法的優點是: 單個表的大小可控,擴充套件的時候無需資料遷移

 

缺點也很明顯,一般來說時間越近或者序號越大的資料越“新”,因此被訪問的頻率和機率相比“老”資料更多。會導致 壓力主要集中在新的庫中,而歷史越久的庫,越空閒

 

 

Hash切分

與「範圍切分」正好相反,這是一種「離散式」的切分方式。

 

它的優點就是解決了「範圍切分」的缺點, 新資料被分散到了各個節點中,避免了壓力集中在少數節點上

 

同樣,缺點與「範圍切分」的優點相反, 一旦進行二次擴充套件,必然會涉及到資料遷移 。因為Hash演算法是固定的,演算法一變,資料分佈就變了。

 

大多數情況下,我們的hash演算法可以透過簡單的「取模」運算來進行即可。就像下面這樣:

假如分成11個庫的話,公式就是 orderId % 。 
 %  = ,分配到db0。 %  = ,分配到db1。
....
 %  = ,分配到db0。 %  = ,分配到db1。

 

其實,在某些場景下,我們可以透過自定義id的生成(可以參考之前的文章,《 分散式系統中的必備良藥 —— 全域性唯一單據號生成 》)來做到既可以透過hash切分來打散熱點資料,又可以減少依賴全域性表來定位具體的資料。

 

比如,在orderId中加入userId的尾數,以此達到orderId和userId取模結果相等的效果。還是來舉個例子:

 

一個使用者的userId是200004,如果取一個4bit尾數的話,這裡就是4,用0100表示。

 

然後,我們透過自定義id演算法生成orderId的前60位,在後面補上0100。

 

於是,orderId % 10和 userId % 10的結果就是一樣的了。

 

當然,除了userId之外還想加入其他的因子就不好使了。也就是,可以在不增加全域性表的情況下,額外多支援1個維度。

 

提到了兩次全域性表,那麼啥是全域性表呢?



全域性表

這種方式就是將用作切分依據的分割槽Key與對應的每一條具體資料的id儲存到一個單獨的庫或者表中。例如要增加一張這樣的表:

nodeId    orderId 01        100001
 02        100002
 01        100003
 01        100004
 ...

 

如此一來,的確將大部分具體的資料分佈在了不同伺服器上,但是這張全域性表會給人一種「形散神不散」的感覺。

 

因為請求資料的時候無法直接定位需要的資料在哪臺伺服器上,所以每一次操作都要先查詢一下這張全域性表好知道具體的資料被存放在哪裡。

 

這種「中心化」的模式帶來的副作用就是瓶頸和風險轉移到了這張全域性表上。但是,勝在邏輯簡單。

 

 

好了,那麼這幾種切分方案怎麼選擇呢?

 

Z哥給你的建議是, 如果熱點資料不是特別集中的場景,建議先用「範圍切分」,否則選擇另外2種

 

選擇另外兩種的時候, 資料量越大越傾向選擇Hash切分 。因為後者在整體的可用性和效能上都比前者好,就是實現成本高一些。

 

 

「水平切分」真正做到了可以“無限擴充套件”,但是也存在相應的弊端。

1)批次查詢、分頁等需要做更多的額外工作。特別是當一個表存在多個高頻欄位用於where、order by或者group by的時候。

2)拆分規則不如「垂直切分」那麼明確。

 

所以還是多說一句“廢話”: 沒有完美的方案只有合適的方案,要結合具體的場景來選擇 。(歡迎你在留言區提出你有疑惑的場景,和Z哥來討論討論)

 

 

如何實施

當你在具體實施「水平切分」的時候可以在2個層面動刀,可以是「表」層面,也可以是「庫」層面。

 

在同一個資料庫下面分表,表名order_0 ,order_1, order_2.....。

 

它可以解決單表資料過大,但並不能解決CPU負荷的問題。所以,當CPU並沒多少壓力,只是由於表太大,導致執行SQL操作比較慢的話,可以選擇這種方式。

 

 

這個時候表名可以不變,都叫order,只是分成10個庫。那麼就是db0-user   db1-user db2-user......。

 

我們前面大篇幅都是基於這個模式在聊,就不多說了。

 

 

表+庫

也可以既分庫又分表,比如先分10個庫,然後每個庫再分10張表。

 

這其實是個二級索引的思路,透過庫來進行第一次定位,減少一定的資源消耗。

 

比如,先按年分庫,再按月分表。如此一來,如果需要獲取的資料只跨月但不跨年,我們就可以在單個庫內做聚合運算來完成,不涉及到跨庫操作。

 

 

不過,不管選擇哪種方式來進行,你還是會或多或少面臨以下兩個問題,逃不掉的。

  1. 跨庫join。

  2. 全域性聚合或者排序操作。

 

解決第一個問題最佳方式還是需要改變你的程式設計思維 。儘量將一些邏輯、關係、約束等體現在應用程式的程式碼中,避免因為方便而在SQL中做這些事情。

 

畢竟程式碼是可以寫成“無狀態”的,可以隨時做擴充套件,但是SQL是跟著資料走的,而資料就是“狀態”,天然不利於擴充套件。

 

當然了,退而求其次,你也可以冗餘大量的全域性表來應對。只是如此一來,對「資料一致性」工作是個很大的考驗,另外,對儲存資源也是很大的開銷。

 

第二個問題的解決方案就是需要將原本的一次聚合或者一次排序變成兩次操作 。其中的遍歷多個節點可以以「並行」的方式進行。

 

 

 

那麼資料切分完之後程式如何來使用呢?這又可以分為兩種模式,「程式內」和「程式外」。

 

 

「程式內」的話,可以在封裝好的DAL訪問框架中做,也可以在ORM框架中做,還可以在資料庫驅動中做。這個模式比較知名的解決方案如阿里的tddl。

 

 

「程式外」的話,就是代理模式,這個模式比較知名的解決方案是mycat、cobar、atlas等等,相對多一些,因為這種模式對應用程式是「低侵入」的,使用起來像“一個資料庫”。但是由於多了一道網路通訊,效能上會多一些損耗。

 

 

老規矩,下面再分享一些最佳實踐。

 

 

最佳實踐

首先分享兩個可以不停機做資料切分的小竅門 。我們以實施hash法做水平切分的例子來看一下。

 

第一次做切分的時候,你可以以「主-從」的形式將新增的節點作為原始節點的副本,進行全量實時同步。

 

 

然後在這個基礎上刪除不屬於它的資料。(當然了,不刪也沒啥問題,就是多佔用一些空間)

 

 

這樣就可以不用停機了。

 

 

第二,隨著時間的推移,如果後續支撐不住了,需要二次切分的話,我們可以選擇用2的倍數來擴充套件。

 

如此一來,資料的遷移變得很簡單,只需要做區域性的遷移,和第一次做切分的思路是一樣的。

 

 

 

當然了,如果選擇的切分方式是「範圍切分」的話,就沒有二次切分時的困擾,資料自然跑到最新的節點上去了。比如我們按年月分表的話。2019年3月的資料自然就落到了xxxx_201903的表中。

 

 

到這裡,Z哥還是想特別強調的是, 能不切分儘量不要切分,可以先使用「讀寫分離」之類的方案先來應對面臨的問題 。 

 

如果實在要進行切分的話,務必先「垂直切分」,再考慮「水平切分」

 

一般來說,以這樣的順序來考慮,價效比更好。

 

 

總結

好了,我們總結一下。

 

這次呢,Z哥先向你介紹了做資料庫切分的兩種思路。兩種思路通俗理解就是:「垂直拆分」等於“列”變“行”不變,「水平拆分」等於“行”變“列”不變。

 

然後著重聊了下「水平切分」的3種實現方式和具體實施的思路。

 

最後分享了一些實踐中的經驗給你。

 

希望對你有所啟發。

 

 

相關文章:

 


 

作者: Zachary

出處: https://www.cnblogs.com/Zachary-Fan/p/databasesegmentation.html

 

如果你喜歡這篇文章,可以點一下左下角的「  大拇指  」。

 

這樣可以給我一點反饋。: )

 

謝謝你的舉手之勞。

 

▶關於作者:張帆(Zachary,個人微訊號:Zachary-ZF)。堅持用心打磨每一篇高質量原創。

定期發表原創內容:架構設計丨分散式系統丨產品丨運營丨一些思考。

 

如果你是初級程式設計師,想提升但不知道如何下手。又或者做程式設計師多年,陷入了一些瓶頸想拓寬一下視野。歡迎關注我的公眾號「   跨界架構師   」,回覆「   技術   」,送你一份我長期收集和整理的思維導圖。  

如果你是運營,面對不斷變化的市場束手無策。又或者想了解主流的運營策略,以豐富自己的“倉庫”。歡迎關注我的公眾號「   跨界架構師   」,回覆「   運營   」,送你一份我長期收集和整理的思維導圖。



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

相關文章