作為一種資料儲存層面上的水平伸縮解決方案,資料庫Sharding技術由來已久,很多海量資料系統在其發展演進的歷程中都曾經歷過分庫分表的Sharding改造階段。簡單地說,Sharding就是將原來單一資料庫按照一定的規則進行切分,把資料分散到多臺物理機(我們稱之為Shard)上儲存,從而突破單機限制,使系統能以Scale-Out的方式應對不斷上漲的海量資料,但是這種切分對上層應用來說是透明的,多個物理上分佈的資料庫在邏輯上依然是一個庫。實現Sharding需要解決一系列關鍵的技術問題,這些問題主要包括:切分策略、節點路由、全域性主鍵生成、跨節點排序/分組/表關聯、多資料來源事務處理和資料庫擴容等。本文將重點圍繞“資料庫擴容”進行深入討論,並提出一種允許自由規劃並能避免資料遷移和修改路由程式碼的Sharding擴容方案。
Sharding擴容——系統維護不能承受之重
任何Sharding系統,在上線執行一段時間後,資料就會積累到當前節點規模所能承載的上限,此時就需要對資料庫進行擴容了,也就是增加新的物理結點來分攤資料。如果系統使用的是基於ID進行雜湊的路由方式,那麼團隊需要根據新的節點規模重新計算所有資料應處的目標Shard,並將其遷移過去,這對團隊來說無疑是一個巨大的維護負擔;而如果系統是按增量區間進行路由(如每1千萬條資料或是每一個月的資料存放在一個節點上 ),雖然可以避免資料的遷移,卻有可能帶來“熱點”問題,也就是近期系統的讀寫都集中在最新建立的節點上(很多系統都有此類特點:新生資料的讀寫頻率明顯高於舊有資料),從而影響了系統效能。面對這種兩難的處境,Sharding擴容顯得異常困難。
一般來說,“理想”的擴容方案應該努力滿足以下幾個要求:
- 最好不遷移資料 (無論如何,資料遷移都是一個讓團隊壓力山大的問題)
- 允許根據硬體資源自由規劃擴容規模和節點儲存負載
- 能均勻的分佈資料讀寫,避免“熱點”問題
- 保證對已經達到儲存上限的節點不再寫入資料
目前,能夠避免資料遷移的優秀方案並不多,相對可行的有兩種,一種是維護一張記錄資料ID和目標Shard對應關係的對映表,寫入時,資料都寫入新擴容的Shard,同時將ID和目標節點寫入對映表,讀取時,先查對映表,找到目標Shard後再執行查詢。該方案簡單有效,但是讀寫資料都需要訪問兩次資料庫,且對映表本身也極易成為效能瓶頸。為此係統不得不引入分散式快取來快取對映表資料,但是這樣也無法避免在寫入時訪問兩次資料庫,同時大量對映資料對快取資源的消耗以及專門為此而引入分散式快取的代價都是需要權衡的問題。另一種方案來自淘寶綜合業務平臺團隊,它利用對2的倍數取餘具有向前相容的特性(如對4取餘得1的數對2取餘也是1)來分配資料,避免了行級別的資料遷移,但是依然需要進行表級別的遷移,同時對擴容規模和分表數量都有限制。總得來說,這些方案都不是十分的理想,多多少少都存在一些缺點,這也從一個側面反映出了Sharding擴容的難度。
取長補短,相容幷包——一種理想的Sharding擴容方案
如前文所述,Sharding擴容與系統採用的路由規則密切相關:基於雜湊的路由能均勻地分佈資料,但卻需要資料遷移,同時也無法避免對達到上限的節點不再寫入新資料;基於增量區間的路由天然不存在資料遷移和向某一節點無上限寫入資料的問題,但卻存在“熱點”困擾。我們設計方案的初衷就是希望能結合兩種路由規則的優勢,摒棄各自的劣勢,創造出一種接近“理想”狀態的擴容方式,而這種方式簡單概括起來就是:全域性按增量區間分佈資料,使用增量擴容,無資料遷移,區域性使用雜湊方式分散資料讀寫,解決“熱點”問題,同時對Sharding拓撲結構進行建模,使用一致的路由演算法,擴容時只需追加節點資料,不再修改雜湊邏輯程式碼。
原理
首先,作為方案的基石,為了能使系統感知到Shard並基於Shard的分佈進行路由計算,我們需要建立一個可以描述Sharding拓撲結構的程式設計模型。按照一般的切分原則,一個單一的資料庫會首先進行垂直切分,垂直切分只是將關係密切的表劃分在一起,我們把這樣分出的一組表稱為一個Partition。 接下來,如果Partition裡的表資料量很大且增速迅猛,就再進行水平切分,水平切分會將一張表的資料按增量區間或雜湊方式分散到多個Shard上儲存。在我們的方案裡,我們使用增量區間與雜湊相結合的方式,全域性上,資料按增量區間分佈,但是每個增量區間並不是按照某個Shard的儲存規模劃分的,而是根據一組Shard的儲存總量來確定的,我們把這樣的一組Shard稱為一個ShardGroup,區域性上,也就是一個ShardGroup內,記錄會再按雜湊方式均勻分佈到組內各Shard上。這樣,一條資料的路由會先根據其ID所處的區間確定ShardGroup,然後再通過雜湊命中ShardGroup內的某個目標Shard。在每次擴容時,我們會引入一組新的Shard,組成一個新的ShardGroup,為其分配增量區間並標記為“可寫入”,同時將原有ShardGroup標記為“不可寫入”,於是新生資料就會寫入新的ShardGroup,舊有資料不需要遷移。同時,在ShardGroup內部各Shard之間使用雜湊方式分佈資料讀寫,進而又避免了“熱點”問題。最後,在Shard內部,當單表資料達到一定上限時,表的讀寫效能就開始大幅下滑,但是整個資料庫並沒有達到儲存和負載的上限,為了充分發揮伺服器的效能,我們通常會新建多張結構一樣的表,並在新表上繼續寫入資料,我們把這樣的表稱為“分段表”(Fragment Table)。不過,引入分段表後所有的SQL在執行前都需要根據ID將其中的表名替換成真正的分段表名,這無疑增加了實現Sharding的難度,如果系統再使用了某種ORM框架,那麼替換起來可能會更加困難。目前很多資料庫提供一種與分段表類似的“分割槽”機制,但沒有分段表的副作用,團隊可以根據系統的實現情況在分段表和分割槽機制中靈活選擇。總之,基於上述切分原理,我們將得到如下Sharding拓撲結構的領域模型:
圖1. Sharding拓撲結構領域模型
在這個模型中,有幾個細節需要注意:ShardGroup的writable屬性用於標識該ShardGroup是否可以寫入資料,一個Partition在任何時候只能有一個ShardGroup是可寫的,這個ShardGroup往往是最近一次擴容引入的;startId和endId屬性用於標識該ShardGroup的ID增量區間;Shard的hashValue屬性用於標識該Shard節點接受哪些雜湊值的資料;FragmentTable的startId和endId是用於標識該分段表儲存資料的ID區間。
確立上述模型後,我們需要通過配置檔案或是在資料庫中建立與之對應的表來儲存節點後設資料,這樣,整個儲存系統的拓撲結構就可以被持久化起來,系統啟動時就能從配置檔案或資料庫中載入出當前的Sharding拓撲結構進行路由計算了(如果結點規模並不大可以使用配置檔案,如果節點規模非常大,需要建立相關表結構儲存這些結點後設資料。從最新的Oracle釋出的《面向大規模可伸縮網站基礎設施的MySQL參考架構》白皮書一文的“超大型系統架構參考”章節給出的架構圖中我們可以看到一種名為:Shard Catalog的專用伺服器,這個其實是儲存結點配置資訊的資料庫),擴容時只需要向對應的檔案或表中加入相關的節點資訊重啟系統即可,不需要修改任何路由邏輯程式碼。
示例
讓我們通過示例來了解這套方案是如何工作的。
階段一:初始上線
假設某系統初始上線,規劃為某表提供4000W條記錄的儲存能力,若單表儲存上限為1000W條,單庫儲存上限為2000W條,共需2個Shard,每個Shard包含兩個分段表,ShardGroup增量區間為0-4000W,按2取餘分散到2個Shard上,具體規劃方案如下:
圖2. 初始4000W儲存規模的規劃方案
與之相適應,Sharding拓撲結構的後設資料如下:
圖3. 對應Sharding後設資料
階段二:系統擴容
經過一段時間的執行,當原表總資料逼近4000W條上限時,系統就需要擴容了。為了演示方案的靈活性,我們假設現在有三臺伺服器Shard2、Shard3、Shard4,其效能和儲存能力表現依次為Shard2<Shard3<Shard4,我們安排Shard2儲存1000W條記錄,Shard3儲存2000W條記錄,Shard4儲存3000W條記錄,這樣,該表的總儲存能力將由擴容前的4000W條提升到10000W條,以下是詳細的規劃方案:
圖4. 二次擴容6000W儲存規模的規劃方案
相應拓撲結構表資料下:
圖5. 對應Sharding後設資料
從這個擴容案例中我們可以看出該方案允許根據硬體情況進行靈活規劃,對擴容規模和節點數量沒有硬性規定,是一種非常自由的擴容方案。
增強
接下來讓我們討論一個高階話題:對“再生”儲存空間的利用。對於大多數系統來說,歷史資料較為穩定,被更新或是刪除的概率並不高,反映到資料庫上就是歷史Shard的資料量基本保持恆定,但也不排除某些系統其資料有同等的刪除概率,甚至是越老的資料被刪除的可能性越大,這樣反映到資料庫上就是歷史Shard隨著時間的推移,資料量會持續下降,在經歷了一段時間後,節點就會騰出很大一部分儲存空間,我們把這樣的儲存空間叫“再生”儲存空間,如何有效利用再生儲存空間是這些系統在設計擴容方案時需要特別考慮的。回到我們的方案,實際上我們只需要在現有基礎上進行一個簡單的升級就可以實現對再生儲存空間的利用,升級的關鍵就是將過去ShardGroup和FragmentTable的單一的ID區間提升為多重ID區間。為此我們把ShardGroup和FragmentTable的ID區間屬性抽離出來,分別用ShardGroupInterval和FragmentTableIdInterval表示,並和它們保持一對多關係。
圖6. 增強後的Sharding拓撲結構領域模型
讓我們還是通過一個示例來了解升級後的方案是如何工作的。
階段三:不擴容,重複利用再生儲存空間
假設系統又經過一段時間的執行之後,二次擴容的6000W條儲存空間即將耗盡,但是由於系統自身的特點,早期的很多資料被刪除,Shard0和Shard1又各自騰出了一半的儲存空間,於是ShardGroup0總計有2000W條的儲存空間可以重新利用。為此,我們重新將ShardGroup0標記為writable=true,並給它追加一段ID區間:10000W-12000W,進而得到如下規劃方案:
圖7. 重複利用2000W再生儲存空間的規劃方案
相應拓撲結構的後設資料如下:
圖8. 對應Sharding後設資料
小結
這套方案綜合利用了增量區間和雜湊兩種路由方式的優勢,避免了資料遷移和“熱點”問題,同時,它對Sharding拓撲結構建模,使用了一致的路由演算法,從而避免了擴容時修改路由程式碼,是一種理想的Sharding擴容方案。