從原則、方案、策略及難點闡述分庫分表

 

轉自:http://www.ywnds.com/?p=7239算法

 

1、分庫分表原則

關係型數據庫自己比較容易成爲系統性能瓶頸,單機存儲容量、鏈接數、處理能力等都頗有限,數據庫自己的「有狀態性」致使了它並不像Web和應用服務器那麼容易擴展。在互聯網行業海量數據和高併發訪問的考驗下,聰明的技術人員提出了分庫分表技術(有些地方也稱爲Sharding、分片)。同時,流行的分佈式系統中間件(例如MongoDB、ElasticSearch等)均自身友好支持Sharding,其原理和思想都是大同小異的。sql

目前針對海量數據的優化,其分庫分表是MySQL永遠的話題,通常狀況下認爲MySQL是個簡單的數據庫,在數據量大到必定程度以後處理查詢的效率下降,若是須要繼續保持高性能運轉的話,必須分庫或者分表了。關於數據量達到多少大是個極限這個事兒,本文先不討論,研究源碼的同窗已經證明MySQL或者Innodb內部的鎖粒度太大的問題大大限制了MySQL提供QPS的能力或者處理大規模數據的能力。在這點上,通常的使用者只好坐等官方不斷推出的優化版本了。shell

在通常運維的角度來看,咱們什麼狀況下須要考慮分庫分表?數據庫

首先說明,這裏所說的分庫分表是指把數據庫數據的物理拆分到多個實例或者多臺機器上去,而不是相似分區表的原地切分。安全

原則零:能不分就不分服務器

是的,MySQL 是關係數據庫,數據庫表之間的關係從必定的角度上映射了業務邏輯。任何分庫分表的行爲都會在某種程度上提高業務邏輯的複雜度,數據庫除了承載數據的存儲和訪問外,協助業務更好的實現需求和邏輯也是其重要工做之一。分庫分表會帶來數據的合併,查詢或者更新條件的分離,事務的分離等等多種後果,業務實現的複雜程度每每會翻倍或者指數級上升。因此,在分庫分表以前,不要爲分而分,去作其餘力所能及的事情吧,例如升級硬件,升級,升級網絡,升級數據庫版本,讀寫分離,負載均衡等等。全部分庫分表的前提是,這些你已經盡力了。網絡

原則一:數據量太大,正常的運維影響正常業務訪問架構

這裏說的運維,例如:併發

1)對數據庫的備份。若是單表或者單個實例太大,在作備份的時候須要大量的磁盤IO或者網絡IO資源。例如1T的數據,網絡傳輸佔用50MB的時候,須要20000秒才能傳輸完畢,在此整個過程當中的維護風險都是高於平時的。咱們在Qunar的作法是給全部的數據庫機器添加第二塊網卡,用來作備份,或者SST,Group Communication等等各類內部的數據傳輸。1T的數據的備份,也會佔用大量的磁盤IO,若是是SSD還好,固然這裏忽略某些廠商的產品在集中IO的時候會出一些BUG的問題。若是是普通的物理磁盤,則在不限流的狀況下去執行xtrabackup,該實例基本不可用。負載均衡

2)對數據表的修改。若是某個表過大,對此表作DDL的時候,MySQL會鎖住全表,這個時間可能很長,在這段時間業務不能訪問此表,影響甚大。解決的辦法有相似騰訊遊戲DBA本身改造的能夠在線秒改表,不過他們目前也只是能添加字段而已,對別的DDL仍是無效;或者使用pt-online-schema-change,固然在使用過程當中,它須要創建觸發器和影子表,同時也須要很長很長的時間,在此操做過程當中的全部時間,均可以看作是風險時間。把數據表切分,總量減少,有助於改善這種風險。

3)整個表熱點,數據訪問和更新頻繁,常常有鎖等待,你又沒有能力去修改源碼,下降鎖的粒度,那麼只會把其中的數據物理拆開,用空間換時間,變相下降訪問壓力。

原則二:表設計不合理,須要對某些字段垂直拆分

這裏舉一個例子,若是你有一個用戶表,在最初設計的時候多是這樣:

通常的users表會有不少字段,我就不列舉了。如上所示,在一個簡單的應用中,這種設計是很常見的。可是:

