ceph存儲分佈式系統設計系列 -- 基本原理及高可用策略

https://blog.csdn.net/skdkjzz/article/details/50737241?utm_source=blogxgwz1

“分佈式系統設計”系列第一篇文章,這篇文章主要介紹一些入門的概念和原理,後面帶來一些高可用、數據分佈的實踐方法!!

 

==> 分佈式系統中的概念
==> 分佈式系統與單節點的不同
==> 分佈式系統特性
==> 分佈式系統設計策略
==> 分佈式系統設計實踐
 

 

 

【分佈式系統中的概念】

        三元組   

                其實,分佈式系統說白了,就是很多機器組成的集羣,靠彼此之間的網絡通信,擔當的角色可能不同,共同完成同一個事情的系統。如果按”實體“來劃分的話,就是如下這幾種:
                1、節點 -- 系統中按照協議完成計算工作的一個邏輯實體,可能是執行某些工作的進程或機器
                2、網絡 -- 系統的數據傳輸通道,用來彼此通信。通信是具有方向性的。
                3、存儲 -- 系統中持久化數據的數據庫或者文件存儲。

                如圖
                
 

        狀態特性

                各個節點的狀態可以是“無狀態”或者“有狀態的”.

                一般認爲,節點是偏計算和通信的模塊,一般是無狀態的。這類應用一般不會存儲自己的中間狀態信息,比如Nginx,一般情況下是轉發請求而已,不會存儲中間信息。另一種“有狀態”的,如mysql等數據庫,狀態和數據全部持久化到磁盤等介質。

                “無狀態”的節點一般我們認爲是可隨意重啓的,因爲重啓後只需要立刻工作就好。“有狀態”的則不同,需要先讀取持久化的數據,才能開始服務。所以,“無狀態”的節點一般是可以隨意擴展的,“有狀態”的節點需要一些控制協議來保證擴展。
 

        系統異常

                異常,可認爲是節點因爲某種原因不能工作,此爲節點異常。還有因爲網絡原因,臨時、永久不能被其他節點所訪問,此爲網絡異常。在分佈式系統中,要有對異常的處理,保證集羣的正常工作。

 

