一、 基本思想
Sharding的基本思想就要把一個資料庫切分成多個部分放到不同的資料庫(server)上,從而緩解單一資料庫的效能問題。對于海量資料的資料庫,如果是因為表多而資料多,這時候適合使用垂直切分,即把關係緊密(比如同一模組)的表切分出來放在一個伺服器上。如果表並不多,但每張表的資料非常多,這時候適合水平切分,即把表的資料按某種規則(比如按ID散列)切分到多個資料庫(server)上。根據實際情況做出選擇,也可能會綜合使用垂直與水平切分。 1、 垂直切分
資料的垂直切分,也可以稱之為縱向切分。將資料庫想象成為由很多個一大塊一大塊的“資料區塊”(表)組成,我們垂直的將這些資料區塊切開,然後將他們分散到多台資料庫主機上面,這樣的切分方法就是一個垂直(縱向)的資料切分。
系統功能可以基本分為以下四個功能模組:使用者、群組訊息、相簿以及事件,分別對應為如下這些表:
1. 使用者模組表 user、user_profile、user_group、user_photo_album
2. 群組討論表 groups、group_message、group_message_content、top_message
3. 相簿相關表 photo、photo_album、photo_album_relation、photo_comment
4. 事件資訊表 event
模組之間的關係:
1. 群組討論模組和使用者模組之間主要存在通過使用者或者是群組關係來進行關聯。一般關聯的時候都會是通過使用者id或者nick_name以及group的 id來進行關聯,,通過模組之間的介面實現不會帶來太多麻煩。
2. 相簿模組僅僅與使用者模組存在通過使用者的關聯。這兩個模組之間的關聯基本就有通過使用者id關聯的內容,簡單清晰,介面明確。
3. 事件模組與各個模組可能都有關聯,但是都只關注其各個模組中對象的資訊ID,同樣可以做到很容易分拆。
所以,我們第一步可以將資料庫按照功能模組相關的表進行一次垂直分割,每個模組 涉及的表單獨到一個資料庫中,模組與模組之間的表關聯都在應用系統端通過介面來處理。 如下圖所示:
通過這樣的垂直切分之後,之前只能通過一個資料庫來提供的服務,就被分拆成四個資料庫來提供服務,服務能力自然是增加幾倍了。
垂直切分的優點
u 資料庫的拆分簡單明了,拆分規則明確
u 應用程式模組清晰明確,整合容易
u 資料維護方便易行,容易定位
垂直切分的缺點
u 部分表關聯無法在資料庫層級完成,需要在程式中完成
u 對於訪問極其頻繁且資料量超大的表仍然存在效能平靜,不一定能滿足要求
u 交易處理相對更為複雜
u 切分達到一定程度之後,擴充性會遇到限制
u 過度切分可能會帶來系統過渡複雜而難以維護 2、 水平切分
資料的水平切分,一般來說,簡單的水平切分主要是將某個訪問極其平凡的表再按照某個欄位的某種規則來分散到多個表之中,每個表中包含一部分資料。簡單來說,我們可以將資料的水平切分理解為是按照資料行的切分,就是將表中的某些行切分到一個資料庫,而另外的某些行又切分到其他的資料庫中。
對於我們的樣本資料庫來說,大部分的表都可以根據使用者ID來進行水平的切分,不同使用者相關的資料進行切分之後存放在不同的資料庫中。如將所有使用者ID通過2模數,然後分別存放於兩個不同的資料庫中,每個和使用者ID關聯上的表都可以這樣切分。這樣,基本上每個使用者相關的資料,都在同一個資料庫中,即使是需要關聯,也可以非常簡單的關聯上。
我們可以通過下圖來更為直觀的展示水平切分相關資訊:
水平切分的優點
u 表關聯基本能夠在資料庫端全部完成
u 不會存在某些超大型資料量和高負載的表遇到瓶頸的問題
u 應用程式端整體架構改動相對較少
u 交易處理相對簡單
u 只要切分規則能夠定義好,基本上較難遇到擴充性限制
水平切分的缺點
u 切分規則相對更為複雜,很難抽象出一個能夠滿足整個資料庫的切分規則
u 後期資料的維護難度有所增加,人為手工定位元據更困難
u 應用系統各模組耦合度較高,可能會對後面資料的遷移拆分造成一定的困難 3、 垂直與水平聯合切分
一般來說,我們資料庫中的所有表很難通過某一個(或少數幾個)欄位全部關聯起來,所以很難簡單的僅僅通過資料的水平切分來解決所有問題。而垂直切分也只能解決部分問題,對於那些負載非常高的系統,即使僅僅只是單個表都無法通過單台資料庫主機來承擔其負載,我們必須結合“水平” 和 “垂直”兩種切分方式同時使用,充分利用兩者的優點,避開其缺點。
每一個應用系統的負載都是一步一步增長上來的,在開始遇到效能瓶頸的時候,大多數架構師和DBA都會選擇先進行資料的垂直分割,然而,隨著業務的不斷擴張,系統負載的持續增長,在系統穩定一段時期之後,經過了垂直分割之後的資料庫叢集可能又再一次不堪重負,遇到了效能瓶頸。
這時候我們就必須要通過資料的水平切分的優勢,來解決這裡所遇到的問題。對於我們的樣本資料庫,假設在最開始,我們進行了資料的垂直切分,然而隨著業務的不斷增長,資料庫系統遇到了瓶頸,我們選擇重構資料庫叢集的架構。如何重構。考慮到之前已經做好了資料的垂直切分,而且模組結構清晰明確。而業務增長的勢頭越來越猛,即使現在進一步再次拆分模組,也堅持不了太久。我們選擇了在垂直切分的基礎上再進行水平分割。
在經曆過垂直分割後的各個資料庫叢集中的每一個都只有一個功能模組,而每個功能模切中的所有表基本上都會與某個欄位進行關聯。如使用者模組全部都可以通過使用者ID進行切分,群組討論模組則都通過群組ID 來切分,相簿模組則根據相簿 ID 分,最後的事件通知資訊表考慮到資料的時限性(僅僅只會訪問最近某個事件段的資訊),則考慮按時間來切分。
下圖展示了切分後的整個架構:
在應對不同的應用情境的時候,也需要充分考慮到這兩種切分方法各自的局限,以及各自的優勢,在不同的時期(負載壓力)使用不同的結合方式。
聯合切分的優點
u 可以充分利用垂直切分和水平切分各自的優勢而避免各自的缺陷
u 讓系統擴充性得到最大化提升
聯合切分的缺點
u 資料庫系統架構比較複雜,維護難度更大
u 應用程式架構也相對更複雜
二、 拆分實施策略和樣本示範
第一部分:實施策略
1. 準備階段
對資料庫進行分庫分表(Sharding化)前,需要充分瞭解系統商務邏輯和資料庫schema.繪製一張資料庫ER圖,以圖為基礎劃分shard,直觀易行,可以確保清醒思路。
2. 分析階段
1. 垂直切分
垂直切分的依據原則是:將業務緊密,表間關聯密切的表劃分在一起,例如同一模組的表。結合已經準備好的資料庫ER圖或領域模型圖,仿照活動圖表中的泳道概念,一個泳道代表一個shard,把所有表格劃分到不同的泳道中。下面的分析樣本會展示這種做法。這種方式多個資料庫之間的表結構不同。
2. 水平切分
垂直切分後,需要對shard內表格的資料量和增速進一步分析,以確定是否需要進行水平切分。這些資料庫中的表結構完全相同。
2.1 若劃分到一起的表格式資料增長緩慢,在產品上線後可遇見的足夠長的時期內均可以由單一資料庫承載,則不需要進行水平切分,所有表格駐留同一shard,所有表間關聯關係會得到最大限度的保留,同時保證了書寫SQL的自由度,不易受join、group by、order by等子句限制。
2.2 若劃分到一起的表格式資料量巨大,增速迅猛,需要進一步進行水平分割。進一步的水平分割就這樣進行:
2.2.1結合商務邏輯和表間關係,將當前shard劃分成多個更小的shard,通常情況下,這些更小的shard每一個都只包含一個主表(將以該表ID進行散列的表)和多個與其關聯或間接關聯的次表。這種一個shard一張主表多張次表的狀況是水平切分的必然結果。這樣切分下來,shard數量就會迅速增多。如果每一個shard代表一個獨立的資料庫,那麼管理和維護資料庫將會非常麻煩,而且這些小shard往往只有兩三張表,為此而建立一個新庫,利用率並不高,因此,在水平切分完成後可再進行一次“反向的Merge”,即:將業務上相近,並且具有相近資料增長速率(主表資料量在同一數量級上)的兩個或多個shard放到同一個資料庫上,在邏輯上它們依然是獨立的shard,有各自的主表,並依據各自主表的ID進行散列,不同的只是它們的散列模數(即節點數量)必需是一致的。這樣,每個資料庫結點上的表格數量就相對平均了。
2.2.2 所有表格均劃分到合適的shard之後,所有跨越shard的表間關聯都必須打斷,在書寫sql時,跨shard的join、group by、order by都將被禁止,需要在應用程式層面協調解決這些問題。
3. 實施階段
如果項目在開發伊始就決定進行分庫分表,則嚴格按照分析設計方案推進即可。如果是在中期架構演化中實施,除搭建實現sharding邏輯的基礎設施外,還需要對原有SQL逐一過濾分析,修改那些因為sharding而受到影響的sql。
第二部分:樣本示範
以下使用jpetstore(寵物店的電子商務系統)來示範如何進行分庫分表(sharding)在分析階段的工作。jpetstore來自原ibatis官方的一個Demo版本,SVN地址為:
http://mybatis.googlecode.com/svn/tags/java_release_2.3.4-726/jpetstore-5
由於系統較簡單,我們很容易從模型上看出,其主要由三個模組組成:使用者,產品和訂單。那麼垂直切分的方案也就出來了。接下來看水平切分,如果我們從一個實際的寵物店出發考慮,可能出現資料激增的單表應該是Account和Order,因此這兩張表需要進行水平切分。對於Product模組來說,如果是一個實際的系統,Product和Item的數量都不會很大,因此只做垂直切分就足夠了,也就是(Product,Category,Item,Iventory,Supplier)五張表在一個資料庫結點上(沒有水平切分,不會存在兩個以上的資料庫結點)。但是作為一個示範,我們假設產品模組也有大量的資料需要我們做水平切分,那麼分析來看,這個模組要拆分出兩個shard:一個是(Product(主),Category),另一個是(Item(主),Iventory,Supplier),同時,我們認為:這兩個shard在資料增速上應該是相近的,且在業務上也很緊密,那麼我們可以把這兩個shard放在同一個資料庫節點上,Item和Product資料在散列時取一樣的模。根據前文介紹的圖紙繪製方法,我們得到下面這張sharding示意圖:
對於這張圖再說明幾點:
1. 使用泳道表示物理shard(一個資料庫結點)
2. 若垂直切分出的shard進行了進一步的水平切分,但公用一個物理shard的話,則用虛線框住,表示其在邏輯上是一個獨立的shard。
3. 深色實體表示主表
4. X表示需要打斷的表間關聯 三、 全域主鍵建置原則
一旦資料庫被切分到多個物理結點上,我們將不能再依賴資料庫自身的主鍵產生機制。一方面,某個分區資料庫自產生的ID無法保證在全域上是唯一的;另一方面,應用程式在插入資料之前需要先獲得ID,以便進行SQL路由。
flickrTeam Dev在2010年撰文介紹了flickr使用的一種主鍵產生測策略,同時表示該方案在flickr上的實際運行效果也非常令人滿意,它與一般Sequence表方案有些類似,但卻很好地解決了效能瓶頸和單點問題,是一種非常可靠而高效的全域主鍵產生方案。
flickr這一方案的整體思想是:建立兩台以上的資料庫ID產生伺服器,每個伺服器都有一張記錄各表當前ID的Sequence表,但是Sequence中ID增長的步長是伺服器的數量,起始值依次錯開,這樣相當於把ID的產生散列到了每個伺服器節點上。例如:如果我們設定兩台資料庫ID產生伺服器,那麼就讓一台的Sequence表的ID起始值為1,每次增長步長為2,另一台的Sequence表的ID起始值為2,每次增長步長也為2,那麼結果就是奇數的ID都將從第一台伺服器上產生,偶數的ID都從第二台伺服器上產生,這樣就將產生ID的壓力均勻分散到兩台伺服器上,同時配合應用程式的控制,當一個伺服器失效後,系統能自動切換到另一個伺服器上擷取ID,從而保證了系統的容錯。
關於這個方案,有幾點細節這裡再說明一下:
1. flickr的資料庫ID產生伺服器是專用伺服器,伺服器上只有一個資料庫,資料庫中表都是用於產生Sequence的,這也是因為auto-increment-offset和auto-increment-increment這兩個資料庫變數是資料庫執行個體層級的變數。
2. flickr的方案中表格中的stub欄位只是一個char(1) NOT NULL存根欄位,並非表名,因此,一般來說,一個Sequence表只有一條紀錄,可以同時為多張表產生ID,如果需要表的ID是有連續的,需要為該表單獨建立Sequence表。
3. 方案使用了MySQL的LAST_INSERT_ID()函數,這也決定了Sequence表只能有一條記錄。
4. 使用REPLACE INTO插入資料,這是很討巧的作法,主要是希望利用mysql自身的機制產生ID,不僅是因為這樣簡單,更是因為我們需要ID按照我們設定的方式(初值和步長)來產生。
5. SELECT LAST_INSERT_ID()必須要於REPLACE INTO語句在同一個資料庫連接下才能得到剛剛插入的新ID,否則返回的值總是0
6. 該方案中Sequence表使用的是MyISAM引擎,以擷取更高的效能,注意:MyISAM引擎使用的是表層級的鎖,MyISAM對錶的讀寫是串列的,因此不必擔心在並發時兩次讀取會得到同一個ID(另外,應該程式也不需要同步,每個請求的線程都會得到一個新的connection,不存在需要同步的共用資源)。經過實際對比測試,使用一樣的Sequence表進行ID產生,MyISAM引擎要比InnoDB表現高出很多。
7. 可使用純JDBC實現對Sequence表的操作,以便獲得更高的效率,實驗表明,即使只使用spring JDBC效能也不及純JDBC來得快。
實現該方案,應用程式同樣需要做一些處理,主要是兩方面的工作:
1. 自動均衡資料庫ID產生伺服器的訪問
2. 確保在某個資料庫ID產生伺服器失效的情況下,能將請求轉寄到其他伺服器上執行。 四、 sharding實現層面
通過前面的章節,我們已經很清楚了通過資料庫的資料切分可以極大的提高系統的擴充性。但是,資料庫中的資料在經過垂直和(或)水平切分被存放在不同的資料庫主機之後,應用系統面臨的最大問題就是如何來讓這些資料來源得到較好的整合。
在應用伺服器與資料庫之間通過代理實現
在應用伺服器與資料庫之間加入一個代理,應用程式向資料發出的資料請求會先通過代理,代理會根據配置的路由規則,對SQL進行解析後路由到目標shard,因為這種方案對應用程式完全透明,通用性好,所以成為了很多sharding產品的選擇。在這方面較為知名的產品是mysql官方的代理工具:Mysql Proxy和一款國人開發的產品:amoeba。mysql proxy本身並沒有實現任何sharding邏輯,它只是作為一種面向mysql資料庫的代理,給開發人員提供了一個嵌入sharding邏輯的場所,它使用lua作為程式設計語言,這對很多團隊來說是需要考慮的一個問題。amoeba則是專門實現讀寫分離與sharding的代理產品,它使用非常簡單,不使用任何程式設計語言,只需要通過xml進行配置。不過amoeba不支援事務(從應用程式發出的包含事務資訊的請求到達amoeba時,事務資訊會被抹去,因此,即使是單點資料訪問也不會有事務存在)一直是個硬傷。當然,這要看產品的定位和設計理念,我們只能說對於那些對事務要求非常高的系統,amoeba是不適合的。
Amoeba For MySQL主要是專門針對MySQL資料庫的解決方案,前端應用程式請求的協議以及後端串連的資料來源資料庫都必須是MySQL。對於用戶端的任何應用程式來說, amoeba For MySQL和一個MySQL沒有什麼區別,任何使用MySQL協議的用戶端請求, 都可以被 Amoeba For MySQL解析並進行相應的處理,下圖可以告訴我們Amoeba For MySQL的架構資訊:
Amoeba使用指南:
http://docs.hexnova.com/amoeba/index.html
其他的一些實現層可以參考:
http://blog.csdn.NET/bluishglc/article/details/7766508
五、 多資料來源的交易處理
六、 一種支援自由規劃無須資料移轉和修改路由代碼的Sharding擴容方案
Sharding擴容——系統維護不能承受之重
任何Sharding系統,在上線運行一段時間後,資料就會積累到當前節點規模所能承載的上限,此時就需要對資料庫進行擴容了,也就是增加新的物理結點來分攤資料。如果系統使用的是基於ID進行散列的路由方式,那麼團隊需要根據新的節點規模重新計算所有資料應處的目標Shard,並將其遷移過去,這對團隊來說無疑是一個巨大的維護負擔;而如果系統是按增量區間進行路由(如每1千萬條資料或是每一個月的資料存放在一個節點上 ),雖然可以避免資料的遷移,卻有可能帶來“熱點”問題,也就是近期系統的讀寫都集中在最新建立的節點上(很多系統都有此類特點:新生資料的讀寫頻率明顯高於舊有資料),從而影響了系統效能。面對這種兩難的處境,Sharding擴容顯得異常困難。
一般來說,“理想”的擴容方案應該努力滿足以下幾個要求:
1. 最好不遷移資料 (無論如何,資料移轉都是一個讓團隊壓力山大的問題)
2. 允許根據硬體資源自由規劃擴容規模和節點儲存負載
3. 能均勻的分布資料讀寫,避免“熱點”問題
4. 保證對已經達到儲存上限的節點不再寫入資料
目前,能夠避免資料移轉的優秀方案並不多,相對可行的有兩種,一種是維護一張記錄資料ID和目標Shard對應關係的映射表,寫入時,資料都寫入新擴容的Shard