數(shù)據(jù)庫分庫分表(sharding)系列

目錄;
(一) 拆分實施策略和示例演示
(二) 全局主鍵生成策略
(三) 關(guān)于使用框架還是自主開發(fā)以及sharding實現(xiàn)層面的考量
(四) 多數(shù)據(jù)源的事務處理
(五) 一種支持自由規(guī)劃無須數(shù)據(jù)遷移和修改路由代碼的Sharding擴容方案
(一) 拆分實施策略和示例演示
第一部分:實施策略


圖1.數(shù)據(jù)庫分庫分表(sharding)實施策略圖解
1.準備階段
對數(shù)據(jù)庫進行分庫分表(Sharding化)前,需要開發(fā)人員充分了解系統(tǒng)業(yè)務邏輯和數(shù)據(jù)庫schema.一個好的建議是繪制一張數(shù)據(jù)庫ER圖或領(lǐng)域模型圖,以這類圖為基礎(chǔ)劃分shard,直觀易行,可以確保開發(fā)人員始終保持清醒思路。對于是選擇數(shù)據(jù)庫ER圖還是領(lǐng)域模型圖要根據(jù)項目自身情況進行選擇。如果項目使用數(shù)據(jù)驅(qū)動的開發(fā)方式,團隊以數(shù)據(jù)庫ER圖作為業(yè)務交流的基礎(chǔ),則自然會選擇數(shù)據(jù)庫ER圖,如果項目使用的是領(lǐng)域驅(qū)動的開發(fā)方式,并通過OR-Mapping構(gòu)建了一個良好的領(lǐng)域模型,那么領(lǐng)域模型圖無疑是最好的選擇。就我個人來說,更加傾向使用領(lǐng)域模型圖,因為進行切分時更多的是以業(yè)務為依據(jù)進行分析判斷,領(lǐng)域模型無疑更加清晰和直觀。
2.分析階段

  1. 垂直切分
    垂直切分的依據(jù)原則是:將業(yè)務緊密,表間關(guān)聯(lián)密切的表劃分在一起,例如同一模塊的表。結(jié)合已經(jīng)準備好的數(shù)據(jù)庫ER圖或領(lǐng)域模型圖,仿照活動圖中的泳道概念,一個泳道代表一個shard,把所有表格劃分到不同的泳道中。下面的分析示例會展示這種做法。當然,你也可以在打印出的ER圖或模型圖上直接用鉛筆圈,一切取決于你自己的喜好。
  2. 水平切分
    垂直切分后,需要對shard內(nèi)表格的數(shù)據(jù)量和增速進一步分析,以確定是否需要進行水平切分。
    2.1若劃分到一起的表格數(shù)據(jù)增長緩慢,在產(chǎn)品上線后可遇見的足夠長的時期內(nèi)均可以由單一數(shù)據(jù)庫承載,則不需要進行水平切分,所有表格駐留同一shard,所有表間關(guān)聯(lián)關(guān)系會得到最大限度的保留,同時保證了書寫SQL的自由度,不易受join、group by、order by等子句限制。
    2.2 若劃分到一起的表格數(shù)據(jù)量巨大,增速迅猛,需要進一步進行水平分割。進一步的水平分割就這樣進行:
    2.2.1.結(jié)合業(yè)務邏輯和表間關(guān)系,將當前shard劃分成多個更小的shard,通常情況下,這些更小的shard每一個都只包含一個主表(將以該表ID進行散列的表)和多個與其關(guān)聯(lián)或間接關(guān)聯(lián)的次表。這種一個shard一張主表多張次表的狀況是水平切分的必然結(jié)果。這樣切分下來,shard數(shù)量就會迅速增多。如果每一個shard代表一個獨立的數(shù)據(jù)庫,那么管理和維護數(shù)據(jù)庫將會非常麻煩,而且這些小shard往往只有兩三張表,為此而建立一個新庫,利用率并不高,因此,在水平切分完成后可再進行一次“反向的Merge”,即:將業(yè)務上相近,并且具有相近數(shù)據(jù)增長速率(主表數(shù)據(jù)量在同一數(shù)量級上)的兩個或多個shard放到同一個數(shù)據(jù)庫上,在邏輯上它們依然是獨立的shard,有各自的主表,并依據(jù)各自主表的ID進行散列,不同的只是它們的散列取模(即節(jié)點數(shù)量)必需是一致的。這樣,每個數(shù)據(jù)庫結(jié)點上的表格數(shù)量就相對平均了。
    2.2.2. 所有表格均劃分到合適的shard之后,所有跨越shard的表間關(guān)聯(lián)都必須打斷,在書寫sql時,跨shard的join、group by、order by都將被禁止,需要在應用程序?qū)用鎱f(xié)調(diào)解決這些問題。
    特別想提一點:經(jīng)水平切分后,shard的粒度往往要比只做垂直切割的粒度要小,原單一垂直shard會被細分為一到多個以一個主表為中心關(guān)聯(lián)或間接關(guān)聯(lián)多個次表的shard,此時的shard粒度與領(lǐng)域驅(qū)動設(shè)計中的“聚合”概念不謀而合,甚至可以說是完全一致,每個shard的主表正是一個聚合中的聚合根!
    3.實施階段
    如果項目在開發(fā)伊始就決定進行分庫分表,則嚴格按照分析設(shè)計方案推進即可。如果是在中期架構(gòu)演進中實施,除搭建實現(xiàn)sharding邏輯的基礎(chǔ)設(shè)施外(關(guān)于該話題會在下篇文章中進行闡述),還需要對原有SQL逐一過濾分析,修改那些因為sharding而受到影響的sql.
    第二部分:示例演示
    本文選擇一個人盡皆知的應用:jpetstore來演示如何進行分庫分表(sharding)在分析階段的工作。由于一些個人原因,演示使用的jpetstore來自原ibatis官方的一個Demo版本,SVN地址為:http://mybatis.googlecode.com/svn/tags/java_release_2.3.4-726/jpetstore-5。關(guān)于jpetstore的業(yè)務邏輯這里不再介紹,這是一個非常簡單的電商系統(tǒng)原型,其領(lǐng)域模型如下圖:

    圖2. jpetstore領(lǐng)域模型
    由于系統(tǒng)較簡單,我們很容易從模型上看出,其主要由三個模塊組成:用戶,產(chǎn)品和訂單。那么垂直切分的方案也就出來了。接下來看水平切分,如果我們從一個實際的寵物店出發(fā)考慮,可能出現(xiàn)數(shù)據(jù)激增的單表應該是Account和Order,因此這兩張表需要進行水平切分。對于Product模塊來說,如果是一個實際的系統(tǒng),Product和Item的數(shù)量都不會很大,因此只做垂直切分就足夠了,也就是(Product,Category,Item,Iventory,Supplier)五張表在一個數(shù)據(jù)庫結(jié)點上(沒有水平切分,不會存在兩個以上的數(shù)據(jù)庫結(jié)點)。但是作為一個演示,我們假設(shè)產(chǎn)品模塊也有大量的數(shù)據(jù)需要我們做水平切分,那么分析來看,這個模塊要拆分出兩個shard:一個是(Product(主),Category),另一個是(Item(主),Iventory,Supplier),同時,我們認為:這兩個shard在數(shù)據(jù)增速上應該是相近的,且在業(yè)務上也很緊密,那么我們可以把這兩個shard放在同一個數(shù)據(jù)庫節(jié)點上,Item和Product數(shù)據(jù)在散列時取一樣的模。根據(jù)前文介紹的圖紙繪制方法,我們得到下面這張sharding示意圖:

    圖3. jpetstore sharding示意圖
    對于這張圖再說明幾點:
    1.使用泳道表示物理shard(一個數(shù)據(jù)庫結(jié)點)
    2.若垂直切分出的shard進行了進一步的水平切分,但公用一個物理shard的話,則用虛線框住,表示其在邏輯上是一個獨立的shard。
    3.深色實體表示主表
    4.X表示需要打斷的表間關(guān)聯(lián)
    (二) 全局主鍵生成策略
    第一部分:一些常見的主鍵生成策略
    一旦數(shù)據(jù)庫被切分到多個物理結(jié)點上,我們將不能再依賴數(shù)據(jù)庫自身的主鍵生成機制。一方面,某個分區(qū)數(shù)據(jù)庫自生成的ID無法保證在全局上是唯一的;另一方面,應用程序在插入數(shù)據(jù)之前需要先獲得ID,以便進行SQL路由。目前幾種可行的主鍵生成策略有:
  3. UUID:使用UUID作主鍵是最簡單的方案,但是缺點也是非常明顯的。由于UUID非常的長,除占用大量存儲空間外,最主要的問題是在索引上,在建立索引和基于索引進行查詢時都存在性能問題。
  4. 結(jié)合數(shù)據(jù)庫維護一個Sequence表:此方案的思路也很簡單,在數(shù)據(jù)庫中建立一個Sequence表,表的結(jié)構(gòu)類似于:
    [sql] view plaincopy
    01.CREATE TABLE SEQUENCE (
  5. tablename varchar(30) NOT NULL,
  6. nextid bigint(20) NOT NULL,
  7. PRIMARY KEY (tablename)
    05.) ENGINE=InnoDB
    每當需要為某個表的新紀錄生成ID時就從Sequence表中取出對應表的nextid,并將nextid的值加1后更新到數(shù)據(jù)庫中以備下次使用。此方案也較簡單,但缺點同樣明顯:由于所有插入任何都需要訪問該表,該表很容易成為系統(tǒng)性能瓶頸,同時它也存在單點問題,一旦該表數(shù)據(jù)庫失效,整個應用程序?qū)o法工作。有人提出使用Master-Slave進行主從同步,但這也只能解決單點問題,并不能解決讀寫比為1:1的訪問壓力問題。
    除此之外,還有一些方案,像對每個數(shù)據(jù)庫結(jié)點分區(qū)段劃分ID,以及網(wǎng)上的一些ID生成算法,因為缺少可操作性和實踐檢驗,本文并不推薦。實際上,接下來,我們要介紹的是Fickr使用的一種主鍵生成方案,這個方案是目前我所知道的最優(yōu)秀的一個方案,并且經(jīng)受了實踐的檢驗,可以為大多數(shù)應用系統(tǒng)所借鑒。
    第二部分:一種極為優(yōu)秀的主鍵生成策略
    flickr開發(fā)團隊在2010年撰文介紹了flickr使用的一種主鍵生成測策略,同時表示該方案在flickr上的實際運行效果也非常令人滿意,原文連接:Ticket Servers: Distributed Unique Primary Keys on the Cheap 這個方案是我目前知道的最好的方案,它與一般Sequence表方案有些類似,但卻很好地解決了性能瓶頸和單點問題,是一種非常可靠而高效的全局主鍵生成方案。

    圖1. flickr采用的sharding主鍵生成方案示意圖
    flickr這一方案的整體思想是:建立兩臺以上的數(shù)據(jù)庫ID生成服務器,每個服務器都有一張記錄各表當前ID的Sequence表,但是Sequence中ID增長的步長是服務器的數(shù)量,起始值依次錯開,這樣相當于把ID的生成散列到了每個服務器節(jié)點上。例如:如果我們設(shè)置兩臺數(shù)據(jù)庫ID生成服務器,那么就讓一臺的Sequence表的ID起始值為1,每次增長步長為2,另一臺的Sequence表的ID起始值為2,每次增長步長也為2,那么結(jié)果就是奇數(shù)的ID都將從第一臺服務器上生成,偶數(shù)的ID都從第二臺服務器上生成,這樣就將生成ID的壓力均勻分散到兩臺服務器上,同時配合應用程序的控制,當一個服務器失效后,系統(tǒng)能自動切換到另一個服務器上獲取ID,從而保證了系統(tǒng)的容錯。
    關(guān)于這個方案,有幾點細節(jié)這里再說明一下:
    flickr的數(shù)據(jù)庫ID生成服務器是專用服務器,服務器上只有一個數(shù)據(jù)庫,數(shù)據(jù)庫中表都是用于生成Sequence的,這也是因為auto-increment-offset和auto-increment-increment這兩個數(shù)據(jù)庫變量是數(shù)據(jù)庫實例級別的變量。
    flickr的方案中表格中的stub字段只是一個char(1) NOT NULL存根字段,并非表名,因此,一般來說,一個Sequence表只有一條紀錄,可以同時為多張表生成ID,如果需要表的ID是有連續(xù)的,需要為該表單獨建立Sequence表。
    方案使用了mysql的LAST_INSERT_ID()函數(shù),這也決定了Sequence表只能有一條記錄。
    使用REPLACE INTO插入數(shù)據(jù),這是很討巧的作法,主要是希望利用mysql自身的機制生成ID,不僅是因為這樣簡單,更是因為我們需要ID按照我們設(shè)定的方式(初值和步長)來生成。
    SELECT LAST_INSERT_ID()必須要于REPLACE INTO語句在同一個數(shù)據(jù)庫連接下才能得到剛剛插入的新ID,否則返回的值總是0
    該方案中Sequence表使用的是MyISAM引擎,以獲取更高的性能,注意:MyISAM引擎使用的是表級別的鎖,MyISAM對表的讀寫是串行的,因此不必擔心在并發(fā)時兩次讀取會得到同一個ID(另外,應該程序也不需要同步,每個請求的線程都會得到一個新的connection,不存在需要同步的共享資源)。經(jīng)過實際對比測試,使用一樣的Sequence表進行ID生成,MyISAM引擎要比InnoDB表現(xiàn)高出很多!
    可使用純JDBC實現(xiàn)對Sequence表的操作,以便獲得更高的效率,實驗表明,即使只使用Spring JDBC性能也不及純JDBC來得快