【分佈式系統與單節點的不同】

 

          1、從linux write()系統調用說起

         衆所周知,在unix/linux/mac(類Unix)環境下,兩個機器通信,最常用的就是通過socket連接對方。傳輸數據的話,無非就是調用write()這個系統調用,把一段內存緩衝區發出去。但是可以進一步想一下,write()之後能確認對方收到了這些數據嗎?

         答案肯定是不能,原因就是發送數據需要走內核->網卡->鏈路->對端網卡->內核,這一路徑太長了,所以只能是異步操作。write()把數據寫入內核緩衝區之後就返回到應用層了,具體後面何時發送、怎麼發送、TCP怎麼做滑動窗口、流控都是tcp/ip協議棧內核的事情了。

         所以在應用層,能確認對方受到了消息只能是對方應用返回數據,邏輯確認了這次發送才認爲是成功的。這就卻別與單系統編程,大部分系統調用、庫調用只要返回了就說明已經確認完成了。
 

         2、TCP/IP協議是“不可靠”的

         教科書上明確寫明瞭互聯網是不可靠的,TCP實現了可靠傳輸。何來“不可靠”呢?先來看一下網絡交互的例子,有A、B兩個節點,之間通過TCP連接,現在A、B都想確認自己發出的任何一條消息都能被對方接收並反饋,於是開始瞭如下操作:
         A->B發送數據,然後A需要等待B收到數據的確認,B收到數據後發送確認消息給A,然後B需要等待A收到數據的確認,A收到B的數據確認消息後再次發送確認消息給B,然後A又去需要等待B收到的確認。。。死循環了!!
 

         其實,這就是著名的“拜占庭將軍”問題:

         http://baike.baidu.com/link?url=6iPrbRxHLOo9an1hT-s6DvM5kAoq7RxclIrzgrS34W1fRq1h507RDWJOxfhkDOcihVFRZ2c7ybCkUosWQeUoS_


         所以,通信雙方是“不可能”同時確認對方受到了自己的信息。而教科書上定義的其實是指“單向”通信是成立的,比如A向B發起Http調用,收到了HttpCode 200的響應包,這隻能確認,A確認B收到了自己的請求,並且B正常處理了,不能確認的是B確認A受到了它的成功的消息。

 

         3、不可控的狀態


         在單系統編程中,我們對系統狀態是非常可控的。比如函數調用、邏輯運算,要麼成功,要麼失敗,因爲這些操作被框在一個機器內部,cpu/總線/內存都是可以快速得到反饋的。開發者可以針對這兩個狀態很明確的做出程序上的判斷和後續的操作。
         而在分佈式的網絡環境下,這就變得微妙了。比如一次rpc、http調用,可能成功、失敗,還有可能是“超時”,這就比前者的狀態多了一個不可控因素,導致後面的代碼不是很容易做出判斷。試想一下,用A用支付寶向B轉了一大筆錢,當他按下“確認”後,界面上有個圈在轉啊轉,然後顯示請求超時了,然後A就抓狂了,不知道到底錢轉沒轉過去,開始確認自己的賬戶、確認B的賬戶、打電話找客服等等。

         所以分佈式環境下,我們的其實要時時刻刻考慮面對這種不可控的“第三狀態”設計開發,這也是挑戰之一。
 

         4、視”異常“爲”正常“

         單系統下,進程/機器的異常概率十分小。即使出現了問題,可以通過人工干預重啓、遷移等手段恢復。但在分佈式環境下,機器上千臺,每幾分鐘都可能出現宕機、死機、網絡斷網等異常,出現的概率很大。所以,這種環境下,進程core掉、機器掛掉都是需要我們在編程中認爲隨時可能出現的,這樣才能使我們整個系統健壯起來,所以”容錯“是基本需求。

         異常可以分爲如下幾類:

         節點錯誤:

                  一般是由於應用導致,一些coredump和系統錯誤觸發,一般重新服務後可恢復。

        硬件錯誤:

                  由於磁盤或者內存等硬件設備導致某節點不能服務,需要人工干預恢復。 

        網絡錯誤:

                  由於點對點的網絡抖動,暫時的訪問錯誤,一般拓撲穩定後或流量減小可以恢復。

        網絡分化:

                  網絡中路由器、交換機錯誤導致網絡不可達,但是網絡兩邊都正常,這類錯誤比較難恢復,並且需要在開發時特別處理。【這種情況也會比較前面的問題較難處理】

 

