1. 程式人生 > >海量資料儲存

海量資料儲存

對於海量資料的處理
隨著網際網路應用的廣泛普及,海量資料的儲存和訪問成為了系統設計的瓶頸問題。對於一個大型的網際網路應用,每天幾十億的PV無疑對資料庫造成了相當高的負載。對於系統的穩定性和擴充套件性造成了極大的問題。通過資料切分來提高網站效能,橫向擴充套件資料層已經成為架構研發人員首選的方式。

水平切分資料庫:可以降低單臺機器的負載,同時最大限度的降低了宕機造成的損失;
負載均衡策略:可以降低單臺機器的訪問負載,降低宕機的可能性;
叢集方案:解決了資料庫宕機帶來的單點資料庫不能訪問的問題;
讀寫分離策略:最大限度了提高了應用中讀取資料的速度和併發量;

什麼是資料切分
"Shard" 這個詞英文的意思是"碎片",而作為資料庫相關的技術用語,似乎最早見於大型多人線上角色扮演遊戲中。"Sharding" 姑且稱之為"分片"。Sharding 不是一個某個特定資料庫軟體附屬的功能,而是在具體技術細節之上的抽象處理,是水平擴充套件(Scale Out,亦或橫向擴充套件、向外擴充套件)的解決方案,其主要目的是為突破單節點資料庫伺服器的 I/O 能力限制,解決資料庫擴充套件性問題。通過一系列的切分規則將資料水平分佈到不同的DB或table中,在通過相應的DB路由或者table路由規則找到需要查詢的具體的DB或者table,以進行Query操作。“sharding”通常是指“水平切分”,這也是本文討論的重點。接下來舉個簡單的例子:我們針對一個Blog應用中的日誌來說明,比如日誌文章(article)表有如下欄位:

面對這樣的一個表,我們怎樣切分呢?怎樣將這樣的資料分佈到不同的資料庫中的表中去呢?我們可以這樣做,將user_id為1~10000的所有的文章資訊放入DB1中的article表中,將user_id為10001~20000的所有文章資訊放入DB2中的 article表中,以此類推,一直到DBn。這樣一來,文章資料就很自然的被分到了各個資料庫中,達到了資料切分的目的。

接下來要解決的問題就是怎樣找到具體的資料庫呢?其實問題也是簡單明顯的,既然分庫的時候我們用到了區分欄位user_id,那麼很自然,資料庫路由的過程當然還是少不了user_id的。就是我們知道了這個blog的user_id,就利用這個user_id,利用分庫時候的規則,反過來定位具體的資料庫。比如user_id是234,利用剛才的規則,就應該定位到DB1,假如user_id是12343,利用該才的規則,就應該定位到DB2。以此類推,利用分庫的規則,反向的路由到具體的DB,這個過程我們稱之為“DB路由”。

平常我們會自覺的按照正規化來設計我們的資料庫,考慮到資料切分的DB設計,將違背這個通常的規矩和約束。為了切分,我們不得不在資料庫的表中出現冗餘欄位,用作區分欄位或者叫做分庫的標記欄位。比如上面的article的例子中的user_id這樣的欄位(當然,剛才的例子並沒有很好的體現出user_id的冗餘性,因為user_id這個欄位即使就是不分庫,也是要出現的,算是我們撿了便宜吧)。當然冗餘欄位的出現並不只是在分庫的場景下才出現的,在很多大型應用中,冗餘也是必須的,這個涉及到高效DB的設計,本文不再贅述。

為什麼要資料切分

上面對什麼是資料切分做了個概要的描述和解釋,讀者可能會疑問,為什麼需要資料切分呢?像 Oracle這樣成熟穩定的資料庫,足以支撐海量資料的儲存與查詢了?為什麼還需要資料切片呢?
的確,Oracle的DB確實很成熟很穩定,但是高昂的使用費用和高階的硬體支撐不是每一個公司能支付的起的。試想一下一年幾千萬的使用費用和動輒上千萬元的小型機作為硬體支撐,這是一般公司能支付的起的嗎?即使就是能支付的起,假如有更好的方案,有更廉價且水平擴充套件效能更好的方案,我們為什麼不選擇呢?

我們知道每臺機器無論配置多麼好它都有自身的物理上限,所以當我們應用已經能觸及或遠遠超出單臺機器的某個上限的時候,我們惟有尋找別的機器的幫助或者繼續升級的我們的硬體,但常見的方案還是橫向擴充套件,通過新增更多的機器來共同承擔壓力。我們還得考慮當我們的業務邏輯不斷增長,我們的機器能不能通過線性增長就能滿足需求?Sharding可以輕鬆的將計算,儲存,I/O並行分發到多臺機器上,這樣可以充分利用多臺機器各種處理能力,同時可以避免單點失敗,提供系統的可用性,進行很好的錯誤隔離。