實現(xiàn)該方案,應用程序同樣需要做一些處理,主要是兩方面的工作:

  1. 自動均衡數(shù)據(jù)庫ID生成服務器的訪問
  2. 確保在某個數(shù)據(jù)庫ID生成服務器失效的情況下,能將請求轉(zhuǎn)發(fā)到其他服務器上執(zhí)行。
    (三) 關(guān)于使用框架還是自主開發(fā)以及sharding實現(xiàn)層面的考量
    一、sharding邏輯的實現(xiàn)層面
    從一個系統(tǒng)的程序架構(gòu)層面來看,sharding邏輯可以在DAO層、JDBC API層、介于DAO與JDBC之間的Spring數(shù)據(jù)訪問封裝層(各種spring的template)以及介于應用服務器與數(shù)據(jù)庫之間的sharding代理服務器四個層面上實現(xiàn)。



    圖1. Sharding實現(xiàn)層面與相關(guān)框架/產(chǎn)品
    在DAO層實現(xiàn)
    當團隊決定自行實現(xiàn)sharding的時候,DAO層可能是嵌入sharding邏輯的首選位置,因為在這個層面上,每一個DAO的方法都明確地知道需要訪問的數(shù)據(jù)表以及查詢參數(shù),借助這些信息可以直接定位到目標shard上,而不必像框架那樣需要對SQL進行解析然后再依據(jù)配置的規(guī)則進行路由。另一個優(yōu)勢是不會受ORM框架的制約。由于現(xiàn)在的大多數(shù)應用在數(shù)據(jù)訪問層上會依賴某種ORM框架,而多數(shù)的shrading框架往往無法支持或只能支持一種orm框架,這使得在選擇和應用框架時受到了很大的制約,而自行實現(xiàn)sharding完全沒有這方面的問題,甚至不同的shard使用不同的orm框架都可以在一起協(xié)調(diào)工作。比如現(xiàn)在的java應用大多使用hibernate,但是當下還沒有非常令人滿意的基于hibernate的sharding框架,(關(guān)于hibernate hards會在下文介紹),因此很多團隊會選擇自行實現(xiàn)sharding。
    簡單總結(jié)一下,在DAO層自行實現(xiàn)sharding的優(yōu)勢在于:不受ORM框架的制約、實現(xiàn)起來較為簡單、易于根據(jù)系統(tǒng)特點進行靈活的定制、無需SQL解析和路由規(guī)則匹配,性能上表現(xiàn)會稍好一些;劣勢在于:有一定的技術(shù)門檻,工作量比依靠框架實現(xiàn)要大(反過來看,框架會有學習成本)、不通用,只能在特定系統(tǒng)里工作。當然,在DAO層同樣可以通過XML配置或是注解將sharding邏輯抽離到“外部”,形成一套通用的框架. 不過目前還沒有出現(xiàn)此類的框架。
    在ORM框架層實現(xiàn)
    在ORM框架層實現(xiàn)sharding有兩個方向,一個是在實現(xiàn)O-R Mapping的前提下同時提供sharding支持,從而定位為一種分布式的數(shù)據(jù)訪問框架,這一類類型的框架代表就是guzz另一個方向是通過對既有ORM框架進行修改增強來加入sharding機制。此類型的代表產(chǎn)品是hibernate shard. 應該說以hibernate這樣主流的地位,行業(yè)對于一款面向hibernate的sharding框架的需求是非常迫切的,但是就目前的hibernate shards來看,表現(xiàn)還算不上令人滿意,主要是它對使用hibernate的限制過多,比如它對HQL的支持就非常有限。在mybatis方面,目前還沒有成熟的相關(guān)框架產(chǎn)生。有人提出利用mybatis的插件機制實現(xiàn)sharding,但是遺憾的是,mybatis的插件機制控制不到多數(shù)據(jù)源的連接層面,另一方面,離開插件層又失去了對sql進行集中解析和路由的機會,因此在mybatis框架上,目前還沒有可供借鑒的框架,團隊可能要在DAO層或Spring模板類上下功夫了。
    在JDBC API層實現(xiàn)
    JDBC API層是很多人都會想到的一個實現(xiàn)sharding的絕佳場所,如果我們能提供一個實現(xiàn)了sharding邏輯的JDBC API實現(xiàn),那么sharding對于整個應用程序來說就是完全透明的,而這樣的實現(xiàn)可以直接作為通用的sharding產(chǎn)品了。但是這種方案的技術(shù)門檻和工作量顯然不是一般團隊能做得來的,因此基本上沒有團隊會在這一層面上實現(xiàn)sharding,甚至也沒有此類的開源產(chǎn)品。筆者知道的只有一款商業(yè)產(chǎn)品dbShards采用的是這一方案。
    在介于DAO與JDBC之間的Spring數(shù)據(jù)訪問封裝層實現(xiàn)
    在springd大行其道的今天,幾乎沒有哪個java平臺上構(gòu)建的應用不使用spring,在DAO與JDBC之間,spring提供了各種template來管理資源的創(chuàng)建與釋放以及與事務的同步,大多數(shù)基于spring的應用都會使用template類做為數(shù)據(jù)訪問的入口,這給了我們另一個嵌入sharding邏輯的機會,就是通過提供一個嵌入了sharding邏輯的template類來完成sharding工作.這一方案在效果上與基于JDBC API實現(xiàn)的方案基本一致,同樣是對上層代碼透明,在進行sharding改造時可以平滑地過度,但它的實現(xiàn)卻比基于JDBC API的方式簡單,因此成為了不少框架的選擇,阿里集團研究院開源的Cobar Client就是這類方案的一種實現(xiàn)。
    在應用服務器與數(shù)據(jù)庫之間通過代理實現(xiàn)
    在應用服務器與數(shù)據(jù)庫之間加入一個代理,應用程序向數(shù)據(jù)發(fā)出的數(shù)據(jù)請求會先通過代理,代理會根據(jù)配置的路由規(guī)則,對SQL進行解析后路由到目標shard,因為這種方案對應用程序完全透明,通用性好,所以成為了很多sharding產(chǎn)品的選擇。在這方面較為知名的產(chǎn)品是mysql官方的代理工具:Mysql Proxy和一款國人開發(fā)的產(chǎn)品:amoeba。mysql proxy本身并沒有實現(xiàn)任何sharding邏輯,它只是作為一種面向mysql數(shù)據(jù)庫的代理,給開發(fā)人員提供了一個嵌入sharding邏輯的場所,它使用lua作為編程語言,這對很多團隊來說是需要考慮的一個問題。amoeba則是專門實現(xiàn)讀寫分離與sharding的代理產(chǎn)品,它使用非常簡單,不使用任何編程語言,只需要通過xml進行配置。不過amoeba不支持事務(從應用程序發(fā)出的包含事務信息的請求到達amoeba時,事務信息會被抹去,因此,即使是單點數(shù)據(jù)訪問也不會有事務存在)一直是個硬傷。當然,這要看產(chǎn)品的定位和設(shè)計理念,我們只能說對于那些對事務要求非常高的系統(tǒng),amoeba是不適合的。
    二、使用框架還是自主開發(fā)?
    前面的討論中已經(jīng)羅列了很多開源框架與產(chǎn)品,這里再整理一下:基于代理方式的有MySQL Proxy和Amoeba,基于Hibernate框架的是Hibernate Shards,通過重寫spring的ibatis template類是Cobar Client,這些框架各有各的優(yōu)勢與短板,架構(gòu)師可以在深入調(diào)研之后結(jié)合項目的實際情況進行選擇,但是總的來說,我個人對于框架的選擇是持謹慎態(tài)度的。一方面多數(shù)框架缺乏成功案例的驗證,其成熟性與穩(wěn)定性值得懷疑。另一方面,一些從成功商業(yè)產(chǎn)品開源出框架(如阿里和淘寶的一些開源項目)是否適合你的項目是需要架構(gòu)師深入調(diào)研分析的。當然,最終的選擇一定是基于項目特點、團隊狀況、技術(shù)門檻和學習成本等綜合因素考量確定的。
    (四) 多數(shù)據(jù)源的事務處理
    分布式事務
    這是最為人們所熟知的多數(shù)據(jù)源事務處理機制。本文并不打算對分布式事務做過多介紹,讀者可參考此文:關(guān)于分布式事務、兩階段提交、一階段提交、Best Efforts 1PC模式和事務補償機制的研究 。在這里只想對分布式事務的利弊作一下分析。
    優(yōu)勢:

  3. 基于兩階段提交,最大限度地保證了跨數(shù)據(jù)庫操作的“原子性”,是分布式系統(tǒng)下最嚴格的事務實現(xiàn)方式。
  4. 實現(xiàn)簡單,工作量小。由于多數(shù)應用服務器以及一些獨立的分布式事務協(xié)調(diào)器做了大量的封裝工作,使得項目中引入分布式事務的難度和工作量基本上可以忽略不計。
    劣勢:
    系統(tǒng)“水平”伸縮的死敵。基于兩階段提交的分布式事務在提交事務時需要在多個節(jié)點之間進行協(xié)調(diào),最大限度地推后了提交事務的時間點,客觀上延長了事務的執(zhí)行時間,這會導致事務在訪問共享資源時發(fā)生沖突和死鎖的概率增高,隨著數(shù)據(jù)庫節(jié)點的增多,這種趨勢會越來越嚴重,從而成為系統(tǒng)在數(shù)據(jù)庫層面上水平伸縮的"枷鎖", 這是很多Sharding系統(tǒng)不采用分布式事務的主要原因。
    基于Best Efforts 1PC模式的事務
    與分布式事務采用的兩階段提交不同,Best Efforts 1PC模式采用的是一階段端提交,犧牲了事務在某些特殊情況(當機、網(wǎng)絡(luò)中斷等)下的安全性,卻獲得了良好的性能,特別是消除了對水平伸縮的桎酷。Distributed transactions in Spring, with and without XA一文對Best Efforts 1PC模式進行了詳細的說明,該文提供的Demo代碼更是直接給出了在Spring環(huán)境下實現(xiàn)一階段提交的多數(shù)據(jù)源事務管理示例。不過需要注意的是,原示例是基于spring 3.0之前的版本,如果你使用spring 3.0+,會得到如下錯誤:java.lang.IllegalStateException: Cannot activate transaction synchronization - already active,如果使用spring 3.0+,你需要參考spring-data-neo4j的實現(xiàn)。鑒于Best Efforts 1PC模式的性能優(yōu)勢,以及相對簡單的實現(xiàn)方式,它被大多數(shù)的sharding框架和項目采用。
    事務補償機制
    對于那些對性能要求很高,但對一致性要求并不高的系統(tǒng),往往并不苛求系統(tǒng)的實時一致性,只要在一個允許的時間周期內(nèi)達到最終一致性即可,這使得事務補償機制成為一種可行的方案。事務補償機制最初被提出是在“長事務”的處理中,但是對于分布式系統(tǒng)確保一致性也有很好的參考意義。籠統(tǒng)地講,與事務在執(zhí)行中發(fā)生錯誤后立即回滾的方式不同,事務補償是一種事后檢查并補救的措施,它只期望在一個容許時間周期內(nèi)得到最終一致的結(jié)果就可以了。事務補償?shù)膶崿F(xiàn)與系統(tǒng)業(yè)務緊密相關(guān),并沒有一種標準的處理方式。一些常見的實現(xiàn)方式有:對數(shù)據(jù)進行對帳檢查;基于日志進行比對;定期同標準數(shù)據(jù)來源進行同步,等等。
    小結(jié)
    分布式事務,最嚴格的事務實現(xiàn),但性能是個大問題;Best Efforts 1PC模式,性能與事務可靠性的平衡,支持系統(tǒng)水平伸縮,大多數(shù)情況下是最合適的選擇;事務補償機制,只能適用于對事務性要求不高,允許數(shù)據(jù)“最終一致”即可的系統(tǒng),犧牲實時一致性,獲得最大的性能回報。
    (五) 一種支持自由規(guī)劃無須數(shù)據(jù)遷移和修改路由代碼的Sharding擴容方案
    本文將重點圍繞“數(shù)據(jù)庫擴容”進行深入討論,并提出一種允許自由規(guī)劃并能避免數(shù)據(jù)遷移和修改路由代碼的Sharding擴容方案
    Sharding擴容——系統(tǒng)維護不能承受之重
    任何Sharding系統(tǒng),在上線運行一段時間后,數(shù)據(jù)就會積累到當前節(jié)點規(guī)模所能承載的上限,此時就需要對數(shù)據(jù)庫進行擴容了,也就是增加新的物理結(jié)點來分攤數(shù)據(jù)。如果系統(tǒng)使用的是基于ID進行散列的路由方式,那么團隊需要根據(jù)新的節(jié)點規(guī)模重新計算所有數(shù)據(jù)應處的目標Shard,并將其遷移過去,這對團隊來說無疑是一個巨大的維護負擔;而如果系統(tǒng)是按增量區(qū)間進行路由(如每1千萬條數(shù)據(jù)或是每一個月的數(shù)據(jù)存放在一個節(jié)點上 ),雖然可以避免數(shù)據(jù)的遷移,卻有可能帶來“熱點”問題,也就是近期系統(tǒng)的讀寫都集中在最新創(chuàng)建的節(jié)點上(很多系統(tǒng)都有此類特點:新生數(shù)據(jù)的讀寫頻率明顯高于舊有數(shù)據(jù)),從而影響了系統(tǒng)性能。面對這種兩難的處境,Sharding擴容顯得異常困難。
    一般來說,“理想”的擴容方案應該努力滿足以下幾個要求:
    最好不遷移數(shù)據(jù) (無論如何,數(shù)據(jù)遷移都是一個讓團隊壓力山大的問題)
    允許根據(jù)硬件資源自由規(guī)劃擴容規(guī)模和節(jié)點存儲負載
    能均勻的分布數(shù)據(jù)讀寫,避免“熱點”問題
    保證對已經(jīng)達到存儲上限的節(jié)點不再寫入數(shù)據(jù)