【分佈式系統特性】

         CAP是分佈式系統裏最著名的理論,wiki百科如下

                Consistency(all nodes see the same data at the same time)
                Availability (a guarantee that every request receives a response about whether it was successful or failed)
                Partition tolerance (the system continues to operate despite arbitrary message loss or failure of part of the system)
                                (摘自 :http://en.wikipedia.org/wiki/CAP_theorem)


          早些時候,國外的大牛已經證明了CAP三者是不能兼得,很多實踐也證明了。
          本人就不挑戰權威了,感興趣的同學可以自己Google。本人以自己的觀點總結了一下:

          一致性
                    描述當前所有節點存儲數據的統一模型,分爲強一致性和弱一致性:
                    強一致性描述了所有節點的數據高度一致,無論從哪個節點讀取,都是一樣的。無需擔心同一時刻會獲得不同的數據。是級別最高的,實現的代價比較高
                    如圖:
    
                              弱一致性又分爲單調一致性和最終一致性:
                              1、單調一致性強調數據是按照時間的新舊,單調向最新的數據靠近,不會回退,如:
                                   數據存在三個版本v1->v2->v3,獲取只能向v3靠近(如取到的是v2,就不可能再次獲得v1)
                              2、最終一致性強調數據經過一個時間窗口之後,只要多嘗試幾次,最終的狀態是一致的,是最新的數據
                                    如圖:



                              強一致性的場景,就好像交易系統,存取錢的+/-操作必須是馬上一致的,否則會令很多人誤解。
                              弱一致性的場景,大部分就像web互聯網的模式,比如發了一條微博,改了某些配置,可能不會馬上生效,但刷新幾次後就可以看到了,其實弱一致性就是在系統上通過業務可接受的方式換取了一些系統的低複雜度和可用性。


          可用性
                    保證系統的正常可運行性,在請求方看來,只要發送了一個請求,就可以得到恢復無論成功還是失敗(不會超時)!


         分區容忍性
                    在系統某些節點或網絡有異常的情況下,系統依舊可以繼續服務。
                    這通常是有負載均衡和副本來支撐的。例如計算模塊異常可通過負載均衡引流到其他平行節點,存儲模塊通過其他幾點上的副本來對外提供服務。

          擴展性
                    擴展性是融合在CAP裏面的特性,我覺得此處可以單獨講一下。擴展性直接影響了分佈式系統的好壞,系統開發初期不可能把系統的容量、峯值都考慮到,後期肯定牽扯到擴容,而如何做到快而不太影響業務的擴容策略,也是需要考慮的。(後面在介紹數據分佈時會着重討論這個問題)


 

【分佈式系統設計策略】

 

          1、重試機制

 

          一般情況下,寫一段網絡交互的代碼,發起rpc或者http,都會遇到請求超時而失敗情況。可能是網絡抖動(暫時的網絡變更導致包不可達,比如拓撲變更)或者對端掛掉。這時一般處理邏輯是將請求包在一個重試循環塊裏,如下:

 

[cpp] view plain copy

  1. int retry = 3;  
  2. while(!request() && retry--)  
  3.     sched_yield();   // or usleep(100)  

 
  1. int retry = 3;

  2. while(!request() && retry--)

  3. sched_yield(); // or usleep(100)

 

 

          此種模式可以防止網絡暫時的抖動,一般停頓時間很短,並重試多次後,請求成功!但不能防止對端長時間不能連接(網絡問題或進程問題)
 

          2、心跳機制

          心跳顧名思義,就是以固定的頻率向其他節點彙報當前節點狀態的方式。收到心跳,一般可以認爲一個節點和現在的網絡拓撲是良好的。當然,心跳彙報時,一般也會攜帶一些附加的狀態、元數據信息,以便管理。如下圖:
 


          但心跳不是萬能的,收到心跳可以確認ok,但是收不到心跳卻不能確認節點不存在或者掛掉了,因爲可能是網絡原因倒是鏈路不通但是節點依舊在工作。
          所以切記,”心跳“只能告訴你正常的狀態是ok,它不能發現節點是否真的死亡,有可能還在繼續服務。(後面會介紹一種可靠的方式 -- Lease機制)

 

          3、副本


          副本指的是針對一份數據的多份冗餘拷貝,在不同的節點上持久化同一份數據,當某一個節點的數據丟失時,可以從副本上獲取數據。數據副本是分佈式系統解決數據丟失異常的僅有的唯一途徑。當然對多份副本的寫入會帶來一致性和可用性的問題,比如規定副本數爲3,同步寫3份,會帶來3次IO的性能問題。還是同步寫1份,然後異步寫2份,會帶來一致性問題,比如後面2份未寫成功其他模塊就去讀了(下個小結會詳細討論如果在副本一致性中間做取捨)。

 

          4、中心化/無中心化


          系統模型這方面,無非就是兩種:
          中心節點,例如mysql的MSS單主雙從、MongDB Master、HDFS NameNode、MapReduce JobTracker等,有1個或幾個節點充當整個系統的核心元數據及節點管理工作,其他節點都和中心節點交互。這種方式的好處顯而易見,數據和管理高度統一集中在一個地方,容易聚合,就像領導者一樣,其他人都服從就好。簡單可行。
          但是缺點是模塊高度集中,容易形成性能瓶頸,並且如果出現異常,就像羣龍無首一樣。

 

          無中心化的設計,例如cassandra、zookeeper,系統中不存在一個領導者,節點彼此通信並且彼此合作完成任務。好處在於如果出現異常,不會影響整體系統,局部不可用。缺點是比較協議複雜,而且需要各個節點間同步信息。

 

【分佈式系統設計實踐】


          基本的理論和策略簡單介紹這麼多,後面本人會從工程的角度,細化說一下”數據分佈“、"副本控制"和"高可用協議"

          在分佈式系統中,無論是計算還是存儲,處理的對象都是數據,數據不存在於一臺機器或進程中,這就牽扯到如何多機均勻分發數據的問題,此小結主要討論"哈希取模",”一致性哈希“,”範圍表劃分“,”數據塊劃分“
 

          1、哈希取模:

                    哈希方式是最常見的數據分佈方式,實現方式是通過可以描述記錄的業務的id或key(比如用戶 id),通過Hash函數的計算求餘。餘數作爲處理該數據的服務器索引編號處理。如圖:
                    

                    這樣的好處是只需要通過計算就可以映射出數據和處理節點的關係,不需要存儲映射。難點就是如果id分佈不均勻可能出現計算、存儲傾斜的問題,在某個節點上分佈過重。並且當處理節點宕機時,這種”硬哈希“的方式會直接導致部分數據異常,還有擴容非常困難,原來的映射關係全部發生變更。
 

                    此處,如果是”無狀態“型的節點,影響比較小,但遇到”有狀態“的存儲節點時,會發生大量數據位置需要變更,發生大量數據遷移的問題。這個問題在實際生產中,可以通過按2的冪的機器數,成倍擴容的方式來緩解,如圖:

 

                   

                    不過擴容的數量和方式後收到很大限制。下面介紹一種”自適應“的方式解決擴容和容災的問題。

 

          2、一致性哈希:

          一致性哈希 -- Consistent Hash 是使用一個哈希函數計算數據或數據特徵的哈希值,令該哈希函數的輸出值域爲一個封閉的環,最大值+1=最小值。將節點隨機分佈到這個環上,每個節點負責處理從自己開始順
          時針至下一個節點的全部哈希值域上的數據,如圖:
          

################################################3

          一致性哈希的優點在於可以任意動態添加、刪除節點,每次添加、刪除一個節點僅影響一致性哈希環上相鄰的節點。 爲了儘可能均勻的分佈節點和數據,一種常見的改進算法是引入虛節點的概念,系統會創建許多虛擬節點,個數遠大於當前節點的個數,均勻分佈到一致性哈希值域環上。讀寫數據時,首先通過數據的哈希值在環上找到對應的虛節點,然後查找到對應的real節點。這樣在擴容和容錯時,大量讀寫的壓力會再次被其他部分節點分攤,主要解決了壓力集中的問題。如圖:

  


 

 

          3、數據範圍劃分:

          有些時候業務的數據id或key分佈不是很均勻,並且讀寫也會呈現聚集的方式。比如某些id的數據量特別大,這時候可以將數據按Group劃分,從業務角度劃分比如id爲0~10000,已知8000以上的id可能訪問量特別大,那麼分佈可以劃分爲[[0~8000],[8000~9000],[9000~1000]]。將小訪問量的聚集在一起。
          這樣可以根據真實場景按需劃分,缺點是由於這些信息不能通過計算獲取,需要引入一個模塊存儲這些映射信息。這就增加了模塊依賴,可能會有性能和可用性的額外代價。
 

          4、數據塊劃分:


          許多文件系統經常採用類似設計,將數據按固定塊大小(比如HDFS的64MB),將數據分爲一個個大小固定的塊,然後這些塊均勻的分佈在各個節點,這種做法也需要外部節點來存儲映射關係。
          由於與具體的數據內容無關,按數據量分佈數據的方式一般沒有數據傾斜的問題,數據總是被均勻切分並分佈到集羣中。當集羣需要重新負載均衡時,只需通過遷移數據塊即可完成。

          如圖:
          


          大概說了一下數據分佈的具體實施,後面根據這些分佈,看看工程中各個節點間如何相互配合、管理,一起對外服務。

 

                    1、paxos

  paxos很多人都聽說過了,這是唯一一個被認可的在工程中證實的強一致性、高可用的去中心化分佈式協議。
雖然論文裏提到的概念比較複雜,但基本流程不難理解。本人能力有限,這裏只簡單的闡述一下基本原理:
Paxos 協議中,有三類角色: 
Proposer:Proposer 可以有多個,Proposer 提出議案,此處定義爲value。不同的 Proposer 可以提出不同的甚至矛盾的 value,例如某個 Proposer 提議“將變量a設置爲x1” ,另一個 Proposer 提議“將變量a設置爲x2” ,但對同一輪 Paxos過程,最多隻有一個 value 被批准。 
Acceptor: 批准者。 Acceptor 有 N 個, Proposer 提出的 value 必須獲得超過半數(N/2+1)的 Acceptor批准後才能通過。Acceptor 之間對等獨立。 
Learner:學習者。Learner 學習被批准的 value。所謂學習就是通過讀取各個 Proposer 對 value的選擇結果, 如果某個 value 被超過半數 Proposer 通過, 則 Learner 學習到了這個 value。從而學習者需要至少讀取 N/2+1 個 Accpetor,至多讀取 N 個 Acceptor 的結果後,能學習到一個通過的 value。


  paxos在開源界裏比較好的實現就是zookeeper(類似Google chubby),zookeeper犧牲了分區容忍性,在一半節點宕機情況下,zookeeper就不可用了。可以提供中心化配置管理下發、分佈式鎖、選主等消息隊列等功能。其中前兩者依靠了Lease機制來實現節點存活感知和網絡異常檢測。
 

                    2、Lease機制

  Lease英文含義是”租期“、”承諾“。在分佈式環境中,此機制描述爲:
  Lease 是由授權者授予的在一段時間內的承諾。授權者一旦發出 lease,則無論接受方是否收到,也無論後續接收方處於何種狀態,只要 lease 不過期,授權者一定遵守承諾,按承諾的時間、內容執行。接收方在有效期內可以使用頒發者的承諾,只要 lease 過期,接
收方放棄授權,不再繼續執行,要重新申請Lease。
                             如圖:

                              

                             


  Lease用法舉例1:
  現有一個類似DNS服務的系統,數據的規律是改動很少,大量的讀操作。客戶端從服務端獲取數據,如果每次都去服務器查詢,則量比較大。可以把數據緩存在本地,當數據有變動的時候重新拉取。現在服務器以lease的形式,把數據和lease一同推送給客戶端,在lease中存放承諾該數據的不變的時間,然後客戶端就可以一直放心的使用這些數據(因爲這些數據在服務器不會發生變更)。如果有客戶端修改了數據,則把這些數據推送給服務器,服務器會阻塞一直到已發佈的所有lease都已經超時用完,然後後面發送數據和lease時,更新現在的數據。

  這裏有個優化可以做,當服務器收到數據更新需要等所有已經下發的lease超時的這段時間,可以直接發送讓數據和lease失效的指令到客戶端,減小服務器等待時間,如果不是所有的lease都失效成功,則退化爲前面的等待方案(概率小)。



  Lease用法舉例2:

  現有一個系統,有三個角色,選主模塊Manager,唯一的Master,和其他salver節點。slaver都向Maganer註冊自己,並由manager選出唯一的Master節點並告知其他slaver節點。當網絡出現異常時,可能是Master和Manager之間的鏈路斷了,Master認爲Master已經死掉了,則會再選出一個Master,但是原來的Master對其他網絡鏈路可能都還是正常的,原來的Master認爲自己還是主節點,繼續服務。這時候系統中就出現了”雙主“,俗稱”腦裂“。
  解決這個問題的方式可以通過Lease,來規定節點可以當Master的時間,如果沒有可用的Lease,則自動退化爲Slaver。如果出現”雙主“,原Master會因爲Lease到期而放棄當Master,退化爲Slaver,恢復了一個Master的情況。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章