1. 程式人生 > >儲存系統科普——分散式儲存系統解決方案介紹

儲存系統科普——分散式儲存系統解決方案介紹

簡介

該篇blog只是儲存系列科普文章中的第四篇,所有文章請參考:

部落格所有文章

在工程架構領域裡,儲存是一個非常重要的方向,這個方向從底至上,我分成了如下幾個層次來介紹:

  1. 硬體層:講解磁碟,SSD,SAS, NAS, RAID等硬體層的基本原理,以及其為作業系統提供的儲存介面;
  2. 作業系統層:即檔案系統,作業系統如何將各個硬體管理並對上提供更高層次介面;
  3. 單機引擎層:常見儲存系統對應單機引擎原理大概介紹,利用檔案系統介面提供更高級別的儲存系統介面;
  4. 分散式層:如何將多個單機引擎組合成一個分散式儲存系統;
  5. 查詢層:使用者典型的查詢語義表達以及解析;

分散式系統主要分成儲存模型和計算模型兩類。本文主要描述的是儲存模型的介紹。其中計算模型的分散式系統原理跟儲存模型類似,只是會根據自身計算特點加一些特殊排程邏輯進去。

分散式層

分散式系統簡介

任何一個分散式系統都需要考慮如下5個問題:

  1. 資料如何分佈

    就像把雞蛋放進籃子裡面。一般來說籃子大小是一樣的,當然也有的系統支援不一樣大小的籃子。雞蛋大小也不一樣,有很多系統就把雞蛋給"切割"成一樣大小然後再放。並且有的雞蛋表示對籃子有要求,比如對機房/機架位的要求。

    衡量一個數據分佈演算法好不好就看他是否分得足夠均勻,使得所有機器的負載方差足夠小。

  2. 如何容災

    分散式系統一個很重要的定位就是要讓程式自動來管機器,儘量減少人工參與,否則一個分散式系統的運維成本將不可接受。

    容災問題非常複雜,有很多很成熟的系統也不敢保證自己做得特別好,那麼來看看一個典型的系統都有可能出哪些問題吧:

    1. 機器宕機

      這是最常見的故障了。系統中最容易出問題的硬碟的年故障率可能能達到10%。這樣算下來,一個有1000臺機器的叢集,每一個星期就會有2臺機器宕機。所以在機器數量大了之後,這是一個很正常的事情。

      一般一臺機器出故障之後修復週期是24小時,這個過程是人工接入換裝置或者重啟機器。在機器恢復之後記憶體資訊完全丟失,硬碟資訊可能可以儲存。

      一個分散式系統必須保證一臺機器的宕機對服務不受影響,並且在修復好了之後再重新放到叢集當中之後也能正常工作。

    2. 網路故障

      這是最常見且要命的故障。就是該問題會大大增加分散式系統設計的難度。故障一般發生在網路擁塞,路由變動,裝置異常等情況。出現的問題可能是丟包,可能是延時,也可能是完全失去連線。

      有鑑於此,我們一般在設計分散式系統的時候,四層協議都採用TCP,很少採用UDP/UDT協議。而且由於TCP協議並不能完全保證資料傳輸到對面,比如我們再發送資料,只要資料寫入本地緩衝區,作業系統就會返回應用層說傳送成功,但是有可能根本沒送到對面。所以我們一般還需要加上應用層的ACK,來保證網路層的行為是可預期的。

      但是,即使加上應用層的ACK,當傳送請求之後遲遲沒收到ACK。這個時候作為傳送方也並不知道到底對方是直接掛了沒收到請求,還是收到請求之後才掛的。這個尤其是對於一些控制命令請求的傳送尤為致命。

      一般系統有兩種方案:

      1. 傳送查詢命令來判斷到底是哪種情況
      2. 將協議設計成"冪等性"(即可重複傳送資料並不影響最終資料), 然後不停重試
    3. 其他異常

      比如磁碟壞塊,但是機器並沒有宕機;機器還活著,就是各種操作特別慢;由於網路擁塞導致一會網路斷掉,不傳送資料之後又好了,一旦探活之後重新使用又掛了等噁心的情況;

      這些異常都需要根據實際情況來分析,在長期工程實踐中去調整解決。

      並且令人非常沮喪的事實是:你在設計階段考慮的異常一定會在實際執行情況中遇到,你沒考慮到的異常也會在實際執行中遇到。所以分散式系統設計的一個原則是:不放過任何一個你看得到的異常。

  3. 讀寫過程一致性如何保證

    一致性的概率很簡單,就是我更新/刪除請求返回之後,別人是否能讀到我新寫的這個值。對於單機系統,這個一致性要達到很簡單,大不了是損失一點寫的效率。但是對於分散式系統,這個就複雜了。為了容災,一份資料肯定有多個副本,那麼如何更新這多個副本以及控制讀寫協議就成了一個大問題。

    而且有的寫操作可能會跨越多個分片,這就更復雜了。再加上剛才提到的網路故障,可能在同步資料的時候還會出現各種網路故障,想想就頭疼。

    而且即使達到了一致性,有可能讀寫效能也會受到很大損失。我們設計系統的時候就像一個滑動條,左邊是一致性,右邊是效能,兩者無法同時滿足(CAP原理)。一般的系統會取折衷,設計得比較好的系統能夠讓使用者通過配置來控制這個滑動條的位置,滿足不同型別的需求。

    一致性一般怎麼折衷呢?我們來看看如下幾種一致性的定義。注意除了強一致性以外,其他幾種一致性並不衝突,一個系統可以同時滿足一種或者幾種一致性特點。

    1. 強一致性

      不用多說,就是最嚴格的一致性要求。任何時候任何使用者只要寫了,寫請求返回的一霎那,所有其他使用者都能讀到新的值了。

    2. 最終一致性

      這個也是提得很多的一個概念,很多系統預設提供這種方式的一致性。即最終系統將將達到"強一致性"的狀態,但在之前會有一段不確定的時間,系統處於不一致的狀態。

    3. 會話一致性

      這個也很容易理解,能滿足很多場景下的需求。在同一個會話當中,使用者感受到的是"強一致性"的服務。

    4. 單調一致性

      這個比會話一致性還要弱一點。他之保證一個使用者在讀到某個資料之後,絕對不會讀到比上一次讀到的值更老的資料。

  4. 如何提高效能

    分散式系統設計之初就是為了通過堆積機器來增加系統整體效能,所以系統性能也非常重要。效能部分一般會受一致性/容災等設計的影響,會有一定的折衷。

    衡量一個分散式系統的效能指標往往有:

    1. 最大容量
    2. 讀qps
    3. 寫qps
  5. 如何保證橫向擴充套件

    橫向擴充套件是指一個叢集的服務能力是否可以通過加機器做到線性擴充套件。

