分庫分錶帶來的問題

一.分庫分錶帶來的問題

分庫分表能有效的環節單機和單庫帶來的性能瓶頸和壓力,突破網絡IO、硬件資源、連接數的瓶頸,同時也帶來了一些問題。下面將描述這些技術挑戰以及對應的解決思路。

1、事務一致性問題

分佈式事務

當更新內容同時分佈在不同庫中,不可避免會帶來跨庫事務問題。跨分片事務也是分佈式事務,沒有簡單的方案,一般可使用"XA協議"和"兩階段提交"處理。

分佈式事務能最大限度保證了數據庫操作的原子性。但在提交事務時需要協調多個節點,推後了提交事務的時間點,延長了事務的執行時間。導致事務在訪問共享資源時發生衝突或死鎖的概率增高。隨着數據庫節點的增多,這種趨勢會越來越嚴重,從而成爲系統在數據庫層面上水平擴展的枷鎖。

最終一致性

對於那些性能要求很高,但對一致性要求不高的系統,往往不苛求系統的實時一致性,只要在允許的時間段內達到最終一致性即可,可採用事務補償的方式。與事務在執行中發生錯誤後立即回滾的方式不同,事務補償是一種事後檢查補救的措施,一些常見的實現方法有:對數據進行對賬檢查,基於日誌進行對比,定期同標準數據來源進行同步等等。事務補償還要結合業務系統來考慮。

2、跨節點關聯查詢 join 問題

切分之前,系統中很多列表和詳情頁所需的數據可以通過sql join來完成。而切分之後,數據可能分佈在不同的節點上,此時join帶來的問題就比較麻煩了,考慮到性能,儘量避免使用join查詢。

解決這個問題的一些方法:

1)全局表

全局表,也可看做是"數據字典表",就是系統中所有模塊都可能依賴的一些表,爲了避免跨庫join查詢,可以將這類表在每個數據庫中都保存一份。這些數據通常很少會進行修改,所以也不擔心一致性的問題。

2)字段冗餘

一種典型的反範式設計,利用空間換時間,爲了性能而避免join查詢。例如:訂單表保存userId時候,也將userName冗餘保存一份,這樣查詢訂單詳情時就不需要再去查詢"買家user表"了。

但這種方法適用場景也有限,比較適用於依賴字段比較少的情況。而冗餘字段的數據一致性也較難保證,就像上面訂單表的例子,買家修改了userName後,是否需要在歷史訂單中同步更新呢?這也要結合實際業務場景進行考慮。

3)數據組裝

在系統層面,分兩次查詢,第一次查詢的結果集中找出關聯數據id,然後根據id發起第二次請求得到關聯數據。最後將獲得到的數據進行字段拼裝。

4)ER分片

關係型數據庫中,如果可以先確定表之間的關聯關係,並將那些存在關聯關係的表記錄存放在同一個分片上,那麼就能較好的避免跨分片join問題。在1:1或1:n的情況下,通常按照主表的ID主鍵切分。如下圖所示:

這樣一來,Data Node1上面的order訂單表與orderdetail訂單詳情表就可以通過orderId進行局部的關聯查詢了,Data Node2上也一樣。

3、跨節點分頁、排序、函數問題

跨節點多庫進行查詢時,會出現limit分頁、order by排序等問題。分頁需要按照指定字段進行排序,當排序字段就是分片字段時,通過分片規則就比較容易定位到指定的分片;當排序字段非分片字段時,就變得比較複雜了。需要先在不同的分片節點中將數據進行排序並返回,然後將不同分片返回的結果集進行彙總和再次排序,最終返回給用戶。如圖所示:

上圖中只是取第一頁的數據,對性能影響還不是很大。但是如果取得頁數很大,情況則變得複雜很多,因爲各分片節點中的數據可能是隨機的,爲了排序的準確性,需要將所有節點的前N頁數據都排序好做合併,最後再進行整體的排序,這樣的操作時很耗費CPU和內存資源的,所以頁數越大,系統的性能也會越差。

在使用Max、Min、Sum、Count之類的函數進行計算的時候,也需要先在每個分片上執行相應的函數,然後將各個分片的結果集進行彙總和再次計算,最終將結果返回。如圖所示:

4、全局主鍵避重問題