設想狀況一:你的業務中彩了,用戶數從100w飆升到10個億。你爲了統計活躍用戶,在每一個人登陸的時候都會記錄一下他的最近登陸時間。而且的用戶活躍得很,不斷的去更新這個login_time,搞的你的這個表不斷的被update,壓力很是大。那麼,在這個時候,只要考慮對它進行拆分,站在業務的角度,最好的辦法是先把last_login_time拆分出去,咱們叫它 user_time。這樣作,業務的代碼只有在用到這個字段的時候修改一下就好了。若是你不這麼作,直接把users表水平切分了,那麼,全部訪問users表的地方,都要修改。或許你會說,我有proxy,可以動態merge數據。到目前爲止我還從沒看到誰家的proxy不影響性能的。

設想狀況二:personal_info這個字段原本沒啥用,你就是讓用戶註冊的時候填一些我的愛好而已,基本不查詢。一開始的時候有它沒它無所謂。可是到後來發現兩個問題,一,這個字段佔用了大量的空間,由於是text嘛,有不少人喜歡長篇大論地介紹本身。更糟糕的是二,不知道哪天哪一個產品經理心血來潮,說容許我的信息公開吧,以方便讓你們更好的相互瞭解。那麼在全部人獵奇窺私心理的影響下,對此字段的訪問大幅度增長。數據庫壓力瞬間抗不住了,這個時候,只好考慮對這個表的垂直拆分了。

原則三:某些數據表出現了無窮增加

例子很好舉,各類的評論,消息,日誌記錄。這個增加不是跟人口成比例的,而是不可控的,例如微博的feed的廣播,我發一條消息,會擴散給不少不少人。雖然主體可能只存一份,但不排除一些索引或者路由有這種存儲需求。這個時候,增長存儲,提高機器配置已經蒼白無力了,水平切分是最佳實踐。拆分的標準不少,按用戶的,按時間的,按用途的,不在一一舉例。

原則四:安全性和可用性的考慮

這個很容易理解,雞蛋不要放在一個籃子裏,我不但願個人數據庫出問題,但我但願在出問題的時候不要影響到100%的用戶,這個影響的比例越少越好,那麼,水平切分能夠解決這個問題,把用戶,庫存,訂單等等原本同統一的資源切分掉,每一個小的數據庫實例承擔一小部分業務,這樣總體的可用性就會提高。這對Qunar這樣的業務仍是比較合適的,人與人之間,某些庫存與庫存之間,關聯不太大,能夠作一些這樣的切分。

原則五:業務耦合性考慮

這個跟上面有點相似,主要是站在業務的層面上,咱們的火車票業務和烤羊腿業務是徹底無關的業務,雖然每一個業務的數據量可能不太大,放在一個MySQL實例中徹底沒問題,可是極可能烤羊腿業務的DBA 或者開發人員水平不好,動不動給你出一些幺蛾子,直接把數據庫搞掛。這個時候,火車票業務的人員雖然技術很優秀,工做也很努力,照樣被老闆打屁股。解決的辦法很簡單:惹不起,躲得起。

2、分庫分表方案

  • 垂直拆分

垂直拆分常見有垂直分庫和垂直分表兩種。垂直分表在平常開發和設計中比較常見,通俗的說法叫作「大表拆小表」,拆分是基於關係型數據庫中的「列」(字段)進行的。一般狀況,某個表中的字段比較多,能夠新創建一張「擴展表」,將不常用或者長度較大的字段拆分出去放到「擴展表」中,以下圖所示:

從原則、方案、策略及難點闡述分庫分表

在字段不少的狀況下,拆分開確實更便於開發和維護(筆者曾見過某個遺留系統中,一個大表中包含100多列的)。某種意義上也能避免「跨頁」的問題(MySQL、MSSQL底層都是經過「數據頁」來存儲的,「跨頁」問題可能會形成額外的性能開銷,這裏不展開,感興趣的朋友能夠自行查閱相關資料進行研究)。

拆分字段的操做建議在數據庫設計階段就作好。若是是在發展過程當中拆分,則須要改寫之前的查詢語句,會額外帶來必定的成本和風險,建議謹慎。