目前,能夠避免數(shù)據(jù)遷移的優(yōu)秀方案并不多,相對可行的有兩種,一種是維護一張記錄數(shù)據(jù)ID和目標Shard對應關(guān)系的映射表,寫入時,數(shù)據(jù)都寫入新擴容的Shard,同時將ID和目標節(jié)點寫入映射表,讀取時,先查映射表,找到目標Shard后再執(zhí)行查詢。該方案簡單有效,但是讀寫數(shù)據(jù)都需要訪問兩次數(shù)據(jù)庫,且映射表本身也極易成為性能瓶頸。為此系統(tǒng)不得不引入分布式緩存來緩存映射表數(shù)據(jù),但是這樣也無法避免在寫入時訪問兩次數(shù)據(jù)庫,同時大量映射數(shù)據(jù)對緩存資源的消耗以及專門為此而引入分布式緩存的代價都是需要權(quán)衡的問題。另一種方案來自淘寶綜合業(yè)務平臺團隊,它利用對2的倍數(shù)取余具有向前兼容的特性(如對4取余得1的數(shù)對2取余也是1)來分配數(shù)據(jù),避免了行級別的數(shù)據(jù)遷移,但是依然需要進行表級別的遷移,同時對擴容規(guī)模和分表數(shù)量都有限制。總得來說,這些方案都不是十分的理想,多多少少都存在一些缺點,這也從一個側(cè)面反映出了Sharding擴容的難度。
取長補短,兼容并包——一種理想的Sharding擴容方案
如前文所述,Sharding擴容與系統(tǒng)采用的路由規(guī)則密切相關(guān):基于散列的路由能均勻地分布數(shù)據(jù),但卻需要數(shù)據(jù)遷移,同時也無法避免對達到上限的節(jié)點不再寫入新數(shù)據(jù);基于增量區(qū)間的路由天然不存在數(shù)據(jù)遷移和向某一節(jié)點無上限寫入數(shù)據(jù)的問題,但卻存在“熱點”困擾。我們設(shè)計方案的初衷就是希望能結(jié)合兩種路由規(guī)則的優(yōu)勢,摒棄各自的劣勢,創(chuàng)造出一種接近“理想”狀態(tài)的擴容方式,而這種方式簡單概括起來就是:全局按增量區(qū)間分布數(shù)據(jù),使用增量擴容,無數(shù)據(jù)遷移,局部使用散列方式分散數(shù)據(jù)讀寫,解決“熱點”問題,同時對Sharding拓撲結(jié)構(gòu)進行建模,使用一致的路由算法,擴容時只需追加節(jié)點數(shù)據(jù),不再修改散列邏輯代碼。
原理
首先,作為方案的基石,為了能使系統(tǒng)感知到Shard并基于Shard的分布進行路由計算,我們需要建立一個可以描述Sharding拓撲結(jié)構(gòu)的編程模型。按照一般的切分原則,一個單一的數(shù)據(jù)庫會首先進行垂直切分,垂直切分只是將關(guān)系密切的表劃分在一起,我們把這樣分出的一組表稱為一個Partition。 接下來,如果Partition里的表數(shù)據(jù)量很大且增速迅猛,就再進行水平切分,水平切分會將一張表的數(shù)據(jù)按增量區(qū)間或散列方式分散到多個Shard上存儲。在我們的方案里,我們使用增量區(qū)間與散列相結(jié)合的方式,全局上,數(shù)據(jù)按增量區(qū)間分布,但是每個增量區(qū)間并不是按照某個Shard的存儲規(guī)模劃分的,而是根據(jù)一組Shard的存儲總量來確定的,我們把這樣的一組Shard稱為一個ShardGroup,局部上,也就是一個ShardGroup內(nèi),記錄會再按散列方式均勻分布到組內(nèi)各Shard上。這樣,一條數(shù)據(jù)的路由會先根據(jù)其ID所處的區(qū)間確定ShardGroup,然后再通過散列命中ShardGroup內(nèi)的某個目標Shard。在每次擴容時,我們會引入一組新的Shard,組成一個新的ShardGroup,為其分配增量區(qū)間并標記為“可寫入”,同時將原有ShardGroup標記為“不可寫入”,于是新生數(shù)據(jù)就會寫入新的ShardGroup,舊有數(shù)據(jù)不需要遷移。同時,在ShardGroup內(nèi)部各Shard之間使用散列方式分布數(shù)據(jù)讀寫,進而又避免了“熱點”問題。最后,在Shard內(nèi)部,當單表數(shù)據(jù)達到一定上限時,表的讀寫性能就開始大幅下滑,但是整個數(shù)據(jù)庫并沒有達到存儲和負載的上限,為了充分發(fā)揮服務器的性能,我們通常會新建多張結(jié)構(gòu)一樣的表,并在新表上繼續(xù)寫入數(shù)據(jù),我們把這樣的表稱為“分段表”(Fragment Table)。不過,引入分段表后所有的SQL在執(zhí)行前都需要根據(jù)ID將其中的表名替換成真正的分段表名,這無疑增加了實現(xiàn)Sharding的難度,如果系統(tǒng)再使用了某種ORM框架,那么替換起來可能會更加困難。目前很多數(shù)據(jù)庫提供一種與分段表類似的“分區(qū)”機制,但沒有分段表的副作用,團隊可以根據(jù)系統(tǒng)的實現(xiàn)情況在分段表和分區(qū)機制中靈活選擇。總之,基于上述切分原理,我們將得到如下Sharding拓撲結(jié)構(gòu)的領(lǐng)域模型:


