NO IMAGE

聽過很多道理,卻依然過不好這一生。

看過很多關於學習的技巧、方法,卻沒應用到自己的學習中。

隨著年紀變大,記憶力越來越差,整塊的時間也越來越少,於是,越來越希望能夠更高效的學習。學習是一種習慣也是一種能力,這種能力在上學期間養成是最好的,畢竟那個時候絕大部分時間都在學習。但很遺憾,我沒有養成適合自己的、好的學習習慣。工作之後,除了在日常工作中用到的知識技術,很難通過自學掌握新的知識(偏向於專業知識,即技術)。而網際網路行業的分支、知識點又是如此之多,於是會出現這樣的情況,遇到一個新的知識,覺得很厲害很感興趣,看兩天,但很快就忘記了。另外,對於一些比較龐雜的技術,又無從下手,也很難堅持下去。

根本的問題在於學習不繫統,沒有把一個個的知識點連線起來,本來這些新的知識就很少在工作中實踐,如果又是一個個的資訊孤島,很快就會被遺忘。另一個問題,沒有良好的規劃,今天看看這裡,明天看看哪裡,糾結於細枝末節,忘了從整體上把握。

幸好,差不多半年前開始意識到了這個問題,開始看書,看別人的部落格,開始思考如何充分利用好有限的時間。自己也實踐了一些想法,比如寫部落格,堅持寫部落格。也有很多沒做好,比如如何學習掌握一門新技術。關於這一點,其實看了許多文章,也有很多印象深刻,覺得很有道理;也有一些好書,比如《study more,learn less》。紙上得來終覺淺,絕知此事要躬行,別人的辦法再好也需要親身實踐才知道是否對自己適用。

需要學習的技術很多,要自學新知識也不是一件容易的事,選擇一個自己比較感興趣的會是一個比較好的開端,於是,打算學一學分散式系統。

帶著問題,有目的的學習,先了解整體架構,在深入感興趣的細節,這是我的計劃。

首先得有問題,如果每日重複相同的工作,也不主動去學習,很難發現新的問題。不怕自己無知,就怕不知道自己無知,只有不斷的學習,才會發現更多未知的知識領域!

帶著問題出發

分散式要解決什麼問題呢?解決持久化資料太大,單個節點的硬碟無法儲存的問題;解決運算量太大,單個節點的記憶體、CPU無法處理的問題。解決這些問題,有兩種思路:scale up,scale out。前者就是提升單個節點的能力,更大的磁碟,更快的CPU,定製的軟硬體,然而這意味著更高的價格,而且再怎麼scaleup 也是有上限的。後者就是把儲存、計算任務分擔到普通的機器上,通過動態增加節點來應對資料量的增長,但缺點是多個節點的管理、任務的排程比較麻煩,這也是分散式系統研究和解決的問題。只有當資料量達到單機無法儲存、處理的情況下才考慮分散式,不然都是自找麻煩。

狀態的維護比計算要難很多,所謂狀態就是需要持久化的資料。因此主要考慮分散式儲存,況且即使是分散式計算,為了節省頻寬需要儘量保證data locality,也是需要分散式儲存。
現在有一堆資料,可能是結構化或者半結構化,需要將資料分片(segment、fragment、shard),形成一個個的資料子集,儲存到一組物理節點上,物理節點之間通過網路通訊。那麼需要考慮兩個問題:
第一:資料如何劃分;
第二:資料的可靠性、可用性問題

資料分片

資料分片是指將資料子集儘可能均衡的劃分到各個物理節點上。那麼會有哪些挑戰呢?

(1)如果某個物理節點宕機,如何將該物理節點負責的資料儘快的轉移到其他物理節點;

(2)如果新增了物理節點,怎麼從其他節點遷移資料到新節點;

(3)對於可修改的資料(即不是隻能追加的資料),比如資料庫資料,如果某節點資料量變大,怎麼將部分資料遷移到其他負載較小的節點,及達到動態均衡的效果。

(4)後設資料的管理問題:當資料分佈在各個節點,那麼當使用者使用的時候需要知道具體的資料在哪一個節點上。因此,系統需要維護資料的後設資料:即每一個資料所在的位置、狀態等資訊。當使用者需要具體的資料時,先查詢後設資料,然後再去具體的節點上查詢。當資料在節點之間遷移的時候,也需要更新後設資料。後設資料的管理節點這裡稱之為meta
server。後設資料的管理也帶來了新的挑戰:
(4.1)如何抽取資料的特徵(特徵是分片的依據,也是使用者查詢資料時的key),或者支援使用者自定義資料特徵;

(4.2)如何保證meta server的高效能和高可用,是單點還是複製集