垂直分庫是根據數據庫裏面的數據表的相關性進行拆分,好比:一個數據庫裏面既存在用戶數據,又存在訂單數據,那麼垂直拆分能夠把用戶數據放到用戶庫、把訂單數據放到訂單庫。垂直分表是對數據表進行垂直拆分的一種方式,常見的是把一個多字段的大表按經常使用字段和很是用字段進行拆分,每一個表裏面的數據記錄數通常狀況下是相同的,只是字段不同,使用主鍵關聯。

另外,在「微服務」盛行的今天已經很是普及了,按照業務模塊來劃分出不一樣的數據庫,也是一種垂直拆分。而不是像早期同樣將全部的數據表都放到同一個數據庫中。以下圖:

從原則、方案、策略及難點闡述分庫分表

垂直拆分優勢:

  • 可使得行數據變小,一個數據塊 (Block) 就能存放更多的數據,在查詢時就會減小 I/O 次數 (每次查詢時讀取的 Block 就少)。
  • 能夠達到最大化利用 Cache 的目的,具體在垂直拆分的時候能夠將不常變的字段放一塊兒,將常常改變的放一塊兒。
  • 數據維護簡單。

垂直拆分缺點:

  • 主鍵出現冗餘,須要管理冗餘列。
  • 會引發錶鏈接 JOIN 操做(增長 CPU 開銷)能夠經過在業務服務器上進行 join 來減小數據庫壓力。
  • 依然存在單表數據量過大的問題(須要水平拆分)。
  • 事務處理複雜。

垂直拆分小結:

系統層面的「服務化」拆分操做,可以解決業務系統層面的耦合和性能瓶頸,有利於系統的擴展維護。而數據庫層面的拆分,道理也是相通的。與服務的「治理」和「降級」機制相似,咱們也能對不一樣業務類型的數據進行「分級」管理、維護、監控、擴展等。

衆所周知,數據庫每每最容易成爲應用系統的瓶頸,而數據庫自己屬於「有狀態」的,相對於Web和應用服務器來說,是比較難實現「橫向擴展」的。數據庫的鏈接資源比較寶貴且單機處理能力也有限,在高併發場景下,垂直分庫必定程度上可以突破IO、鏈接數及單機硬件資源的瓶頸,是大型分佈式系統中優化數據庫架構的重要手段。

而後,不少人並無從根本上搞清楚爲何要拆分,也沒有掌握拆分的原則和技巧,只是一味的模仿大廠的作法。致使拆分後遇到不少問題(例如:跨庫join,分佈式事務等)。

  • 水平拆分

水平拆分是經過某種策略將數據分片來存儲,分爲庫內分表和分庫分表兩部分,每片數據會分散到不一樣的MySQL表或庫,達到分佈式的效果,可以支持很是大的數據量。

庫內分表,僅僅是單純的解決了單一表數據過大的問題,因爲沒有把表的數據分佈到不一樣的機器上,所以對於減輕 MySQL 服務器的壓力來講,並無太大的做用,你們仍是競爭同一個物理機上的 IO、CPU、網絡,這個就要經過分庫分表來解決。

最多見的方式就是經過主鍵或者時間等字段進行Hash和取模後拆分。以下圖所示:

從原則、方案、策略及難點闡述分庫分表

當下分表有靜態分表和動態分表兩種:

靜態分表:事先估算出表能達到的量,而後根據每個表須要存多少數據直接算出須要建立表的數量。如:1億數據每個表100W條數據那就要建100張表,而後經過必定的hash算法計算每一條數據存放在那張表。其實就有點像是使用partition table同樣。靜態分表有一個斃命就是當分的那麼多表還不知足時,須要再擴展難度和成本就會很高。

動態分表:一樣也是對大數據量的表進行拆分,他能夠避免靜態分錶帶來的後遺症。固然也須要在設計上多一些東西(這每每是咱們能接受的)。

某種意義上來說,有些系統中使用的「冷熱數據分離」(將一些使用較少的歷史數據遷移到其餘的數據庫中。而在業務功能上,一般默認只提供熱點數據的查詢),也是相似的實踐。在高併發和海量數據的場景下,分庫分表可以有效緩解單機和單庫的性能瓶頸和壓力,突破IO、鏈接數、硬件資源的瓶頸。固然,投入的硬件成本也會更高。同時,這也會帶來一些複雜的技術問題和挑戰(例如:跨分片的複雜查詢,跨分片事務等)。