圖1. Sharding拓撲結(jié)構(gòu)領(lǐng)域模型
在這個模型中,有幾個細節(jié)需要注意:ShardGroup的writable屬性用于標識該ShardGroup是否可以寫入數(shù)據(jù),一個Partition在任何時候只能有一個ShardGroup是可寫的,這個ShardGroup往往是最近一次擴容引入的;startId和endId屬性用于標識該ShardGroup的ID增量區(qū)間;Shard的hashValue屬性用于標識該Shard節(jié)點接受哪些散列值的數(shù)據(jù);FragmentTable的startId和endId是用于標識該分段表儲存數(shù)據(jù)的ID區(qū)間。
確立上述模型后,我們需要通過配置文件或是在數(shù)據(jù)庫中建立與之對應的表來存儲節(jié)點元數(shù)據(jù),這樣,整個存儲系統(tǒng)的拓撲結(jié)構(gòu)就可以被持久化起來,系統(tǒng)啟動時就能從配置文件或數(shù)據(jù)庫中加載出當前的Sharding拓撲結(jié)構(gòu)進行路由計算了,擴容時只需要向?qū)奈募虮碇屑尤胂嚓P(guān)的節(jié)點信息重啟系統(tǒng)即可,不需要修改任何路由邏輯代碼。
示例
讓我們通過示例來了解這套方案是如何工作的。
階段一:初始上線
假設(shè)某系統(tǒng)初始上線,規(guī)劃為某表提供4000W條記錄的存儲能力,若單表存儲上限為1000W條,單庫存儲上限為2000W條,共需2個Shard,每個Shard包含兩個分段表,ShardGroup增量區(qū)間為0-4000W,按2取余分散到2個Shard上,具體規(guī)劃方案如下:

圖2. 初始4000W存儲規(guī)模的規(guī)劃方案
與之相適應,Sharding拓撲結(jié)構(gòu)的元數(shù)據(jù)如下:

圖3. 對應Sharding元數(shù)據(jù)
階段二:系統(tǒng)擴容
經(jīng)過一段時間的運行,當原表總數(shù)據(jù)逼近4000W條上限時,系統(tǒng)就需要擴容了。為了演示方案的靈活性,我們假設(shè)現(xiàn)在有三臺服務器Shard2、Shard3、Shard4,其性能和存儲能力表現(xiàn)依次為Shard2<Shard3<Shard4,我們安排Shard2儲存1000W條記錄,Shard3儲存2000W條記錄,Shard4儲存3000W條記錄,這樣,該表的總存儲能力將由擴容前的4000W條提升到10000W條,以下是詳細的規(guī)劃方案:

圖4. 二次擴容6000W存儲規(guī)模的規(guī)劃方案
相應拓撲結(jié)構(gòu)表數(shù)據(jù)下:

圖5. 對應Sharding元數(shù)據(jù)
從這個擴容案例中我們可以看出該方案允許根據(jù)硬件情況進行靈活規(guī)劃,對擴容規(guī)模和節(jié)點數(shù)量沒有硬性規(guī)定,是一種非常自由的擴容方案。
增強
接下來讓我們討論一個高級話題:對“再生”存儲空間的利用。對于大多數(shù)系統(tǒng)來說,歷史數(shù)據(jù)較為穩(wěn)定,被更新或是刪除的概率并不高,反映到數(shù)據(jù)庫上就是歷史Shard的數(shù)據(jù)量基本保持恒定,但也不排除某些系統(tǒng)其數(shù)據(jù)有同等的刪除概率,甚至是越老的數(shù)據(jù)被刪除的可能性越大,這樣反映到數(shù)據(jù)庫上就是歷史Shard隨著時間的推移,數(shù)據(jù)量會持續(xù)下降,在經(jīng)歷了一段時間后,節(jié)點就會騰出很大一部分存儲空間,我們把這樣的存儲空間叫“再生”存儲空間,如何有效利用再生存儲空間是這些系統(tǒng)在設(shè)計擴容方案時需要特別考慮的。回到我們的方案,實際上我們只需要在現(xiàn)有基礎(chǔ)上進行一個簡單的升級就可以實現(xiàn)對再生存儲空間的利用,升級的關(guān)鍵就是將過去ShardGroup和FragmentTable的單一的ID區(qū)間提升為多重ID區(qū)間。為此我們把ShardGroup和FragmentTable的ID區(qū)間屬性抽離出來,分別用ShardGroupInterval和FragmentTableIdInterval表示,并和它們保持一對多關(guān)系。
[圖片上傳中。。。(11)]
圖6. 增強后的Sharding拓撲結(jié)構(gòu)領(lǐng)域模型
讓我們還是通過一個示例來了解升級后的方案是如何工作的。
階段三:不擴容,重復利用再生存儲空間
假設(shè)系統(tǒng)又經(jīng)過一段時間的運行之后,二次擴容的6000W條存儲空間即將耗盡,但是由于系統(tǒng)自身的特點,早期的很多數(shù)據(jù)被刪除,Shard0和Shard1又各自騰出了一半的存儲空間,于是ShardGroup0總計有2000W條的存儲空間可以重新利用。為此,我們重新將ShardGroup0標記為writable=true,并給它追加一段ID區(qū)間:10000W-12000W,進而得到如下規(guī)劃方案:

圖7. 重復利用2000W再生存儲空間的規(guī)劃方案
相應拓撲結(jié)構(gòu)的元數(shù)據(jù)如下:

圖8. 對應Sharding元數(shù)據(jù)
小結(jié)
這套方案綜合利用了增量區(qū)間和散列兩種路由方式的優(yōu)勢,避免了數(shù)據(jù)遷移和“熱點”問題,同時,它對Sharding拓撲結(jié)構(gòu)建模,使用了一致的路由算法,從而避免了擴容時修改路由代碼,是一種理想的Sharding擴容方案。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內(nèi)容