在分庫分表環境中,由於表中數據同時存在不同數據庫中,主鍵值平時使用的自增長將無用武之地,某個分區數據庫自生成的ID無法保證全局唯一。因此需要單獨設計全局主鍵,以避免跨庫主鍵重複問題。有一些常見的主鍵生成策略:

1)UUID

UUID標準形式包含32個16進制數字,分爲5段,形式爲8-4-4-4-12的36個字符,例如:550e8400-e29b-41d4-a716-446655440000

UUID是主鍵是最簡單的方案,本地生成,性能高,沒有網絡耗時。但缺點也很明顯,由於UUID非常長,會佔用大量的存儲空間;另外,作爲主鍵建立索引和基於索引進行查詢時都會存在性能問題,在InnoDB下,UUID的無序性會引起數據位置頻繁變動,導致分頁。

2)結合數據庫維護主鍵ID表

在數據庫中建立 sequence 表:

CREATE TABLE `sequence` ( `id` bigint(20) unsigned NOT NULL auto_increment, `stub` char(1) NOT NULL default '', PRIMARY KEY (`id`), UNIQUE KEY `stub` (`stub`) ) ENGINE=MyISAM;

stub字段設置爲唯一索引,同一stub值在sequence表中只有一條記錄,可以同時爲多張表生成全局ID。sequence表的內容,如下所示:

+-------------------+------+ | id | stub | +-------------------+------+ | 72157623227190423 | a | +-------------------+------+

使用 MyISAM 存儲引擎而不是 InnoDB,以獲取更高的性能。MyISAM使用的是表級別的鎖,對錶的讀寫是串行的,所以不用擔心在併發時兩次讀取同一個ID值。

當需要全局唯一的64位ID時,執行:

REPLACE INTO sequence (stub) VALUES ('a'); SELECT LAST_INSERT_ID();

這兩條語句是Connection級別的,select last_insert_id() 必須與 replace into 在同一數據庫連接下才能得到剛剛插入的新ID。

使用replace into代替insert into好處是避免了錶行數過大,不需要另外定期清理。

此方案較爲簡單,但缺點也明顯:存在單點問題,強依賴DB,當DB異常時,整個系統都不可用。配置主從可以增加可用性,但當主庫掛了,主從切換時,數據一致性在特殊情況下難以保證。另外性能瓶頸限制在單臺MySQL的讀寫性能。

flickr團隊使用的一種主鍵生成策略,與上面的sequence表方案類似,但更好的解決了單點和性能瓶頸的問題。

這一方案的整體思想是:建立2個以上的全局ID生成的服務器,每個服務器上只部署一個數據庫,每個庫有一張sequence表用於記錄當前全局ID。表中ID增長的步長是庫的數量,起始值依次錯開,這樣能將ID的生成散列到各個數據庫上。如下圖所示:

由兩個數據庫服務器生成ID,設置不同的auto_increment值。第一臺sequence的起始值爲1,每次步長增長2,另一臺的sequence起始值爲2,每次步長增長也是2。結果第一臺生成的ID都是奇數(1, 3, 5, 7 ...),第二臺生成的ID都是偶數(2, 4, 6, 8 ...)。

這種方案將生成ID的壓力均勻分佈在兩臺機器上。同時提供了系統容錯,第一臺出現了錯誤,可以自動切換到第二臺機器上獲取ID。但有以下幾個缺點:系統添加機器,水平擴展時較複雜;每次獲取ID都要讀寫一次DB,DB的壓力還是很大,只能靠堆機器來提升性能。

可以基於flickr的方案繼續優化,使用批量的方式降低數據庫的寫壓力,每次獲取一段區間的ID號段,用完之後再去數據庫獲取,可以大大減輕數據庫的壓力。如下圖所示:

還是使用兩臺DB保證可用性,數據庫中只存儲當前的最大ID。ID生成服務每次批量拉取6個ID,先將max_id修改爲5,當應用訪問ID生成服務時,就不需要訪問數據庫,從號段緩存中依次派發0~5的ID。當這些ID發完後,再將max_id修改爲11,下次就能派發6~11的ID。於是,數據庫的壓力降低爲原來的1/6。

3)Snowflake分佈式自增ID算法

Twitter的snowflake算法解決了分佈式系統生成全局ID的需求,生成64位的Long型數字,組成部分:

  • 第一位未使用
  • 接下來41位是毫秒級時間,41位的長度可以表示69年的時間
  • 5位datacenterId,5位workerId。10位的長度最多支持部署1024個節點
  • 最後12位是毫秒內的計數,12位的計數順序號支持每個節點每毫秒產生4096個ID序列