水平拆分優勢:

  • 不存在單庫大數據和高併發的性能瓶頸。
  • 應用端改造較少。
  • 提升了系統的穩定性和負載能力。

水平拆分缺點:

  • 分片事務一致性難以解決。
  • 跨節點 Join 性能差,邏輯複雜。
  • 數據屢次擴展難度跟維護量極大。

3、分庫分表難點

垂直分庫帶來的問題和解決思路:

  • 跨庫join的問題

在拆分以前,系統中不少列表和詳情頁所需的數據是能夠經過sql join來完成的。而拆分後,數據庫多是分佈式在不一樣實例和不一樣的主機上,join將變得很是麻煩。並且基於架構規範,性能,安全性等方面考慮,通常是禁止跨庫join的。那該怎麼辦呢?首先要考慮下垂直分庫的設計問題,若是能夠調整,那就優先調整。若是沒法調整的狀況,下面筆者將結合以往的實際經驗,總結幾種常見的解決思路,並分析其適用場景。

跨庫Join的幾種解決思路

全局表

所謂全局表,就是有可能系統中全部模塊均可能會依賴到的一些表。比較相似咱們理解的「數據字典」。爲了不跨庫join查詢,咱們能夠將這類表在其餘每一個數據庫中均保存一份。同時,這類數據一般也不多發生修改(甚至幾乎不會),因此也不用太擔憂「一致性」問題。

字段冗餘

這是一種典型的反範式設計,在互聯網行業中比較常見,一般是爲了性能來避免join查詢。

舉個電商業務中很簡單的場景:「訂單表」中保存「賣家Id」的同時,將賣家的「Name」字段也冗餘,這樣查詢訂單詳情的時候就不須要再去查詢「賣家用戶表」。

字段冗餘能帶來便利,是一種「空間換時間」的體現。但其適用場景也比較有限,比較適合依賴字段較少的狀況。最複雜的仍是數據一致性問題,這點很難保證,能夠藉助數據庫中的觸發器或者在業務代碼層面去保證。固然,也須要結合實際業務場景來看一致性的要求。就像上面例子,若是賣家修改了Name以後,是否須要在訂單信息中同步更新呢?

數據同步

定時A庫中的tab_a表和B庫中tbl_b有關聯,能夠定時將指定的表作同步。固然,同步原本會對數據庫帶來必定的影響,須要性能影響和數據時效性中取得一個平衡。這樣來避免複雜的跨庫查詢。筆者曾經在項目中是經過ETL工具來實施的。

系統層組裝

在系統層面,經過調用不一樣模塊的組件或者服務,獲取到數據並進行字段拼裝。提及來很容易,但實踐起來可真沒有這麼簡單,尤爲是數據庫設計上存在問題但又沒法輕易調整的時候。具體狀況一般會比較複雜。

  • 跨庫事務(分佈式事務)問題

按業務拆分數據庫以後,不可避免的就是「分佈式事務」的問題。想要了解分佈式事務,就須要瞭解「XA接口」和「兩階段提交」。值得提到的是,MySQL5.5x和5.6x中的xa支持是存在問題的,會致使主從數據不一致。直到5.7x版本中才獲得修復。Java應用程序能夠採用Atomikos框架來實現XA事務(J2EE中JTA)。感興趣的讀者能夠自行參考《分佈式事務一致性解決方案》,連接地址:http://www.infoq.com/cn/articles/solution-of-distributed-system-transaction-consistency

4、常見分片規則和策略

  • 分佈式全局惟一ID

在不少中小項目中,咱們每每直接使用數據庫自增特性來生成主鍵ID,這樣確實比較簡單。而在分庫分表的環境中,數據分佈在不一樣的分片上,不能再借助數據庫自增加特性直接生成,不然會形成不一樣分片上的數據表主鍵會重複。簡單介紹下使用和了解過的幾種ID生成算法。

1. Twitter的Snowflake(又名「雪花算法」)

2. UUID/GUID(通常應用程序和數據庫均支持)

3. MongoDB ObjectID(相似UUID的方式)

4. Ticket Server(數據庫生存方式,Flickr採用的就是這種方式)