上面簡單介紹了一個典型的分散式系統需要考慮的問題,提出了分散式系統設計的難點和問題,那麼接下來我們就來看看典型分散式系統對這些問題是怎麼解決的吧。

資料分佈(sharding)

資料分佈有兩個問題:

  1. 資料拆分問題。將一個大的檔案/表格資料拆分成多份儲存;
  2. 資料落地問題。針對每份結果在所有機器中尋找一臺機器來作為其儲存伺服器;

資料拆分問題

資料拆分有如下幾種典型的方式:

  1. hash拆分

    這個是最簡單的能想到的拆分演算法。將資料根據某個hash函式雜湊到其中一臺機器上即可。

    好處:

    1. 演算法簡單,幾乎不需要master機器就能知道資料分佈。這裡說"幾乎"是因為一般的hash演算法可能還需要用到總機器數量。

    壞處:

    1. 可擴充套件性太差。需要增加/減少機器的時候幾乎需要挪動所有資料;
    2. 資料可能分佈不均勻。一方面可能是因為資料量不夠大,hash演算法還不能比較平均的三列;另一方面可能是使用者訪問資料就是不均勻的,典型的使用者使用場景都有可能存在2/8原則,小部分請求佔據了絕大部分流量,即使是資料分佈是均勻的,不代表訪問流量就能均勻分配。
    3. 不支援順序讀取資料,順序讀取資料壓力會比較大。
  2. 一致性hash拆分

    一致性hash不做過多解釋,好處跟hash演算法一樣,他解決了擴容/縮容/資料遷移的時候普通hash演算法的大動干戈。

    一致性hash演算法的原理請參考這篇文章。

    使用該方案的系統:

    1. Dynamo/Cassandra
  3. 按資料範圍拆分

    這個方式也是非常常見的一種資料拆分方式,類似B+樹,按照儲存資料中某列或者某幾列的組合結果的範圍來判斷資料分佈。

    好處:

    1. 順序讀取資料比較友好
    2. 能比較容易的控制資料量分佈。一般系統會實現每臺機器負責範圍的動態合併和分裂,這樣就能比較好的動態控制每臺機器的負載了。

    壞處:

    1. 需要master伺服器來維持範圍和機器的對映關係,增加系統的複雜度,以及master機器可能會成為整個分散式系統的瓶頸;

    使用該方案的系統:

    1. BigTable
    2. HBase
  4. 按資料量拆分

    當資料總量到達一定大小就拆分出來。這個一般用於分散式檔案系統的大檔案儲存的方案。

    好處:

    1. 資料分佈均勻,實現所有機器均衡使用的複雜度較低

    壞處:

    1. 對資料修改和調整支援不好
    2. 同第3點,也需要一臺專用的master機器來維護對映關係
    3. 對隨機查詢支援不好

    使用該方案的系統:

    1. GFS
    2. HDFS

