一、引言
隨著網路應用的廣泛普及,海量資料的儲存和存取成為了系統設計的瓶頸問題。對於一個大型的網路應用,每天數十億的PV無疑對資料庫造成了相當高的負載。對於系統的穩定性和擴展性造成了極大的問題。透過資料切分來提升網站效能,橫向擴展資料層已成為架構研發人員首選的方式。水平切分資料庫,可以降低單一機器的負載,同時最大限度的降低了當機造成的損失。透過負載平衡策略,有效的降低了單一機器的存取負載,降低了宕機的可能性;透過叢集方案,解決了資料庫宕機帶來的單點資料庫無法存取的問題;透過讀寫分離策略更是最大限度了提高了應用程式中讀取(Read)資料的速度和並發量。目前國內的大型網路應用中,大量的採用了這樣的資料切分方案,Taobao,Alibaba,Tencent,它們大都實現了自己的分散式資料存取層(DDAL)。以實作方式和實作的層次來劃分,大概分為兩個層次(Java應用為例):JDBC層的封裝,ORM框架層的實作。就JDBC層的直接封裝而言,現在國內發展較好的一個項目是被稱作“變形蟲”(Amoeba)的項目,由阿里集團的研究院開發,現在仍然處於測試階段(beta版),其運作效率和生產時效性有待考究。就ORM框架層的實作而言,例如Taobao的基於ibatis和Spring的分佈式資料存取層,已有多年的應用,運作效率和生產實效性得到了開發人員和使用者的肯定。本文就是以ORM框架層為基礎而實現的分散式資料存取層。本課題的困難在於分庫後,路由規則的發展與選擇以及後期的擴展性,例如:如何做到用最少的資料遷移量,達到擴充資料庫容量(增加機器節點)的目的。核心問題將圍繞著資料庫分庫分錶的路由規則和負載平衡策略展開。
二、基本原理和概念
2.1基本原理:
人類認知問題的過程總是這樣的:what(什麼)-? why(為什麼)-? how(怎麼做),接下來,本文將就這三個問題展開討論和研究:
2.1.1什麼是資料切分
"Shard"這個字英文的意思是"碎片",而作為資料庫相關的技術用語,似乎最早見於大型多人線上角色扮演遊戲。 "Sharding"姑且稱為"分片"。 Sharding不是新技術,而是一個相對簡樸的軟體理念。眾所周知,MySQL5之後才有了資料表分區功能,那麼在此之前,許多MySQL的潛在使用者都對MySQL的擴展性有所顧慮,而是否具備分區功能就成了衡量一個資料庫可擴充性與否的一個關鍵指標(當然不是唯一指標)。資料庫擴展性是一個永恆的話題,MySQL的推廣者經常會被問到:如在單一資料庫上處理應用資料捉襟見肘而需要進行分區化之類的處理,是如何辦到的呢?答案是:Sharding。 Sharding不是某個特定資料庫軟體附屬的功能,而是在具體技術細節之上的抽象處理,是水平擴展(ScaleOut,亦或橫向擴展、向外擴展)的解決方案,其主要目的是為突破單節點資料庫伺服器的I/O能力限制,解決資料庫擴充性問題。
透過一系列的切分規則將資料水平分佈到不同的DB或table中,在透過對應的DB路由或table路由規則找到需要查詢的特定的DB或table,以進行Query操作。這裡所說的“sharding”通常是指“水平切分”,這也是本文討論的重點。具體將有什麼樣的切分方式呢和路由方式呢?行文至此,讀者難免有所疑問,接下來舉個簡單的例子:我們針對一個Blog應用中的日誌來說明,例如日誌文章(article)表有如下字段:
article_id(int),title(varchar( 128)),content(varchar(1024)),user_id(int)
面對這樣的一個表,我們要如何切分呢?怎樣將這樣的資料分佈到不同的資料庫中的表格中去呢?其實分析blog的應用,我們不難得出這樣的結論:blog的應用程式中,使用者分為兩種:瀏覽者和blog的主人。瀏覽者瀏覽某個blog,實際上是在一個特定的用戶的blog下進行瀏覽的,而blog的主人管理自己的blog,也同樣是在特定的用戶blog下進行操作的(在自己的空間下) 。所謂的特定的用戶,用資料庫的欄位表示就是「user_id」。就是這個“user_id”,它就是我們需要的分庫的依據和規則的基礎。我們可以這樣做,將user_id為1~10000的所有的文章資訊放入DB1中的article表中,將user_id為10001~20000的所有文章資訊放入DB2中的article表中,以此類推,一直到DBn。這樣一來,文章資料就很自然的被分到了各個資料庫中,達到了資料切分的目的。接下來要解決的問題就是要怎麼找到具體的資料庫呢?其實問題也是簡單明顯的,既然分庫的時候我們用到了區分字段user_id,那麼很自然,資料庫路由的過程當然還是少不了user_id的。想想我們剛剛呈現的blog應用,不管是訪問別人的blog還是管理自己的blog,總之我都要知道這個blog的使用者是誰吧,也就是我們知道了這個blog的user_id,就利用這個user_id,利用分庫時候的規則,反過來定位具體的資料庫,例如user_id是234,利用該才的規則,就應該定位到DB1,假如user_id是12343,利用該才的規則,就應該定位到DB2。以此類推,利用分庫的規則,反向的路由到特定的DB,這個過程我們稱之為「DB路由」。
當然考慮到資料切分的DB設計必然是非常規,不正統的DB設計。那麼什麼樣的DB設計是正統的DB設計呢?
我們平常規規矩矩用的基本都是。平常我們會自覺的按照範式來設計我們的資料庫,負載高點可能考慮使用相關的Replication機制來提高讀寫的吞吐和效能,這可能已經可以滿足很多需求,但這套機製本身的缺陷還是比較顯而易見的(下文會提及)。上面提到的「自覺的按照範式設計」。考慮到資料切分的DB設計,將違反這個通常的規矩和約束,為了切分,我們不得不在資料庫的表中出現冗餘字段,用作區分字段或者叫做分庫的標記字段,例如上面的article的例子中的user_id這樣的欄位(當然,剛才的例子並沒有很好的體現出user_id的冗餘性,因為user_id這個欄位即使就是不分庫,也是要出現的,算是我們撿了便宜吧)。當然冗餘欄位的出現不只是在分庫的場景下才出現的,在許多大型應用中,冗餘也是必須的,這個涉及到高效DB的設計,本文不再贅述。
2.1.2為什麼要資料切分
上面對什麼是資料切分做了個概要的描述和解釋,讀者可能會疑問,為什麼需要資料切分呢?像Oracle這樣成熟穩定的資料庫,足以支撐海量資料的儲存與查詢了?為什麼還需要資料切片呢?的確,Oracle的DB確實很成熟很穩定,但是高昂的使用費用和高端的硬體支撐不是每個公司能支付的起的。試想一年幾千萬的使用費用和動輒上千萬元的小型機作為硬體支撐,這是一般公司能支付的起的嗎?即使就是能支付的起,假如有更好的方案,有更廉價且水平擴展性能更好的方案,我們為什麼不選擇呢?
但是,事情總是不盡人意。平常我們會自覺的按照範式來設計我們的資料庫,負載高點可能考慮使用相關的Replication機制來提高讀寫的吞吐和效能,這可能已經可以滿足很多需求,但這套機製本身的缺陷還是比較顯而易見的。首先它的有效很依賴於讀取操作的比例,Master往往會成為瓶頸所在,寫入操作需要順序排隊來執行,過載的話Master首先扛不住,Slaves的資料同步的延遲也可能比較大,而且會大大耗費CPU的運算能力,因為write操作在Master上執行以後還是需要在每台slave機器上都跑一次。這時候Sharding可能會成為雞肋了。 Replication搞不定,那為什麼Sharding可以運作呢?道理很簡單,因為它可以很好的擴充。我們知道每台機器無論配置多麼好它都有自身的物理上限,所以當我們應用已經能觸及或遠遠超出單台機器的某個上限的時候,我們惟有尋找別的機器的幫助或者繼續升級的我們的硬件,但常見的方案還是橫向擴展,通過添加更多的機器來共同承擔壓力。我們還得考慮當我們的業務邏輯不斷成長,我們的機器能不能透過線性成長就能滿足需求? Sharding可以輕鬆的將計算,存儲,I/O並行分發到多台機器上,這樣可以充分利用多台機器各種處理能力,同時可以避免單點失敗,提供系統的可用性,進行很好的錯誤隔離。
綜合以上因素,資料切分是很有必要的,且我們在此討論的資料切分也是將MySql作為背景的。基於成本的考慮,許多公司也選擇了Free且Open的MySql。對MySql有所了解的開發人員可能會知道,MySQL5之後才有了資料表分區功能,那麼在此之前,很多MySQL的潛在用戶都對MySQL的擴展性有所顧慮,而是否具備分區功能就成了衡量一個資料庫可擴展性與否的一個關鍵指標(當然不是唯一指標)。資料庫擴展性是一個永恆的話題,MySQL的推廣者常常被問到:如在單一資料庫上處理應用資料捉襟見肘而需要進行分區化之類的處理,是如何辦到的呢?答案也是Sharding,也就是我們所說的數據切分方案。
我們用免費的MySQL和廉價的Server甚至是PC做集群,達到小型機+大型商業DB的效果,減少大量的資金投入,降低運營成本,何樂而不為呢?所以,我們選擇Sharding,擁抱Sharding。
2.1.3怎麼做到資料切分
說到資料切分,再次我們講對資料切分的方法和形式進行比較詳細的闡述和說明。
資料切分可以是物理上的,對資料透過一系列的切分規則將資料分佈到不同的DB伺服器上,透過路由規則路由存取特定的資料庫,這樣一來每次存取面對的就不是單一伺服器了,而是N台伺服器,這樣就可以降低單一機器的負載壓力。
資料切分也可以是資料庫內的,對資料通過一系列的切分規則,將資料分佈到一個資料庫的不同表中,例如將article分為article_001,article_002等子表,若干個子表層級拼合有組成了邏輯上一個完整的article表,這樣做的目的其實也是很簡單的。舉個例子說明,例如article表中現在有5000w條數據,此時我們需要在這個表中增加(insert)一條新的數據,insert完畢後,資料庫會針對這張表重新建立索引,5000w行數據建立索引的系統開銷還是不容忽視的。但反過來,假如我們將這個表分成100個table呢,從article_001一直到article_100,5000w行數據平均下來,每個子表裡邊就只有50萬行數據,這時候我們向一張只有50w行數據的table中insert資料後建立索引的時間就會呈現數量級的下降,大大提高了DB的運行時效率,並提高了DB的並發量。當然分錶的好處還不知這些,還有諸如寫入操作的鎖定操作等,都會帶來許多顯然的好處。
綜上,分庫降低了單點機器的負載;分錶,提高了資料操作的效率,尤其是Write操作的效率。行文至此我們依然沒有涉及到如何切分的問題。接下來,我們將對切分規則進行詳盡的闡述和說明。
上文中提到,要想做到數據的水平切分,在每一個表中都要有相冗餘字符作為切分依據和標記字段,通常的應用中我們選用user_id作為區分字段,基於此就有以下三種分庫的方式與規則:(當然還可以有其他的方式)
按號段分:
(1)user_id為區分,1~1000的對應DB1,1001~2000的user_id為區分,1~1000的對應DB1,1001~2000對應的。 ,以此類推;
優點:可部分遷移
缺點:資料分佈不均
(2)hash取模分:
對user_id進行hash(或者如果user_id是數值型的話直接使用user_id的值也可),然後用一個特定的數字,比如應用中需要將一個數據庫切分成4個數據庫的話,我們就用4這個數字對user_id的hash值進行取模運算,也就是user_id%4,這樣的話每次運算就有四種可能:結果為1的時候對應DB1;結果為2的時候對應DB2;結果為3的時候對應DB3;結果為0的時候對應DB4,這樣一來就非常均勻的將資料分配到4個DB中。
優點:資料分佈均勻
缺點:資料遷移的時候麻煩,不能依照機器效能分攤資料
(3)在認證庫中儲存資料庫配置到映射關係,每次訪問資料庫的時候都要先查詢一次這個資料庫,以獲得具體的DB信息,然後才能進行我們需要的查詢操作。
優點:靈活性強,一對一關係
缺點:每次查詢之前都要多一次查詢,性能大打折扣
以上就是通常的開發中我們選擇的三種方式,有些複雜的項目中可能會混合使用這三種方式。透過上面的描述,我們對分庫的規則也有了簡單的認識與了解。當然還會有更好、更完善的分庫方式,也需要我們不斷的探索與發現。
三、本討論思路的基本輪廓
上面的文字,我們按照人類認知事物的規律,what?why?how這樣的方式闡述了數據庫切分的一些概念和意義以及對一些常規的切分規則做了概要的介紹。本課題所討論的分佈資料層並非僅如此,它是一個完整的資料層解決方案,它到底是什麼樣的呢?接下來的文字,我將詳細闡述本研究主題的完整想法和實現方式。
分散式資料方案提供功能如下:
(1)提供分庫規則和路由規則(RouteRule簡稱RR),將上面的說明中提到的三中切分規則直接內嵌入本系統,具體的嵌入方式在接下來的內容中進行詳細的說明和論述;
(2)引入集群(Group)的概念,保證數據的高可用性;
(3)引入負載平衡策略(LoadBalancePolicy簡稱LB);
LB); (4)引入群集節點可用性探測機制,對單點機器的可用性進行定時的偵測,以確保LB策略的正確實施,以確保系統的高度穩定性; (5)引入讀取/寫入分離,提高資料的查詢速度; 僅是分庫分錶的資料層設計也是不夠完善的,當某個節點上的DB伺服器出現了宕機的情況的時候,會是什麼樣的呢?是的,我們採用了資料庫切分方案,也就是說有N太機器組成了一個完整的DB,如果有一台機器宕機的話,也僅僅是一個DB的N分之一的資料不能存取而已,這是我們能接受的,起碼比切分之前的情況好很多了,總不至於整個DB都不能訪問。在一般的應用中,這樣的機器故障導致的資料無法存取是可以接受的,假設我們的系統是一個高並發的電子商務網站呢?單節點機器宕機帶來的經濟損失是非常嚴重的。也就是說,現在我們這樣的方案還是有問題的,容錯性能是經不起考驗的。當然了,問題總是有解決方案的。我們引入叢集的概念,在此我稱之為Group,也就是每一個分攤的節點我們引入多台機器,每台機器保存的資料是一樣的,一般情況下這多台機器分攤負載,當出現宕機情況,負載平衡器將分配負載給這台宕機的機器。這樣一來,就解決了容錯性的問題。所以我們引入了集群的概念,並將其內嵌入我們的框架中,成為框架的一部分。如上圖所示,整個資料層有Group1,Group2,Group3三個集群組成,這三個集群就是資料水平切分的結果,當然這三個集群也就組成了一個包含完整數據的DB。每一個Group包含1個Master(當然Master也可以是多個)和N個Slave,這些Master和Slave的資料是一致的。例如Group1中的一個slave發生了宕機現象,那麼還有兩個slave是可以用的,這樣的模型總是不會造成某部分資料不能存取的問題,除非整個Group裡的機器全部宕掉,但是考慮到這樣的事情發生的機率非常小(除非是斷電了,否則不容易發生)。
在沒有引入集群以前,我們的一次查詢的過程大致如下:請求資料層,並傳遞必要的分庫區分字段(通常情況下是user_id)?資料層根據區分字段Route到特定的DB?在這個確定的DB內進行資料操作。這是沒有引入集群的情況,當時引入集群會是什麼樣子的?看圖一即可得知,我們的路由器上規則和策略其實只能路由到具體的Group,也就是只能路由到一個虛擬的Group,這個Group並不是某個特定的實體伺服器。接下來要做的工作就是找到具體的實體的DB伺服器,以進行具體的資料操作。基於這個環節的需求,我們引入了負載平衡器的概念(LB)。負載平衡器的職責就是定位到一台具體的DB伺服器。具體的規則如下:負載平衡器會分析目前sql的讀寫特性,如果是寫操作或是要求即時性很強的操作的話,直接將查詢負載分到Master,如果是讀操作則透過負載平衡策略分配一個Slave。我們的負載平衡器的主要研究放向也就是負載分發策略,通常負載平衡包括隨機負載平衡和加權負載平衡。隨機負載平衡很好理解,就是從N個Slave中隨機選取一個Slave。這樣的隨機負載平衡是不考慮機器效能的,它預設為每台機器的效能是一樣的。假如真實的情況是這樣的,這樣做也是無可厚非的。假如實際情況並非如此呢?每個Slave的機器物理性能和配置不一樣的情況,再使用隨機的不考慮性能的負載均衡,是非常不科學的,這樣一來會給機器性能差的機器帶來不必要的高負載,甚至帶來宕機的危險,同時高效能的資料庫伺服器也無法充分發揮其實體效能。基於此考慮從,我們引入了加權負載平衡,也就是在我們的系統內部通過一定的接口,可以給每台DB伺服器分配一個權值,然後再運行時LB根據權值在集群中的比重,分配一定比例的負載給該DB伺服器。當然這樣的概念的引入,無疑地增大了系統的複雜性和可維護性。有得必有失,我們也沒有辦法逃過的。
有了分庫,有了集群,有了負載平衡器,是不是就萬事大吉了呢?事情遠沒有我們想像的那麼簡單。雖然有了這些東西,基本上可以保證我們的資料層可以承受很大的壓力,但是這樣的設計並不能完全規避資料庫宕機的危害。假如Group1中的slave2宕機了,那麼系統的LB並不能得知,這樣的話其實是很危險的,因為LB不知道,它還會以為slave2為可用狀態,所以還是會給slave2分配負載。這樣一來,問題就出來了,客戶端很自然的就會發生資料操作失敗的錯誤或是異常。這樣是非常不友善的!怎樣解決這樣的問題呢?我們引入叢集節點的可用性探測機制,或是可用性的資料推送機制。這兩種機制有什麼不同呢?首先說探測機製吧,顧名思義,探測即使,就是我的數據層客戶端,不定時對集群中各個數據庫進行可用性的嘗試,實現原理就是嘗試性鏈接,或者數據庫端口的嘗試性訪問,都可以做到,當然也可以用JDBC嘗試性鏈接,利用Java的Exception機制進行可用性的判斷,具體的會在後面的文字中提到。那數據推送機制又是什麼呢?其實這個就要放在現實的應用場景中來討論這個問題了,一般情況下應用的DB數據庫宕機的話我相信DBA肯定是知道的,這個時候DBA手動的將數據庫的當前狀態通過程序的方式推送到客戶端,也就是分散式資料層的應用端,這個時候在更新一個本地的DB狀態的清單。並告知LB,這個資料庫節點不能使用,請不要給它分配負載。一個是主動的監聽機制,一個是被動的被告知的機制。兩者各有所長。但是都可以達到相同的效果。這樣一來剛才假設的問題就不會發生了,即使就是發生了,那麼發生的機率也會降到最低。
上面的文字中提到的Master和Slave,我們並沒有做太多深入的講解。如圖一所示,一個Group由1個Master和N個Slave組成。為什麼要這麼做呢?其中Master負責寫入操作的負載,也就是說一切寫的操作都在Master上進行,而讀的操作則分攤到Slave上進行。這樣一來的可以大大提高讀取的效率。在一般的網路應用中,經過一些數據調查得出結論,讀/寫的比例大概在10:1左右,也就是說大量的數據操作是集中在讀的操作,這也就是為什麼我們會有多個Slave的原因。但是為什麼要分離讀寫呢?熟悉DB的研發人員都知道,寫入作業牽涉到鎖的問題,不管是行鎖還是表鎖或區塊鎖,都是比較降低系統執行效率的事情。我們這樣的分離是把寫入操作集中在一個節點上,而讀取操作其其他的N個節點上進行,從另一個方面有效的提高了讀取的效率,保證了系統的高可用性。讀寫分離也會引入新的問題,例如我的Master上的資料怎麼樣和叢集中其他的Slave機器保持資料的同步和一致呢?這個是我們不需要過多的關注的問題,MySql的Proxy機制可以幫助我們做到這一點,由於Proxy機制與本主題相關性不是太強,在這裡不做詳細介紹。
綜上所述,本課題中所研究的分佈式資料層的大體功能就是如此。