其中,Twitter的Snowflake算法是近幾年在分佈式系統項目中使用最多的,未發現重複或併發的問題。該算法生成的是64位惟一Id(由41位的timestamp+10位自定義的機器碼+13位累加計數器組成)。這裏不作過多介紹,感興趣的讀者可自行查閱相關資料。

  • 分片字段該如何選擇

在開始分片以前,咱們首先要肯定分片字段(也可稱爲「片鍵」)。不少常見的例子和場景中是採用ID或者時間字段進行拆分。這也並不絕對的,個人建議是結合實際業務,經過對系統中執行的sql語句進行統計分析,選擇出須要分片的那個表中最頻繁被使用,或者最重要的字段來做爲分片字段。

常見的分片策略有隨機分片和連續分片這兩種,以下圖所示:

從原則、方案、策略及難點闡述分庫分表

當須要使用分片字段進行範圍查找時,連續分片能夠快速定位分片進行高效查詢,大多數狀況下能夠有效避免跨分片查詢的問題。後期若是想對整個分片集羣擴容時,只須要添加節點便可,無需對其餘分片的數據進行遷移。可是,連續分片也有可能存在數據熱點的問題,就像圖中按時間字段分片的例子,有些節點可能會被頻繁查詢壓力較大,熱數據節點就成爲了整個集羣的瓶頸。而有些節點可能存的是歷史數據,不多須要被查詢到。

隨機分片其實並非隨機的,也遵循必定規則。一般,咱們會採用Hash取模的方式進行分片拆分,因此有些時候也被稱爲離散分片。隨機分片的數據相對比較均勻,不容易出現熱點和併發訪問的瓶頸。可是,後期分片集羣擴容起來須要遷移舊的數據。使用一致性Hash算法可以很大程度的避免這個問題,因此不少中間件的分片集羣都會採用一致性Hash算法。離散分片也很容易面臨跨分片查詢的複雜問題。

  • 數據遷移,容量規劃,擴容等問題

不多有項目會在初期就開始考慮分片設計的,通常都是在業務高速發展面臨性能和存儲的瓶頸時纔會提早準備。所以,不可避免的就須要考慮歷史數據遷移的問題。通常作法就是經過程序先讀出歷史數據,而後按照指定的分片規則再將數據寫入到各個分片節點中。

此外,咱們須要根據當前的數據量和QPS等進行容量規劃,綜合成本因素,推算出大概須要多少分片(通常建議單個分片上的單表數據量不要超過1000W)。

若是是採用隨機分片,則須要考慮後期的擴容問題,相對會比較麻煩。若是是採用的範圍分片,只須要添加節點就能夠自動擴容。

5、跨分片技術問題

  • 跨分片的排序分頁

通常來說,分頁時須要按照指定字段進行排序。當排序字段就是分片字段的時候,咱們經過分片規則能夠比較容易定位到指定的分片,而當排序字段非分片字段的時候,狀況就會變得比較複雜了。爲了最終結果的準確性,咱們須要在不一樣的分片節點中將數據進行排序並返回,並將不一樣分片返回的結果集進行彙總和再次排序,最後再返回給用戶。以下圖所示:

從原則、方案、策略及難點闡述分庫分表

上面圖中所描述的只是最簡單的一種狀況(取第一頁數據),看起來對性能的影響並不大。可是,若是想取出第10頁數據,狀況又將變得複雜不少,以下圖所示:

從原則、方案、策略及難點闡述分庫分表

有些讀者可能並不太理解,爲何不能像獲取第一頁數據那樣簡單處理(排序取出前10條再合併、排序)。其實並不難理解,由於各分片節點中的數據多是隨機的,爲了排序的準確性,必須把全部分片節點的前N頁數據都排序好後作合併,最後再進行總體的排序。很顯然,這樣的操做是比較消耗資源的,用戶越日後翻頁,系統性能將會越差。

  • 跨分片的函數處理

在使用Max、Min、Sum、Count之類的函數進行統計和計算的時候,須要先在每一個分片數據源上執行相應的函數處理,而後再將各個結果集進行二次處理,最終再將處理結果返回。以下圖所示:

從原則、方案、策略及難點闡述分庫分表

<摘之>

周彥偉:http://mp.weixin.qq.com/s/BI2P45pnzUceCSQWU961zA

丁浪:http://www.infoq.com/cn/articles/key-steps-and-likely-problems-of-split-table

相關文章
相關標籤/搜索