綜合以上因素
,資料切分是很有必要的。 我們用免費的MySQL和廉價的Server甚至是PC做叢集,達到小型機+大型商業DB的效果,減少大量的資金投入,降低運營成本,何樂而不為呢?所以,我們選擇Sharding,擁抱Sharding。

怎麼做到資料切分
資料切分可以是物理上的,對資料通過一系列的切分規則將資料分佈到不同的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的對應DB2,以此類推;
優點:可部分遷移
缺點:資料分佈不均

(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,這個DB單獨儲存user_id到DB的對映關係,每次訪問資料庫的時候都要先查詢一次這個資料庫,以得到具體的DB資訊,然後才能進行我們需要的查詢操作。
優點:靈活性強,一對一關係
缺點:每次查詢之前都要多一次查詢,效能大打折扣

以上就是通常的開發中我們選擇的三種方式,有些複雜的專案中可能會混合使用這三種方式。 通過上面的描述,我們對分庫的規則也有了簡單的認識和了解。當然還會有更好更完善的分庫方式,還需要我們不斷的探索和發現。

分散式資料方案提供功能如下:
(1)提供分庫規則和路由規則(RouteRule簡稱RR);
(2)引入叢集(Group)的概念,保證資料的高可用性;
(3)引入負載均衡策略(LoadBalancePolicy簡稱LB);
(4)引入叢集節點可用性探測機制,對單點機器的可用性進行定時的偵測,以保證LB策略的正確實施,以確保系統的高度穩定性;
(5)引入讀/寫分離,提高資料的查詢速度;

僅僅是分庫分表的資料層設計也是不夠完善的,當我們採用了資料庫切分方案,也就是說有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分配負載。這樣一來,問題就出來了,客戶端很自然的就會發生資料操作失敗的錯誤或者異常。

這樣是非常不友好的!怎樣解決這樣的問題呢? 我們引入叢集節點的可用性探測機制 ,或者是可用性的資料推送機制。這兩種機制有什麼不同呢?首先說探測機制吧,顧名思義,探測即使,就是我的資料層客戶端,不定時對叢集中各個資料庫進行可用性的嘗試,實現原理就是嘗試性連結,或者資料庫埠的嘗試性訪問,都可以做到。

那資料推送機制又是什麼呢?其實這個就要放在現實的應用場景中來討論這個問題了,一般情況下應用的DB 資料庫宕機的話我相信DBA肯定是知道的,這個時候DBA手動的將資料庫的當前狀態通過程式的方式推送到客戶端,也就是分散式資料層的應用端,這個時候在更新一個本地的DB狀態的列表。並告知LB,這個資料庫節點不能使用,請不要給它分配負載。一個是主動的監聽機制,一個是被動的被告知的機制。兩者各有所長。但是都可以達到同樣的效果。這樣一來剛才假設的問題就不會發生了,即使就是發生了,那麼發生的概率也會降到最低。

上面的文字中提到的Master和Slave ,我們並沒有做太多深入的講解。一個Group由1個Master和N個Slave組成。為什麼這麼做呢?其中Master負責寫操作的負載,也就是說一切寫的操作都在Master上進行,而讀的操作則分攤到Slave上進行。這樣一來的可以大大提高讀取的效率。在一般的網際網路應用中,經過一些資料調查得出結論,讀/寫的比例大概在 10:1左右 ,也就是說大量的資料操作是集中在讀的操作,這也就是為什麼我們會有多個Slave的原因。

但是為什麼要分離讀和寫呢?熟悉DB的研發人員都知道,寫操作涉及到鎖的問題,不管是行鎖還是表鎖還是塊鎖,都是比較降低系統執行效率的事情。我們這樣的分離是把寫操作集中在一個節點上,而讀操作其其他 的N個節點上進行,從另一個方面有效的提高了讀的效率,保證了系統的高可用性。

一致性hash
一致性hash演算法提出了在動態變化的Cache環境中,判定雜湊演算法好壞的四個定義:
1、平衡性(Balance):平衡性是指雜湊的結果能夠儘可能分佈到所有的緩衝中去,這樣可以使得所有的緩衝空間都得到利用。很多雜湊演算法都能夠滿足這一條件。
2、單調性(Monotonicity):單調性是指如果已經有一些內容通過雜湊分派到了相應的緩衝中,又有新的緩衝加入到系統中。雜湊的結果應能夠保證原有已分配的內容可以被對映到原有的或者新的緩衝中去,而不會被對映到舊的緩衝集合中的其他緩衝區。 
3、分散性(Spread):在分散式環境中,終端有可能看不到所有的緩衝,而是隻能看到其中的一部分。當終端希望通過雜湊過程將內容對映到緩衝上時,由於不同終端所見的緩衝範圍有可能不同,從而導致雜湊的結果不一致,最終的結果是相同的內容被不同的終端對映到不同的緩衝區中。這種情況顯然是應該避免的,因為它導致相同內容被儲存到不同緩衝中去,降低了系統儲存的效率。分散性的定義就是上述情況發生的嚴重程度。好的雜湊演算法應能夠儘量避免不一致的情況發生,也就是儘量降低分散性。 
4、負載(Load):負載問題實際上是從另一個角度看待分散性問題。既然不同的終端可能將相同的內容對映到不同的緩衝區中,那麼對於一個特定的緩衝區而言,也可能被不同的使用者對映為不同 的內容。與分散性一樣,這種情況也是應當避免的,因此好的雜湊演算法應能夠儘量降低緩衝的負荷。

在分散式叢集中,對機器的新增刪除,或者機器故障後自動脫離叢集這些操作是分散式叢集管理最基本的功能。如果採用常用的hash(object)%N演算法,那麼在有機器新增或者刪除後,很多原有的資料就無法找到了,這樣嚴重的違反了單調性原則。

什麼是叢集
叢集是一組協同工作的服務實體,用以提供比單一服務實體更具擴充套件性與可用性的服務平臺。在客戶端看來,一個叢集就象是一個服務實體,但 事實上叢集由一組服務實體組成。

叢集的特性 
與單一服務實體相比較,叢集提供了以下兩個關鍵特性: 
1.可擴充套件性--叢集的效能不限於單一的服務實體,新的服 務實體可以動態地加入到叢集,從而增強叢集的效能。 
2. 高可用性--叢集通過服務實體冗餘使客戶端免於輕易遇到out of service的警告。在叢集中,同樣的服務可以由多個服務實體提供。如果一個服務實體失敗了,另一個服務實體會接管失敗的服務實體。叢集提供的從一個出 錯的服務實體恢復到另一個服務實體的功能增強了應用的可用性。 
為了具有可擴充套件性和高可用性特點,叢集的必須具備以下兩大能力: 
(1) 負 載均衡--負載均衡能把任務比較均衡地分佈到叢集環境下的計算和網路資源。 
(2) 錯誤恢復--由於某種原因,執行某個任務的資源出現故障,另一服 務實體中執行同一任務的資源接著完成任務。這種由於一個實體中的資源不能工作,另一個實體中的資源透明的繼續完成任務的過程叫錯誤恢復。 
負載均衡 和錯誤恢復都要求各服務實體中有執行同一任務的資源存在,而且對於同一任務的各個資源來說,執行任務所需的資訊檢視(資訊上下文)必須是一樣的。

叢集的分類 
叢集主要分成三大類:高可用叢集(High Availability Cluster/HA), 負載均衡叢集(Load Balance Cluster),高效能運算叢集(High Performance Computing Cluster/HPC) 
(1) 高可用叢集(High Availability Cluster/HA):一般是指當叢集中有某個節點失效的情況下,其上的任務會自動轉移到其他正常的節點上。還指可以將叢集中的某節點進行離線維護再上線,該過程並不影響整個叢集的執行。常見的就是2個節點做 成的HA叢集,有很多通俗的不科學的名稱,比如"雙機熱備", "雙機互備", "雙機",高可用叢集解決的是保障使用者的應用程式持續對外提供服 務的能力。
(2) 負載均衡叢集(Load Balance Cluster):負載均衡叢集執行時一般通過一個或者多個前端負載均衡器將工作負載分發到後端的一組伺服器上,從而達到將工作負載分發。這樣的計算機叢集有時也被稱為伺服器群(Server Farm)。一般web伺服器叢集、資料庫叢集 和應用伺服器叢集都屬於這種型別。這種叢集可以在接到請求時,檢查接受請求較少,不繁忙的伺服器,並把請求轉到這些伺服器 上。從檢查其他伺服器狀態這一點上 看,負載均衡和容錯叢集很接近,不同之處是數量上更多。 
(3) 高效能運算叢集(High Performance Computing Cluster/HPC):高效能運算叢集採用將計算任務分配到叢集的不同計算節點而提高計算能力,因而主要應用在科學計算領域。這類叢集致力於提供單個計算機所不能提供的強大的計算能力