(5)分片的粒度,即資料子集的大小,也是資料遷移的基本單位。粒度過粗,不利於資料均衡;粒度過細,管理、遷移成本又會比較大。

資料冗餘

前面提到,分散式系統中的節點都是普通的節點,因此有一定的概率會出現物理故障,比如斷電、網路不可用,這些故障導致資料的暫時不可用;另外一些故障更嚴重,會導致資料的丟失,比如磁碟損壞。即使單個節點的故障是小概率,當叢集中的節點數目很多是,故障就成為了一個大概率事件。因此,保證資料的高可用和可靠性是分散式系統必須解決的問題。

為了避免單點故障,可行的辦法就是資料冗餘(複製集),即將同一份資料放在不同的物理節點,甚至是不同的資料中心。如果資料是一次寫,多次讀那很好辦,隨便從哪個副本讀取都行。但對於很多分散式儲存系統,比如資料庫,資料是持續變化的,有讀有寫。那麼複製集會帶來什麼樣的挑戰呢,需要如何權衡呢,假設有三個副本:

(1)三個副本的地位,大家都是平等的還是有主(primary、master)有次(secondary、slave),如果是平等的,那麼每個節點都可以接收寫操作;如果不平等,可以一個節點負責所有的寫操作,所有節點都提供讀操作,

(2)在平等的情況下,怎麼保證寫入操作不衝突,保證各個節點的資料是一致的,怎麼保證能讀取到最新的資料

  (3)不平等的情況下
    (3.1)寫節點怎麼將變更的資料同步到其他節點,同步還是非同步;
    (3.2)非寫節點能否提供讀資料,如果能夠允許,會不會讀取到過時的資料。
    (3.3)主節點是怎麼產生的,當主節點宕機的時候,怎麼選擇出新的主節點。是有統一的複製集管理中心(記錄誰主誰次,各自的狀態),還是複製集自己選舉出一個主節點?

(4)不管複製集內部的節點是平等的,還是有集中式節點的,只要有多個資料副本,就需要考慮資料的一致性可用性問題。按照CAP理論,只能同時滿足一致性 可用性 分割槽容錯性之間的二者,不同的分散式系統需要權衡。


在前文中,提出了分散式系統(尤其是分散式儲存系統)需要解決的兩個最主要的問題,即資料分片和資料冗餘,下面這個圖片(來源)形象生動的解釋了其概念和區別:

其中資料即A、B屬於資料分片,原始資料被拆分成兩個正交子集分佈在兩個節點上。而資料集C屬於資料冗餘,同一份完整的資料在兩個節點都有儲存。當然,在實際的分散式系統中,資料分片和資料冗餘一般都是共存的。

本文主要討論資料分片的三個問題:

(1)如何做資料分片,即如何將資料對映到節點

(2)資料分片的特徵值,即按照資料中的哪一個屬性(欄位)來分片

(3)資料分片的後設資料的管理,如何保證後設資料伺服器的高效能、高可用,如果是一組伺服器,如何保證強一致性

所謂分散式系統,就是利用多個獨立的計算機來解決單個節點(計算機)無法處理的儲存、計算問題,這是非常典型的分而治之的思想。每個節點只負責原問題(即整個系統需要完成的任務)的一個子集,那麼原問題如何拆分到多個節點?在分散式儲存系統中,任務的拆分即資料分片。

何為資料分片(segment,fragment, shard, partition),就是按照一定的規則,將資料集劃分成相互獨立、正交的資料子集,然後將資料子集分佈到不同的節點上。注意,這裡提到,資料分片需要按照一定的規則,不同的分散式應用有不同的規則,但都遵循同樣的原則:按照最主要、最頻繁使用的訪問方式來分片。

三種資料分片方式

  首先介紹三種分片方式:hash方式,一致性hash(consistent hash),按照資料範圍(range based)。對於任何方式,都需要思考以下幾個問題:
  1. 具體如何劃分原始資料集?
  2. 當原問題的規模變大的時候,能否通過增加節點來動態適應?
  3. 當某個節點故障的時候,能否將該節點上的任務均衡的分攤到其他節點?
  4. 對於可修改的資料(比如資料庫資料),如果某節點資料量變大,能否以及如何將部分資料遷移到其他負載較小的節點,及達到動態均衡的效果?
  5. 後設資料的管理(即資料與物理節點的對應關係)規模?後設資料更新的頻率以及複雜度?

為了後面分析不同的資料分片方式,假設有三個物理節點,編號為N0, N1, N2;有以下幾條記錄:

R0: {id: 95, name: ‘aa’, tag:’older’}
R1: {id: 302, name: ‘bb’,}
R2: {id: 759, name: ‘aa’,}
R3: {id: 607, name: ‘dd’, age: 18}
R4: {id: 904, name: ‘ff’,}
R5: {id: 246, name: ‘gg’,}
R6: {id: 148, name: ‘ff’,}
R7: {id: 533, name: ‘kk’,}

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]現在由N2(400, 600] N3(600, 800]負責,因此只需要將記錄R2(id:759), R3(id: 607) 從N2,遷移到N3:

  不難發現一致性hash方式在增刪的時候只會影響到hash環上響應的節點,不會發生大規模的資料遷移。

但是,一致性hash方式在增加節點的時候,只能分攤一個已存在節點的壓力;同樣,在其中一個節點掛掉的時候,該節點的壓力也會被全部轉移到下一個節點。我們希望的是“一方有難,八方支援”,因此需要在增刪節點的時候,已存在的所有節點都能參與響應,達到新的均衡狀態。

因此,在實際工程中,一般會引入虛擬節點(virtual node)的概念。即不是將物理節點對映在hash換上,而是將虛擬節點對映到hash環上。虛擬節點的數目遠大於物理節點,因此一個物理節點需要負責多個虛擬節點的真實儲存。運算元據的時候,先通過hash環找到對應的虛擬節點,再通過虛擬節點與物理節點的對映關係找到對應的物理節點。

引入虛擬節點後的一致性hash需要維護的後設資料也會增加:第一,虛擬節點在hash環上的問題,且虛擬節點的數目又比較多;第二,虛擬節點與物理節點的對映關係。但帶來的好處是明顯的,當一個物理節點失效是,hash環上多個虛擬節點失效,對應的壓力也就會發散到多個其餘的虛擬節點,事實上也就是多個其餘的物理節點。在增加物理節點的時候同樣如此。

工程中,DynamoCassandra都使用了一致性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算兩種)進行簡單總結,主要是針對提出的幾個問題:

  對映難度 後設資料 節點增刪 資料動態均衡
hash方式 簡單 非常簡單,幾乎不用修改 需要遷移的資料比較多 不支援
consistent hash
without virtual node
簡單 比較簡單,取決於節點規模,幾乎不用修改 增刪節點的時候隻影響hash環上相鄰節點,但不能使所有節點都參與資料遷移過程 不支援
consistent hash
with virtual node
中等 稍微複雜一些,主要取決於虛擬節點規模,很少修改 需要遷移的資料比較少,且所有節點都能貢獻部分資料 若支援(修改虛擬節點與物理節點對映關係)
range based 較為複雜 取決於每個塊的大小,一般來說規模較大;且修改頻率較高 需要遷移的資料比較少,且所有節點都能貢獻部分資料 支援,且比較容易

上面的資料動態均衡,值得是上述問題的第4點,即如果某節點資料量變大,能否以及如何將部分資料遷移到其他負載較小的節點

分片特徵值的選擇

上面的三種方式都提到了對資料的分片是基於關鍵值、特徵值的。這個特徵值在不同的系統中有不同的叫法,比如MongoDB中的sharding key
Oracle中的Partition Key,不管怎麼樣,這個特徵值的選擇都是非常非常重要的。

那麼。怎麼選擇這個特徵值呢?《Distributed systems for fun and profit》給出了言簡意賅的標準:

   based on what you think the primary access pattern will be

大概翻譯為:基於最常用的訪問模式。訪問時包括對資料的增刪改查的。比如上面的列子,我們選擇“id”作為分片的依據,那麼就是預設對的資料增刪改查都是通過“id”欄位來進行的。

如果在應用中,大量的資料操作都是通過這個特徵值進行,那麼資料分片就能提供兩個額外的好處:

(1)提升效能和併發,操作被分發到不同的分片,相互獨立

(2)提升系統的可用性,即使部分分片不能用,其他分片不會受到影響

如果大量操作並沒有使用到特徵值,那麼就很麻煩了。比如在本文的例子中,如果用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

  When using findAndModify in a sharded environment, the query must contain
the shard key for all operations against the shard cluster for the sharded collections.

  •   update:這個命令有一個引數multi,預設是false,即只能更新一個document,此時查詢部分必須包含sharding key
All update() operations for a sharded collection that specify the multi: false option
must include theshard key or the _id field in the query specification.
  •     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中如是說:
MongoDB partitions data
in the collection using ranges of shard key values. Each range defines a non-overlapping range of shard key values and is associated with a chunk.

那麼什麼是”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及之前的版本中,後設資料的讀寫按照下面的方式進行:

  When writing to the three config servers, a coordinator dispatches the same write commands to the three config servers and collects the results. Differing results indicate an inconsistent writes to
the config servers and may require manual intervention.

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,lease包含一個有限期(即過期時間)
  •   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機制。

references