這樣的好處是:毫秒數在高位,生成的ID整體上按時間趨勢遞增;不依賴第三方系統,穩定性和效率較高,理論上QPS約爲409.6w/s(1000*2^12),並且整個分佈式系統內不會產生ID碰撞;可根據自身業務靈活分配bit位。

不足就在於:強依賴機器時鐘,如果時鐘回撥,則可能導致生成ID重複。

綜上

結合數據庫和snowflake的唯一ID方案,可以參考業界較爲成熟的解法:Leaf——美團點評分佈式ID生成系統,並考慮到了高可用、容災、分佈式下時鐘等問題。

5、數據遷移、擴容問題

當業務高速發展,面臨性能和存儲的瓶頸時,纔會考慮分片設計,此時就不可避免的需要考慮歷史數據遷移的問題。一般做法是先讀出歷史數據,然後按指定的分片規則再將數據寫入到各個分片節點中。此外還需要根據當前的數據量和QPS,以及業務發展的速度,進行容量規劃,推算出大概需要多少分片(一般建議單個分片上的單表數據量不超過1000W)

如果採用數值範圍分片,只需要添加節點就可以進行擴容了,不需要對分片數據遷移。如果採用的是數值取模分片,則考慮後期的擴容問題就相對比較麻煩。

二. 什麼時候考慮切分

下面講述一下什麼時候需要考慮做數據切分。

1、能不切分儘量不要切分

並不是所有表都需要進行切分,主要還是看數據的增長速度。切分後會在某種程度上提升業務的複雜度,數據庫除了承載數據的存儲和查詢外,協助業務更好的實現需求也是其重要工作之一。

不到萬不得已不用輕易使用分庫分表這個大招,避免"過度設計"和"過早優化"。分庫分表之前,不要爲分而分,先盡力去做力所能及的事情,例如:升級硬件、升級網絡、讀寫分離、索引優化等等。當數據量達到單表的瓶頸時候,再考慮分庫分表。

2、數據量過大,正常運維影響業務訪問

這裏說的運維,指:

1)對數據庫備份,如果單表太大,備份時需要大量的磁盤IO和網絡IO。例如1T的數據,網絡傳輸佔50MB時候,需要20000秒才能傳輸完畢,整個過程的風險都是比較高的

2)對一個很大的表進行DDL修改時,MySQL會鎖住全表,這個時間會很長,這段時間業務不能訪問此表,影響很大。如果使用pt-online-schema-change,使用過程中會創建觸發器和影子表,也需要很長的時間。在此操作過程中,都算爲風險時間。將數據表拆分,總量減少,有助於降低這個風險。

3)大表會經常訪問與更新,就更有可能出現鎖等待。將數據切分,用空間換時間,變相降低訪問壓力

3、隨着業務發展,需要對某些字段垂直拆分

舉個例子,假如項目一開始設計的用戶表如下:

id bigint #用戶的ID name varchar #用戶的名字 last_login_time datetime #最近登錄時間 personal_info text #私人信息 ..... #其他信息字段

在項目初始階段,這種設計是滿足簡單的業務需求的,也方便快速迭代開發。而當業務快速發展時,用戶量從10w激增到10億,用戶非常的活躍,每次登錄會更新 last_login_name 字段,使得 user 表被不斷update,壓力很大。而其他字段:id, name, personal_info 是不變的或很少更新的,此時在業務角度,就要將 last_login_time 拆分出去,新建一個 user_time 表。

personal_info 屬性是更新和查詢頻率較低的,並且text字段佔據了太多的空間。這時候,就要對此垂直拆分出 user_ext 表了。

4、數據量快速增長

隨着業務的快速發展,單表中的數據量會持續增長,當性能接近瓶頸時,就需要考慮水平切分,做分庫分表了。此時一定要選擇合適的切分規則,提前預估好數據容量

5、安全性和可用性

雞蛋不要放在一個籃子裏。在業務層面上垂直切分,將不相關的業務的數據庫分隔,因爲每個業務的數據量、訪問量都不同,不能因爲一個業務把數據庫搞掛而牽連到其他業務。利用水平切分,當一個數據庫出現問題時,不會影響到100%的用戶,每個庫只承擔業務的一部分數據,這樣整體的可用性就能提高。

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