前言:node
本文主要討論數據分片的三個問題:(1)如何作數據分片,即如何將數據映射到節點;(2)數據分片的特徵值,即按照數據中的哪個屬性(字段)來分片;(3)數據分片的元數據的管理,如何保證元數據服務器的高性能、高可用,若是是一組服務器,如何保證強一致性。python
正文算法
在前文中,提出了分佈式系統(尤爲是分佈式存儲系統)須要解決的兩個最主要的問題,即數據分片和數據冗餘,下面這個圖片(來源)形象生動的解釋了其概念和區別:sql
其中數據即A、B屬於數據分片,原始數據被拆分紅兩個正交子集分佈在兩個節點上。而數據集C屬於數據冗餘,同一份完整的數據在兩個節點都有存儲。固然,在實際的分佈式系統中,數據分片和數據冗餘通常都是共存的。mongodb
本文主要討論數據分片的三個問題:數據庫
所謂分佈式系統,就是利用多個獨立的計算機來解決單個節點(計算機)沒法處理的存儲、計算問題,這是很是典型的分而治之的思想。每一個節點只負責原問題(即整個系統須要完成的任務)的一個子集,那麼原問題如何拆分到多個節點?在分佈式存儲系統中,任務的拆分即數據分片。編程
何爲數據分片(segment,fragment, shard, partition),就是按照必定的規則,將數據集劃分紅相互獨立、正交的數據子集,而後將數據子集分佈到不一樣的節點上。注意,這裏提到,數據分片須要按照必定的規則,不一樣的分佈式應用有不一樣的規則,但都遵循一樣的原則:按照最主要、最頻繁使用的訪問方式來分片。緩存
三種數據分片方式性能優化
首先介紹三種分片方式:hash方式,一致性hash(consistent hash),按照數據範圍(range based)。對於任何方式,都須要思考如下幾個問題:服務器
爲了後面分析不一樣的數據分片方式,假設有三個物理節點,編號爲N0, N1, N2;有如下幾條記錄:
hash方式:
哈希表(散列表)是最爲常見的數據結構,根據記錄(或者對象)的關鍵值將記錄映射到表中的一個槽(slot),便於快速訪問。絕大多數編程語言都有對hash表的支持,如python中的dict, C++中的map,Java中的Hashtable, Lua中的table等等。在哈希表中,最爲簡單的散列函數是 mod N(N爲表的大小)。即首先將關鍵值計算出hash值(這裏是一個整型),經過對N取餘,餘數即在表中的位置。
數據分片的hash方式也是這個思想,即按照數據的某一特徵(key)來計算哈希值,並將哈希值與系統中的節點創建映射關係,從而將哈希值不一樣的數據分佈到不一樣的節點上。
咱們選擇id做爲數據分片的key,那麼各個節點負責的數據以下:
由此能夠看到,按照hash方式作數據分片,映射關係很是簡單;須要管理的元數據也很是之少,只須要記錄節點的數目以及hash方式就好了。
但hash方式的缺點也很是明顯:當加入或者刪除一個節點的時候,大量的數據須要移動。好比在這裏增長一個節點N3,所以hash方式變爲了mod 4,數據的遷移以下:
在這種方式下,是不知足單調性(Monotonicity)的:若是已經有一些內容經過哈希分派到了相應的緩衝中,又有新的緩衝加入到系統中。哈希的結果應可以保證原有已分配的內容能夠被映射到原有的或者新的緩衝中去,而不會被映射到舊的緩衝集合中的其餘緩衝區。
在工程中,爲了減小遷移的數據量,節點的數目能夠成倍增加,這樣機率上來說至多有50%的數據遷移。
hash方式還有一個缺點,即很難解決數據不均衡的問題。有兩種狀況:原始數據的特徵值分佈不均勻,致使大量的數據集中到一個物理節點上;第二,對於可修改的記錄數據,單條記錄的數據變大。在這兩種狀況下,都會致使節點之間的負載不均衡,並且在hash方式下很難解決。
一致性hash
一致性hash是將數據按照特徵值映射到一個首尾相接的hash環上,同時也將節點(按照IP地址或者機器名hash)映射到這個環上。對於數據,從數據在環上的位置開始,順時針找到的第一個節點即爲數據的存儲節點。這裏仍然以上述的數據爲例,假設id的範圍爲[0, 1000],N0, N1, N2在環上的位置分別是100, 400, 800,那麼hash環示意圖與數據的分佈以下:
能夠看到相比於上述的hash方式,一致性hash方式須要維護的元數據額外包含了節點在環上的位置,但這個數據量也是很是小的。
一致性hash在增長或者刪除節點的時候,受到影響的數據是比較有限的,好比這裏增長一個節點N3,其在環上的位置爲600,所以,原來N2負責的範圍段(400, 800]如今由N3(400, 600] N2(600, 800]負責,所以只須要將記錄R7(id:533) 從N2,遷移到N3:
不難發現一致性hash方式在增刪的時候只會影響到hash環上響應的節點,不會發生大規模的數據遷移。
可是,一致性hash方式在增長節點的時候,只能分攤一個已存在節點的壓力;一樣,在其中一個節點掛掉的時候,該節點的壓力也會被所有轉移到下一個節點。咱們但願的是「一方有難,八方支援」,所以須要在增刪節點的時候,已存在的全部節點都能參與響應,達到新的均衡狀態。
所以,在實際工程中,通常會引入虛擬節點(virtual node)的概念。即不是將物理節點映射在hash換上,而是將虛擬節點映射到hash環上。虛擬節點的數目遠大於物理節點,所以一個物理節點須要負責多個虛擬節點的真實存儲。操做數據的時候,先經過hash環找到對應的虛擬節點,再經過虛擬節點與物理節點的映射關係找到對應的物理節點。
引入虛擬節點後的一致性hash須要維護的元數據也會增長:第一,虛擬節點在hash環上的問題,且虛擬節點的數目又比較多;第二,虛擬節點與物理節點的映射關係。但帶來的好處是明顯的,當一個物理節點失效是,hash環上多個虛擬節點失效,對應的壓力也就會發散到多個其他的虛擬節點,事實上也就是多個其他的物理節點。在增長物理節點的時候一樣如此。
工程中,Dynamo、Cassandra都使用了一致性hash算法,且在比較高的版本中都使用了虛擬節點的概念。在這些系統中,須要考慮綜合考慮數據分佈方式和數據副本,當引入數據副本以後,一致性hash方式也須要作相應的調整, 能夠參加cassandra的相關文檔。
range based
簡單來講,就是按照關鍵值劃分紅不一樣的區間,每一個物理節點負責一個或者多個區間。其實這種方式跟一致性hash有點像,能夠理解爲物理節點在hash環上的位置是動態變化的。
仍是以上面的數據舉例,三個節點的數據區間分別是N0(0, 200], N1(200, 500], N2(500, 1000]。那麼數據分佈以下:
注意,區間的大小不是固定的,每一個數據區間的數據量與區間的大小也是沒有關係的。好比說,一部分數據很是集中,那麼區間大小應該是比較小的,即以數據量的大小爲片斷標準。在實際工程中,一個節點每每負責多個區間,每一個區間成爲一個塊(chunk、block),每一個塊有一個閾值,當達到這個閾值以後就會分裂成兩個塊。這樣作的目的在於當有節點加入的時候,能夠快速達到均衡的目的。
不知道讀者有沒有發現,若是一個節點負責的數據只有一個區間,range based與沒有虛擬節點概念的一致性hash很相似;若是一個節點負責多個區間,range based與有虛擬節點概念的一致性hash很相似。
range based的元數據管理相對複雜一些,須要記錄每一個節點的數據區間範圍,特別單個節點對於多個區間的狀況。並且,在數據可修改的狀況下,若是塊進行分裂,那麼元數據中的區間信息也須要同步修改。
range based這種數據分片方式應用很是普遍,好比MongoDB, PostgreSQL, HDFS
小結:
在這裏對三種分片方式(應該是四種,有沒有virtual node的一致性hash算兩種)進行簡單總結,主要是針對提出的幾個問題:
上面的數據動態均衡,值得是上述問題的第4點,即若是某節點數據量變大,可否以及如何將部分數據遷移到其餘負載較小的節點
分片特徵值的選擇
上面的三種方式都提到了對數據的分片是基於關鍵值、特徵值的。這個特徵值在不一樣的系統中有不一樣的叫法,好比MongoDB中的sharding key, Oracle中的Partition Key,無論怎麼樣,這個特徵值的選擇都是很是很是重要的。
那麼。怎麼選擇這個特徵值呢?《Distributed systems for fun and profit》給出了言簡意賅的標準:
大概翻譯爲:基於最經常使用的訪問模式。訪問時包括對數據的增刪改查的。好比上面的列子,咱們選擇「id」做爲分片的依據,那麼就是默認對的數據增刪改查都是經過「id」字段來進行的。
若是在應用中,大量的數據操做都是經過這個特徵值進行,那麼數據分片就能提供兩個額外的好處:
若是大量操做並無使用到特徵值,那麼就很麻煩了。好比在本文的例子中,若是用name去查詢,而元數據記錄的是如何根據按照id映射數據位置,那就尷尬了,須要到多有分片都去查一下,而後再作一個聚合!
另一個問題,若是以單個字段爲特徵值(如id),那麼無論按照什麼分佈方式,在多條數據擁有相同的特徵值(如id)的狀況下,這些數據必定都會分佈到同一個節點上。在這種狀況下有兩個問題,一是不能達到節點間數據的均衡,二是若是數據超過了單個節點的存儲能力怎麼辦?關鍵在於,即便按照分佈式系統解決問題的常規辦法 -- 增長節點 --也是於事無補的。
在這個時候,單個字段作特徵值就不行了,可能得再增長一個字段做爲「聯合特徵值」,相似數據庫中的聯合索引。好比,數據是用戶的操做日誌,可使用id和時間戳一塊兒做爲hash函數的輸入,而後算出特徵值;但在這種狀況下,若是還想以id爲查詢關鍵字來查詢,那就得遍歷全部節點了。
因此說沒有最優的設計,只有最符合應用需求的設計。
下面以MongoDB中的sharding key爲例,解釋特徵值選擇的重要性以及對數據操做的影響。若是有數據庫操做基礎,即便沒有使用過MongoDB,閱讀下面的內容應該也沒有問題。
以MongoDB sharding key爲例
關於MongoDB Sharded cluster,以前也寫過一篇文章《經過一步步建立sharded cluster來認識mongodb》,作了簡單介紹。在個人工做場景中,除了聯合查詢(join)和事務,MongoDB的使用和Mysql仍是比較類似的,特別是基本的CRUD操做、數據庫索引。MongoDb中,每個分片成爲一個shard,分片的特徵值成爲sharding key,每一個數據稱之爲一個document。選擇適合的字段做爲shardingkey很是重要,why?
前面也提到,若是使用非sharding key去訪問數據,那麼元數據服務器(或者元數據緩存服務器,後面會講解這一部分)是無法知道對應的數據在哪個shard上,那麼該訪問就得發送到全部的shard,獲得全部shard的結果以後再作聚合,在mongoDB中,由mongos(緩存有元數據信息)作數據聚合。對於數據讀取(R: read or retrieve),經過同一個字段獲取到多個數據,是沒有問題的,只是效率比較低而已。對於數據更新,若是隻能更新一個數據,那麼在哪個shard上更新呢,彷佛都不對,這個時候,MongoDB是拒絕的。對應到MongoDB(MongoDD3.0)的命令包括但不限於:
findandmodify:這個命令只能更新一個document,所以查詢部分必須包含sharding key
update:這個命令有一個參數multi,默認是false,即只能更新一個document,此時查詢部分必須包含sharding key
remove:有一個參數JustOne,若是爲True,只能刪除一個document,也必須使用sharidng key
另外,熟悉sql的同窗都知道,在數據中索引中有unique index(惟一索引),即保證這個字段的值在table中是惟一的。mongoDB中,也能夠創建unique index,可是在sharded cluster環境下,只能對sharding key建立unique index,道理也很簡單,若是unique index不是sharidng key,那麼插入的時候就得去全部shard上查看,並且還得加鎖。
接下來,討論分片到shard上的數據不均的問題,若是一段時間內shardkey過於集中(好比按時間增加),那麼數據只往一個shard寫入,致使沒法平衡集羣壓力。
MongoDB中提供了"range partition"和"hash partition",這個跟上面提到的分片方式 hash方式, ranged based不是一回事兒,而是指對sharding key處理。MongoDB必定是ranged base分片方式,docuemnt中如是說:
那麼什麼是"range partition"和"hash partition",官網的一張圖很好說明了兩者的區別:
上圖左是range partition,右是hash partition。range partition就是使用字段自己做爲分片的邊界,好比上圖的x;而hash partition會將字段從新hash到一個更大、更離散的值域區間。
hash partition的最大好處在於保證數據在各個節點上均勻分佈(這裏的均勻指的是在寫入的時候就均勻,而不是經過MongoDB的balancing功能)。好比MongoDB中默認的_id是objectid,objectid是一個12個字節的BSON類型,前4個字節是機器的時間戳,那麼若是在同一時間大量建立以ObjectId爲_id的數據 會分配到同一個shard上,此時若將_id設置爲hash index 和 hash sharding key,就不會有這個問題。
固然,hash partition相比range partition也有一個很大的缺點,就是範圍查詢的時候效率低!所以到底選用hash partition仍是range partition還得根據應用場景來具體討論。
最後得知道,sharding key一但選定,就沒法修改(Immutable)。若是應用必需要修改sharidng key,那麼只能將數據導出,新建數據庫並建立新的sharding key,最後導入數據。
元數據服務器
在上面討論的三種數據分片分式中,或多或少都會記錄一些元數據:數據與節點的映射關係、節點狀態等等。咱們稱記錄元數據的服務器爲元數據服務器(metaserver),不一樣的系統叫法不同,好比master、configserver、namenode等。
元數據服務器就像人類的大腦,一隻手不能用了還沒忍受,大腦不工做整我的就癱瘓了。所以,元數據服務器的高性能、高可用,要達到這兩個目標,元數據服務器就得高可擴展 -- 以此應對元數據的增加。
元數據的高可用要求元數據服務器不能成爲故障單點(single point of failure),所以須要元數據服務器有多個備份,而且可以在故障的時候迅速切換。
有多個備份,那麼問題就來了,怎麼保證多個備份的數據一致性?
多個副本的一致性、可用性是CAP理論討論的範疇,這裏簡單介紹兩種方案。第一種是主從同步,首先選出主服務器,只有主服務器提供對外服務,主服務器將元數據的變革信息以日誌的方式持久化到共享存儲(例如nfs),而後從服務器從共享存儲讀取日誌並應用,達到與主服務器一致的狀態,若是主服務器被檢測到故障(好比經過心跳),那麼會從新選出新的主服務器。第二種方式,經過分佈式一致性協議來達到多個副本件的一致,好比大名鼎鼎的Paxos協議,以及工程中使用較多的Paxos的特化版本 -- Raft協議,協議能夠實現全部備份都可以提供對外服務,而且保證強一致性。
HDFS元數據
HDFS中,元數據服務器被稱之爲namenode,在hdfs1.0以前,namenode仍是單點,一旦namenode掛掉,整個系統就沒法工做。在hdfs2.0,解決了namenode的單點問題。
上圖中NN即NameNode, DN即DataNode(即實際存儲數據的節點)。從圖中能夠看到, 兩臺 NameNode 造成互備,一臺處於 Active 狀態,爲主 NameNode,另一臺處於 Standby 狀態,爲備 NameNode,只有主 NameNode 才能對外提供讀寫服務。
Active NN與standby NN之間的數據同步經過共享存儲實現,共享存儲系統保證了Namenode的高可用。爲了保證元數據的強一致性,在進行準備切換的時候,新的Active NN必需要在確認元數據徹底同步以後才能繼續對外提供服務。
另外,Namenode的狀態監控以及準備切換都是Zookeeper集羣負責,在網絡分割(network partition)的狀況下,有可能zookeeper認爲原來的Active NN掛掉了,選舉出新的ActiveNN,但實際上原來的Active NN還在繼續提供服務。這就致使了「雙主「或者腦裂(brain-split)現象。爲了解決這個問題,提出了fencing機制,也就是想辦法把舊的 Active NameNode 隔離起來,使它不能正常對外提供服務。具體參見這篇文章。
MongoDB元數據
MongoDB中,元數據服務器被稱爲config server。在MongoDB3.2中,已經再也不建議使用三個鏡像(Mirrored)MongoDB實例做爲config server,而是推薦使用複製集(replica set)做爲config server,此舉的目的是加強config server的一致性,並且config sever中mongod的數目也能從3個達到replica set的上線(50個節點),從而提升了可靠性。
在MongoDB3.0及以前的版本中,元數據的讀寫按照下面的方式進行:
MongoDB的官方文檔並無詳細解釋這一過程,不過在stackexchange上,有人指出這個過程是兩階段提交。
MongoDB3.2及以後的版本,使用了replica set config server,在《CAP理論與MongoDB一致性、可用性的一些思考》文章中,詳細介紹了replica set的write concern、read concern和read references,這三個選項會影響到複製集的一致性、可靠性與讀取性能。在config server中,使用了WriteConcern:Majority;ReadConcern:Majority;ReadReferences:nearest。
元數據的緩存:
即便元數據服務器能夠由一組物理機器組成,也保證了副本集之間的一致性問題。可是若是每次對數據的請求都通過元數據服務器的話,元數據服務器的壓力也是很是大的。不少應用場景,元數據的變化並非很頻繁,所以能夠在訪問節點上作緩存,這樣應用能夠直接利用緩存數據進行數據讀寫,減輕元數據服務器壓力。
在這個環境下,緩存的元數據必須與元數據服務器上的元數據一致,緩存的元數據必須是準確的,未過期的。相反的例子是DNS之類的緩存,即便使用了過時的DNS緩存也不會有太大的問題。
怎麼達到緩存的強一致性呢?比較容易想到的辦法是當metadata變化的時候當即通知全部的緩存服務器(mongos),但問題是通訊有延時,不可靠。
解決不一致的問題,一個比較常見的思路是版本號,好比網絡通訊,通訊協議可能會發生變化,通訊雙方爲了達成一致,那麼可使用版本號。在緩存一致性的問題上,也可使用版本號,基本思路是請求的時候帶上緩存的版本號,路由到具體節點以後比較實際數據的版本號,若是版本號不一致,那麼表示緩存信息過舊,此時須要從元數據服務器從新拉取元數據並緩存。在MongoDB中,mongos緩存上就是使用的這種辦法。
另一種解決辦法,就是大名鼎鼎的lease機制 -- 「An Efficient Fault-Tolerant Mechanism for Distributed File Cache Consistency」,lease機制在分佈式系統中使用很是普遍,不只僅用於分佈式緩存,在不少須要達成某種約定的地方都大顯身手,在《分佈式系統原理介紹》中,對lease機制有較爲詳細的描述,下面對lease機制進行簡單介紹。
Lease機制:
既然,Lease機制提出的時候是爲了解決分佈式存儲系統中緩存一致性的問題,那麼首先來看看Lease機制是怎麼保證緩存的強一致性的。注意,爲了方便後文描述,在本小節中,咱們稱元數據服務器爲服務器,緩存服務器爲客戶端。
要點:
在Lease論文的標題中,提到了「Fault-Tolerant」,那麼lease是怎麼作到容錯的呢。關鍵在於,只要服務器一旦發出數據和lease,不關心客戶端是否收到數據,只要等待lease過時,就能夠修改元數據;另外,lease的有效期經過過時時間(一個時間戳)來標識,所以即便從服務器到客戶端的消息延時到達、或者重複發送都是沒有關係的。
不難發現,容錯的前提是服務器與客戶端的時間要一致。若是服務器的時間比客戶端的時間慢,那麼客戶端收到lease以後很快就過時了,lease機制就發揮不了做用;若是服務器的時間比客戶端的時間快,那麼就比較危險,由於客戶端會在服務器已經開始更新元數據的時候繼續使用緩存,工程中,一般將服務器的過時時間設置得比客戶端的略大,來解決這個問題。爲了保持時間的一致,最好的辦法是使用NTP(Network Time Protocol)來保證時鐘同步。
Lease機制的本質是頒發者授予的在某一有效期內的承諾,承諾的範圍是很是普遍的:好比上面提到的cache;好比作權限控制,例如當須要作併發控制時,同一時刻只給某一個節點頒發lease,只有持有lease的節點才能夠修改數據;好比身份驗證,例如在primary-secondary架構中,給節點頒發lease,只有持有lease的節點才具備primary身份;好比節點的狀態監測,例如在primary-secondary架構中監測primary是否正常,這個後文再詳細介紹。
工程中,lease機制也有大量的應用:GFS中使用Lease肯定Chuck的Primary副本, Lease由Master節點頒發給primary副本,持有Lease的副本成爲primary副本。chubby經過paxos協議實現去中心化的選擇primary節點,而後Secondary節點向primary節點發送lease,該lease的含義是:「承諾在lease時間內,不選舉其餘節點成爲primary節點」。chubby中,primary節點也會向每一個client節點頒發lease。該lease的含義是用來判斷client的死活狀態,一個client節點只有只有合法的lease,才能與chubby中的primary進行讀寫操做。
總結
本文主要介紹分佈式系統中的分片相關問題,包括三種分佈方式:hash、一致性hash、range based,以及各自的優缺點。分片都是按照必定的特徵值來進行,特徵值應該從應用的使用場景來選取,並結合MongoDB展現了特徵值(mongodb中的sharding key)對數據操做的影響。分片信息(即元數據)須要專門的服務器存儲,元數據服務器是分佈式存儲系統的核心,所以須要提到其可用性和可靠性,爲了減輕元數據服務器的壓力,分佈式系統中,會在其餘節點緩存元數據,緩存的元數據由帶來了一致性的挑戰,由此引入了Lease機制。
其實不少技術不是幾句話就能說清楚的,因此乾脆找朋友錄製了一些視頻,不少問題其實答案很簡單,可是背後的思考和邏輯不簡單,要作到知其然還要知其因此然。在此給你們推薦一個Java架構方面的交流學習羣:698581634,裏面會分享一些資深架構師錄製的視頻錄像:有Spring,MyBatis,Netty源碼分析,高併發、高性能、分佈式、微服務架構的原理,JVM性能優化這些成爲架構師必備的知識體系,主要針對Java開發人員提高本身,突破瓶頸,相信你來學習,會有提高和收穫。在這個羣裏會有你須要的內容 朋友們請抓緊時間加入進來吧。