簡介
該篇blog只是存儲系列科普文章中的第四篇,所有文章請參考:
在工程架構領域里,存儲是一個非常重要的方向,這個方向從底至上,我分成了如下幾個層次來介紹:
- 硬件層:講解磁盤,SSD,SAS, NAS, RAID等硬件層的基本原理,以及其為操作系統提供的存儲界面;
- 操作系統層:即文件系統,操作系統如何將各個硬件管理並對上提供更高層次接口;
- 單機引擎層:常見存儲系統對應單機引擎原理大概介紹,利用文件系統接口提供更高級別的存儲系統接口;
- 分布式層:如何將多個單機引擎組合成一個分布式存儲系統;
- 查詢層:用戶典型的查詢語義表達以及解析;
分布式系統主要分成存儲模型和計算模型兩類。本文主要描述的是存儲模型的介紹。其中計算模型的分布式系統原理跟存儲模型類似,只是會根據自身計算特點加一些特殊調度邏輯進去。
分布式層
分布式系統簡介
任何一個分布式系統都需要考慮如下5個問題:
-
數據如何分布
就像把雞蛋放進籃子里面。一般來說籃子大小是一樣的,當然也有的系統支持不一樣大小的籃子。雞蛋大小也不一樣,有很多系統就把雞蛋給"切割"成一樣大小然后再放。並且有的雞蛋表示對籃子有要求,比如對機房/機架位的要求。
衡量一個數據分布算法好不好就看他是否分得足夠均勻,使得所有機器的負載方差足夠小。
-
如何容災
分布式系統一個很重要的定位就是要讓程序自動來管機器,盡量減少人工參與,否則一個分布式系統的運維成本將不可接受。
容災問題非常復雜,有很多很成熟的系統也不敢保證自己做得特別好,那么來看看一個典型的系統都有可能出哪些問題吧:
-
機器宕機
這是最常見的故障了。系統中最容易出問題的硬盤的年故障率可能能達到10%。這樣算下來,一個有1000台機器的集群,每一個星期就會有2台機器宕機。所以在機器數量大了之后,這是一個很正常的事情。
一般一台機器出故障之后修復周期是24小時,這個過程是人工接入換設備或者重啟機器。在機器恢復之后內存信息完全丟失,硬盤信息可能可以保存。
一個分布式系統必須保證一台機器的宕機對服務不受影響,並且在修復好了之后再重新放到集群當中之后也能正常工作。
-
網絡故障
這是最常見且要命的故障。就是該問題會大大增加分布式系統設計的難度。故障一般發生在網絡擁塞,路由變動,設備異常等情況。出現的問題可能是丟包,可能是延時,也可能是完全失去連接。
有鑒於此,我們一般在設計分布式系統的時候,四層協議都采用TCP,很少采用UDP/UDT協議。而且由於TCP協議並不能完全保證數據傳輸到對面,比如我們再發送數據,只要數據寫入本地緩沖區,操作系統就會返回應用層說發送成功,但是有可能根本沒送到對面。所以我們一般還需要加上應用層的ACK,來保證網絡層的行為是可預期的。
但是,即使加上應用層的ACK,當發送請求之后遲遲沒收到ACK。這個時候作為發送方也並不知道到底對方是直接掛了沒收到請求,還是收到請求之后才掛的。這個尤其是對於一些控制命令請求的發送尤為致命。
一般系統有兩種方案:
- 發送查詢命令來判斷到底是哪種情況
- 將協議設計成"冪等性"(即可重復發送數據並不影響最終數據), 然后不停重試
-
其他異常
比如磁盤壞塊,但是機器並沒有宕機;機器還活着,就是各種操作特別慢;由於網絡擁塞導致一會網絡斷掉,不發送數據之后又好了,一旦探活之后重新使用又掛了等惡心的情況;
這些異常都需要根據實際情況來分析,在長期工程實踐中去調整解決。
並且令人非常沮喪的事實是:你在設計階段考慮的異常一定會在實際運行情況中遇到,你沒考慮到的異常也會在實際運行中遇到。所以分布式系統設計的一個原則是:不放過任何一個你看得到的異常。
-
-
讀寫過程一致性如何保證
一致性的概率很簡單,就是我更新/刪除請求返回之后,別人是否能讀到我新寫的這個值。對於單機系統,這個一致性要達到很簡單,大不了是損失一點寫的效率。但是對於分布式系統,這個就復雜了。為了容災,一份數據肯定有多個副本,那么如何更新這多個副本以及控制讀寫協議就成了一個大問題。
而且有的寫操作可能會跨越多個分片,這就更復雜了。再加上剛才提到的網絡故障,可能在同步數據的時候還會出現各種網絡故障,想想就頭疼。
而且即使達到了一致性,有可能讀寫性能也會受到很大損失。我們設計系統的時候就像一個滑動條,左邊是一致性,右邊是性能,兩者無法同時滿足(CAP原理)。一般的系統會取折衷,設計得比較好的系統能夠讓用戶通過配置來控制這個滑動條的位置,滿足不同類型的需求。
一致性一般怎么折衷呢?我們來看看如下幾種一致性的定義。注意除了強一致性以外,其他幾種一致性並不沖突,一個系統可以同時滿足一種或者幾種一致性特點。
-
強一致性
不用多說,就是最嚴格的一致性要求。任何時候任何用戶只要寫了,寫請求返回的一霎那,所有其他用戶都能讀到新的值了。
-
最終一致性
這個也是提得很多的一個概念,很多系統默認提供這種方式的一致性。即最終系統將將達到"強一致性"的狀態,但在之前會有一段不確定的時間,系統處於不一致的狀態。
-
會話一致性
這個也很容易理解,能滿足很多場景下的需求。在同一個會話當中,用戶感受到的是"強一致性"的服務。
-
單調一致性
這個比會話一致性還要弱一點。他之保證一個用戶在讀到某個數據之后,絕對不會讀到比上一次讀到的值更老的數據。
-
-
如何提高性能
分布式系統設計之初就是為了通過堆積機器來增加系統整體性能,所以系統性能也非常重要。性能部分一般會受一致性/容災等設計的影響,會有一定的折衷。
衡量一個分布式系統的性能指標往往有:
- 最大容量
- 讀qps
- 寫qps
-
如何保證橫向擴展
橫向擴展是指一個集群的服務能力是否可以通過加機器做到線性擴展。
上面簡單介紹了一個典型的分布式系統需要考慮的問題,提出了分布式系統設計的難點和問題,那么接下來我們就來看看典型分布式系統對這些問題是怎么解決的吧。
數據分布(sharding)
數據分布有兩個問題:
- 數據拆分問題。將一個大的文件/表格數據拆分成多份存儲;
- 數據落地問題。針對每份結果在所有機器中尋找一台機器來作為其存儲服務器;
數據拆分問題
數據拆分有如下幾種典型的方式:
-
hash拆分
這個是最簡單的能想到的拆分算法。將數據根據某個hash函數散列到其中一台機器上即可。
好處:
- 算法簡單,幾乎不需要master機器就能知道數據分布。這里說"幾乎"是因為一般的hash算法可能還需要用到總機器數量。
壞處:
- 可擴展性太差。需要增加/減少機器的時候幾乎需要挪動所有數據;
- 數據可能分布不均勻。一方面可能是因為數據量不夠大,hash算法還不能比較平均的三列;另一方面可能是用戶訪問數據就是不均勻的,典型的用戶使用場景都有可能存在2/8原則,小部分請求占據了絕大部分流量,即使是數據分布是均勻的,不代表訪問流量就能均勻分配。
- 不支持順序讀取數據,順序讀取數據壓力會比較大。
-
一致性hash拆分
一致性hash不做過多解釋,好處跟hash算法一樣,他解決了擴容/縮容/數據遷移的時候普通hash算法的大動干戈。
一致性hash算法的原理請參考這篇文章。
使用該方案的系統:
- Dynamo/Cassandra
-
按數據范圍拆分
這個方式也是非常常見的一種數據拆分方式,類似B+樹,按照存儲數據中某列或者某幾列的組合結果的范圍來判斷數據分布。
好處:
- 順序讀取數據比較友好
- 能比較容易的控制數據量分布。一般系統會實現每台機器負責范圍的動態合並和分裂,這樣就能比較好的動態控制每台機器的負載了。
壞處:
- 需要master服務器來維持范圍和機器的映射關系,增加系統的復雜度,以及master機器可能會成為整個分布式系統的瓶頸;
使用該方案的系統:
- BigTable
- HBase
-
按數據量拆分
當數據總量到達一定大小就拆分出來。這個一般用於分布式文件系統的大文件存儲的方案。
好處:
- 數據分布均勻,實現所有機器均衡使用的復雜度較低
壞處:
- 對數據修改和調整支持不好
- 同第3點,也需要一台專用的master機器來維護映射關系
- 對隨機查詢支持不好
使用該方案的系統:
- GFS
- HDFS
在實際系統中,我們也可以結合多種方式。比如先按照hash方式存儲,如果發現數據不夠均勻之后,再將不均勻的分片利用數據范圍或者數據量的方式做二次分片。這樣雖然系統實現復雜了,但是卻能達到數據分布均勻,同時master里面存儲的信息又大大減少的好處。
數據落地問題
數據落地算法一般分成兩類:
-
靜態分配
靜態分配是指在數據還沒進來的時候,就將資源給他分配好,並且按照如上的某種拆分算法做好相應初始化工作。
好處:
- 實現簡單
壞處:
- 需要提前預估該數據所需要的資源量
- 可能存在資源浪費
-
動態分配
動態分配則跟靜態分配相反,只有當數據需要新的分片的時候才給他分配真正的資源。
好處:
- 解決靜態分配的資源浪費問題和提前預估問題
壞處:
- 實現復雜
不管是靜態分配還是動態分配,一般來說,都需要整個集群所有機器的資源使用情況,然后利用貪心算法分配一個當次分配最適合的機器給這份數據。
在分布式系統中,數據落地還需要同時考慮副本分布的問題。一份數據的副本往往需要分配到不同的網段甚至地域避免單網段故障;另外,為了避免單台機器宕機的時候該台機器包含的所有流量全部壓到另外一台機器上去,所以所有副本的分布也要足夠的散列和均勻。
數據副本(replication)
數據副本的存在主要是為了避免單機宕機出現的服務停止的情況,增加整個分布式系統的可用性。
但是也是因為副本的存在,以及產品可能的對一致性的要求,會使得在讀寫過程中對副本的控制需要格外的小心。
一般來說,我們用副本控制協議來代表副本管理的方式。典型的副本控制協議又分成兩類:
- 中心化的副本控制協議
- 去中心化的副本控制協議
中心化的副本控制協議
顧名思義,在所有副本當中,會有一個副本作為中心副本,來控制其他副本的行為。可以看到這樣的話,系統的一致性控制實現將會變得很簡單,就類似單機系統的控制了。
在單機系統中要實現一致性控制,用本地鎖就好了。但是如果沒有中心副本,那么要實現一致性就需要一套復雜的分布式交互協議來達到一致性,將大大增加系統實現成本。
下面主要講講幾個最常見case中心化的副本協議操作流程:
-
寫數據
整體流程如下:
- 寫客戶端將寫請求發送給中心副本
- 中心副本確定更新方案
- 中心副本將數據按照既定方案發送給從副本
- 中心副本根據更新完成情況返回用戶成功/失敗
這里重點描述一下流程中提到的更新方案。
更新方案主要有兩種:
-
中心同步
中心同步的意思是由中心節點將數據串行或者並行的同步到所有其他副本上。
-
鏈式同步
鏈式同步是指中心節點之同步給一個副本,這個副本再同步給下一個副本。
這兩個方案其實差不多,最主要的差別是中心同步會比鏈式同步對主副本機器網卡造成更大的壓力。但是實際上因為有很多個數據分片,而數據分片對應的主副本在所有機器中是均勻分配的,所以雖然單分片壓力會增加,但整體集群的資源利用率的均衡程度還好。
-
查詢數據
查詢數據的邏輯跟一致性要求強相關。如果用戶只需要最終一致性,那么讀取任何副本都OK。如果用戶需要強一致性,那么就需要一個比較復雜的協議來控制了。
一般我們有如下幾種方案來實現強一致性:
-
只讀中心副本
這個是最簡單的方案。而且同上面對中心同步和鏈式同步的分析,對整體機器的均衡性影響也可以忽略。該方案最大的問題在於其他副本成了擺設,導致系統的最大qps和吞吐都只限單機。
-
標記副本狀態
這也是很常見的方案,每個副本上都帶上一個版本號,版本號是遞增不減的。在主副本中維護一個當前版本號的信息。
當主副本認為數據更新成功之后,會更新當前版本號。每次讀數據之前,會先得到當前版本號的信息,來選擇版本號一致的副本進行查詢。
這里有一個概念,主副本認為數據更新成功。一般主副本怎樣才認為數據更新成功而不是失敗呢?一般系統有兩種做法:
-
全部寫成功才算成功
這個方案實現簡單,就是對寫數據的可用性有大的損失。因為只要有一個副本有問題,這個副本的所有寫請求都會失敗。
有很多系統,就使用的這個方案,不過一般都會加以優化,來提高系統的寫請求可用性。比如在GFS中,如果發現寫一個副本失敗了,會嘗試另外創建一個副本,只要新副本寫成功了,就OK。不過像這種優化方案不適用於一些副本比較大的系統,並且需要增加過時副本回收機制。
-
寫入部分副本就算成功
更多的系統是給定一個配置值,主要寫入這個配置值對應的副本數就算成功。一般這個配置的值需要超過一半。這個方式還有一個固定的名字:quorum算法。
該算法的定義如下:
假設有N個副本,每次寫W個副本就算成功,那么在讀數據的時候只要讀(N-W+1)個副本的版本信息,就起碼能讀到至少一個正確更新的副本。
一般配合quorum算法,還需要在主副本或者master中保存一下當前副本的最新版本的信息,如果不保存這個信息的話,最壞情況下,就需要讀取全部副本的版本信息,來確定到底哪個版本是當前正確的版本。
現在系統中絕大部分系統都是采用quorum算法的思想來實現的。
-
-
-
異常處理
典型的異常包括:
-
從副本掛掉
問題發現:定期從副本與主副本之間的心跳/租約機制。寫數據的時候異常。
問題解決:通過標記狀態或者版本控制的方式來解決。
-
主副本掛掉
問題發現:通過主副本與master之間的心跳/租約機制。
問題解決:重新指定一個從副本作為主副本。
-
去中心化的副本控制協議
去中心化協議實現相當復雜,為了保證多個副本之間的信息同步,一般需要多輪交互才能達成一致。在實際工程項目中,都是使用的paxos協議及其變種來作為數據一致性協議的。
在現有系統中,主要有兩類系統實現了去中心化的副本協議:
-
chubby/zookeeper
chubby是google提出來的專門做分布式鎖的系統,是第一個將paxos這個學術上的東西帶進了工業界。zookeeper是chubby的開源實現。
paxos就類似選舉,大家都提出自己的意見,最后大家經過一輪又一輪的投票,直到一個人獲得多數票,然后大家就按照這個人的意見來執行,從而達到統一大家意見的目的。
關於這兩個系統的介紹和對比請參考這篇文章。
-
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)兩個值,哪個值是正確的就用戶自己來判斷了。
事務支持
事務典型的例子就是銀行轉賬,一個賬戶減錢,一個賬戶加錢,要么都成功,要么都失敗,不能有中間狀態。
事務支持主要有兩種方案:
-
加鎖
加鎖是最簡單的做法。根據事務涉及到的范圍,又分成表鎖/行鎖。在鎖定期間,其他寫操作需要排隊等待。而且讀操作也必須等待,不然就有可能讓用戶讀到一半事務的值,比如賬戶扣錢了,另外一個賬戶錢還沒漲。
所以這樣就會造成系統性能降低,尤其是讀性能還會受到蠻大影響。
-
MVCC
為了避免加鎖造成的讀等待問題,就很自然的想到給一份數據保存多個版本,在事務執行到一半的時候,已經執行的那些行數據老數據還在,讀請求還用老數據來響應,這樣就不會讓讀請求給hang住了。
在事務執行完畢之后,如果成功,就把新結果合並成真正的數據,從此以后新的讀請求就會讀到事務過后的新數據了。
同時,如果有多個寫事務同時在執行的話,就需要保存多份數據版本,並且在最后合並的時候可能還需要涉及到一定的merge邏輯,merge邏輯跟自身系統的業務特點有關。