在實際系統中,我們也可以結合多種方式。比如先按照hash方式儲存,如果發現數據不夠均勻之後,再將不均勻的分片利用資料範圍或者資料量的方式做二次分片。這樣雖然系統實現複雜了,但是卻能達到資料分佈均勻,同時master裡面儲存的資訊又大大減少的好處。

資料落地問題

資料落地演算法一般分成兩類:

  1. 靜態分配

    靜態分配是指在資料還沒進來的時候,就將資源給他分配好,並且按照如上的某種拆分演算法做好相應初始化工作。

    好處:

    1. 實現簡單

    壞處:

    1. 需要提前預估該資料所需要的資源量
    2. 可能存在資源浪費
  2. 動態分配

    動態分配則跟靜態分配相反,只有當資料需要新的分片的時候才給他分配真正的資源。

    好處:

    1. 解決靜態分配的資源浪費問題和提前預估問題

    壞處:

    1. 實現複雜

不管是靜態分配還是動態分配,一般來說,都需要整個叢集所有機器的資源使用情況,然後利用貪心演算法分配一個當次分配最適合的機器給這份資料。

在分散式系統中,資料落地還需要同時考慮副本分佈的問題。一份資料的副本往往需要分配到不同的網段甚至地域避免單網段故障;另外,為了避免單臺機器宕機的時候該臺機器包含的所有流量全部壓到另外一臺機器上去,所以所有副本的分佈也要足夠的雜湊和均勻。

資料副本(replication)

資料副本的存在主要是為了避免單機宕機出現的服務停止的情況,增加整個分散式系統的可用性。

但是也是因為副本的存在,以及產品可能的對一致性的要求,會使得在讀寫過程中對副本的控制需要格外的小心。

一般來說,我們用副本控制協議來代表副本管理的方式。典型的副本控制協議又分成兩類:

  1. 中心化的副本控制協議
  2. 去中心化的副本控制協議

中心化的副本控制協議

顧名思義,在所有副本當中,會有一個副本作為中心副本,來控制其他副本的行為。可以看到這樣的話,系統的一致性控制實現將會變得很簡單,就類似單機系統的控制了。

在單機系統中要實現一致性控制,用本地鎖就好了。但是如果沒有中心副本,那麼要實現一致性就需要一套複雜的分散式互動協議來達到一致性,將大大增加系統實現成本。

下面主要講講幾個最常見case中心化的副本協議操作流程:

  1. 寫資料

    整體流程如下:
    1. 寫客戶端將寫請求傳送給中心副本
    2. 中心副本確定更新方案
    3. 中心副本將資料按照既定方案發送給從副本
    4. 中心副本根據更新完成情況返回使用者成功/失敗

    這裡重點描述一下流程中提到的更新方案。

    更新方案主要有兩種:

    1. 中心同步

      中心同步的意思是由中心節點將資料序列或者並行的同步到所有其他副本上。

    2. 鏈式同步

      鏈式同步是指中心節點之同步給一個副本,這個副本再同步給下一個副本。

    這兩個方案其實差不多,最主要的差別是中心同步會比鏈式同步對主副本機器網絡卡造成更大的壓力。但是實際上因為有很多個數據分片,而資料分片對應的主副本在所有機器中是均勻分配的,所以雖然單分片壓力會增加,但整體叢集的資源利用率的均衡程度還好。

  2. 查詢資料

    查詢資料的邏輯跟一致性要求強相關。如果使用者只需要最終一致性,那麼讀取任何副本都OK。如果使用者需要強一致性,那麼就需要一個比較複雜的協議來控制了。

    一般我們有如下幾種方案來實現強一致性:

    1. 只讀中心副本

      這個是最簡單的方案。而且同上面對中心同步和鏈式同步的分析,對整體機器的均衡性影響也可以忽略。該方案最大的問題在於其他副本成了擺設,導致系統的最大qps和吞吐都只限單機。

    2. 標記副本狀態

      這也是很常見的方案,每個副本上都帶上一個版本號,版本號是遞增不減的。在主副本中維護一個當前版本號的資訊。

      當主副本認為資料更新成功之後,會更新當前版本號。每次讀資料之前,會先得到當前版本號的資訊,來選擇版本號一致的副本進行查詢。

      這裡有一個概念,主副本認為資料更新成功。一般主副本怎樣才認為資料更新成功而不是失敗呢?一般系統有兩種做法:

      1. 全部寫成功才算成功

        這個方案實現簡單,就是對寫資料的可用性有大的損失。因為只要有一個副本有問題,這個副本的所有寫請求都會失敗。

        有很多系統,就使用的這個方案,不過一般都會加以優化,來提高系統的寫請求可用性。比如在GFS中,如果發現寫一個副本失敗了,會嘗試另外建立一個副本,只要新副本寫成功了,就OK。不過像這種優化方案不適用於一些副本比較大的系統,並且需要增加過時副本回收機制。

      2. 寫入部分副本就算成功

        更多的系統是給定一個配置值,主要寫入這個配置值對應的副本數就算成功。一般這個配置的值需要超過一半。這個方式還有一個固定的名字:quorum演算法

        該演算法的定義如下:

        假設有N個副本,每次寫W個副本就算成功,那麼在讀資料的時候只要讀(N-W+1)個副本的版本資訊,就起碼能讀到至少一個正確更新的副本。

        一般配合quorum演算法,還需要在主副本或者master中儲存一下當前副本的最新版本的資訊,如果不儲存這個資訊的話,最壞情況下,就需要讀取全部副本的版本資訊,來確定到底哪個版本是當前正確的版本。

        現在系統中絕大部分系統都是採用quorum演算法的思想來實現的。

  3. 異常處理

    典型的異常包括:

    1. 從副本掛掉

      問題發現:定期從副本與主副本之間的心跳/租約機制。寫資料的時候異常。

      問題解決:通過標記狀態或者版本控制的方式來解決。

    2. 主副本掛掉

      問題發現:通過主副本與master之間的心跳/租約機制。

      問題解決:重新指定一個從副本作為主副本。

去中心化的副本控制協議

去中心化協議實現相當複雜,為了保證多個副本之間的資訊同步,一般需要多輪互動才能達成一致。在實際工程專案中,都是使用的paxos協議及其變種來作為資料一致性協議的。

在現有系統中,主要有兩類系統實現了去中心化的副本協議:

  1. chubby/zookeeper

    chubby是google提出來的專門做分散式鎖的系統,是第一個將paxos這個學術上的東西帶進了工業界。zookeeper是chubby的開源實現。

    paxos就類似選舉,大家都提出自己的意見,最後大家經過一輪又一輪的投票,直到一個人獲得多數票,然後大家就按照這個人的意見來執行,從而達到統一大家意見的目的。

    關於這兩個系統的介紹和對比請參考這篇文章。

  2. cassandra/dynamo

    cassandra和dynamo其實都是同一個哥們在不同的公司搞出來的,所以我們給他放一起來說。

    他是利用了quorum演算法的思想,寫入超過一半副本就算成功,讀取的時候會讀取多個副本來判斷版本。但是因為沒有了主副本,每次主導更新的副本都可能是不同的副本,這樣在一些非冪等性操作的情況下,就有可能出現一些不符合預期的情況,而cassandra也不處理這種情況,將問題拋給使用者,當然,他會保留一下這個副本上的更新資訊,來輔助使用者來判斷。

    舉例:假設有三個副本 A,B,C。因為某種原因,A和C之間的網路連線掛了,其他網路連線正常,假設副本一開始的值都是1。第一次操作: +1,由A主導,那麼結束之後三副本的值為(2,2,1),對應更新屬性資訊為[(v1,A), (v1, A), ()];第二次操作: +2,由C主導,那麼結束之後副本值為(2,3,3),對應更新資訊為[(v1,A), (v2, C), (v2, C)];第三次操作: +3,由A主導,結束之後三副本的值為(5,5,3),對應更新屬性資訊為[(v1,A; v2,A),(v1,A; v2,A), (v2,C)]。更新資訊其實就是我當前給的這個值的來源,每次版本更新都是哪個副本在負責。

    讀請求可能讀到(3,5)兩個值,哪個值是正確的就使用者自己來判斷了。

事務支援

事務典型的例子就是銀行轉賬,一個賬戶減錢,一個賬戶加錢,要麼都成功,要麼都失敗,不能有中間狀態。

事務支援主要有兩種方案:

  1. 加鎖

    加鎖是最簡單的做法。根據事務涉及到的範圍,又分成表鎖/行鎖。在鎖定期間,其他寫操作需要排隊等待。而且讀操作也必須等待,不然就有可能讓使用者讀到一半事務的值,比如賬戶扣錢了,另外一個賬戶錢還沒漲。

    所以這樣就會造成系統性能降低,尤其是讀效能還會受到蠻大影響。

  2. MVCC

    為了避免加鎖造成的讀等待問題,就很自然的想到給一份資料儲存多個版本,在事務執行到一半的時候,已經執行的那些行資料老資料還在,讀請求還用老資料來響應,這樣就不會讓讀請求給hang住了。

    在事務執行完畢之後,如果成功,就把新結果合併成真正的資料,從此以後新的讀請求就會讀到事務過後的新資料了。

    同時,如果有多個寫事務同時在執行的話,就需要儲存多份資料版本,並且在最後合併的時候可能還需要涉及到一定的merge邏輯,merge邏輯跟自身系統的業務特點有關。