Akka官方文件翻譯:Cluster Specification

devos發表於2015-03-10

參加了CSDN的一個翻譯專案,翻譯Akka的文件。CSDN提供的翻譯系統不好使,故先排版一下放在部落格上。

5.1 叢集規範


注意:本文件介紹了叢集的設計理念。它分成兩部分,第一部分描述了當前已經實現的部分,第二部分描述了未來要增強/增加的部分。對未現部分的引用被用腳註[*]標出。


5.1.1 目前的叢集

簡介

    Akka叢集提供了一個容錯的、去中心化的、基於點對於點的叢集成員關係服務,沒有單點故障,也沒有單點瓶頸。為了實現這些,它使用了gossip協議和一個自動故障檢測器。

術語

節點 叢集的一個邏輯成員。在一個物理機器上可以有多個節點。使用hostname:port:uid的元組來標識。

叢集 通過成員關係服務組合在一起的一些節點。

leader 叢集中的唯一一個作為leader的節點。管理叢集收斂,分割槽[*],容錯[*],再平衡(rebalancing)[*]等。

 

成員關係

  一個叢集由一組成員節點構成。每個節點的識別符號是一個由hostname:port:uid組成的元組。

  一個Akka應用程式可以分佈於一個叢集中,每個節點做為部分程式的宿主。叢集的成員關係和程式的分割槽是[*]解耦合的。一個節點可以是一個叢集的成員,同時不做為任何actor的宿主。加入叢集的動作,由向想要加入的叢集中的一個節點傳送一個Join命令來發起。

    節點識別符號內部包括一個UID,來唯一標識hostname:port處的那個actor system。Akka使用這個UID來可靠地觸發遠端死亡監視(remote death watch)。這意味著同一個actor system一旦被從叢集中移除就再不能加入這個叢集。想要把一個有著同樣hostname:port的actor system重新加入叢集,你必須著先停止這個actor system, 並且用同樣的hostname:port啟動一個新的actor system,這個新的actor system將會獲得一個不同的UID。

  叢集成員關係狀態是一個專門的CRDT,這意味著它有一個收斂的合併函式。當改變併發地發生於不同節點時,這些更新總能被合併以及收斂為同樣的最終結果。

 

Gossip

  Akka使用的叢集成員關係基於Amzaon的Dynamo系統,尤其是Basho‘s的Riak分散式資料庫所採用的方法。叢集成員關係通過Gossip協議來交流,當前叢集的狀態隨機地在叢集中傳播,並且傾向於沒有見到過最新版本的成員。

 

向量時鐘  向量時鐘是一種資料結構和演算法,用來在分散式系統中生成事件的偏序關係並且檢測違背因果關係的情況。

  我們使用向量時鐘來在流言傳播過程中合併叢集狀態的差異,並且使其一致。向量時鐘是一對一對(節點,計數器)的集合。每次叢集狀態的更新都會附帶對向量時鐘的更新。

 

Gossip收斂   關於叢集的資訊在某個時間點在本地收斂。這個時間點就是當一個節點能夠證明它現在觀察到的叢集狀態已經被叢集其它所有的節點觀察到。通過在流言傳播過程中傳遞已經看到當前狀態的節點集來實現收斂。此資訊在gossip概述中被稱為已見集合(seen set)。當所有的節點都在已見集合中時,就會有一次收斂。

  只要有任何節點處於不可達(unreachable)狀態,流言收斂都不會發生。該節點需要重新變成可達(reachable),或者變成失效(down)和已移處(removed)狀態(參見下節中的成員關係生命週期)。這只是阻止leader進行叢集成員關係管理,並不影響執行於叢集之上的應用程式。例如,這意味著,在網路分割槽時,無法往叢集中增加更多節點。節點可以加入,但直到不再分割槽或者不可達的節點變成失效(down),他們才能被移動到啟用(up)狀態。

 

故障探測器   故障探測器負責檢測是否一個節點對於叢集的其它節點是不可達的。為此我們使用了對Hayashibara等人的The Phi Accrual Failure Detector的一個實現。

  累積故障檢測器把監控和解釋解耦合。這使得它們適用於更廣闊的場景,並且更勝任於構建通用的故障檢測服務。它的思想是:通過計算從其它節點收到的心跳資訊來維護一個故障歷史記錄,通過考慮多個因素、以及它們隨時間的累積來做有依據的猜測,來對一個節點是up還是down做一個更好的猜測。它返回一個phi值代表一個節點down的可能性,而不是簡單地對“這個節點是否down了?”這個問題做“是”或“否”的回答。

  這項計算所依據的閥值是由使用者設定的。一個較低的閥值容易產生很多錯誤的猜測,但是當一個真正的故障(crash)發生時,它能保證快速發現。與此相對的是,高閥值會產生成少的錯誤,但是會花更多的時間才能檢測到實際發生的故障。預設閾值為8,適合於大多數情況。然而,在雲環境中,比如Amazon EC2,該值可以提高到12,以應對在這樣的平臺上有時會發生的網路問題。

  在叢集中,每個節點被一些(預設最大為5)其它節點監控,當其中任何一個監測點節檢測到這個被監測節點不可達,這個不可達資訊就會通過流言傳播到節點的其它部分。換句話說,只要有一個節點標記某節點不可達,叢集中的其它節點都會標記這個節點不可達。

  監控節點從一個雜湊有序的節點環(hashed ordered node ring)的相鄰節點中選出。這是為了增加跨機架和資料中心監控的可能性,但是這個順序(譯註:即上一句中的“有序”)對於所有節點都是相同的,這樣保證了完全收斂。

  心跳每一秒傳送一次,每個心跳由一個請求/回覆握手來實現,其中的回覆被用於故障檢測的輸入。

  故障檢測也會檢測一個節點是否重新變成可達。當所有負責檢測那個不可達節點的節點都檢測到它重新變成可達,在流言擴散以後,這個節點被視為可達。

  如果系統訊息不能被送達一個節點,這個節點將被隔離,並且它再不能從不可達狀態中回來。如果有太多未確認的系統訊息(比如watch, Terminated,actor遠端部署,由遠端家長監督的actor失效), 就可能會發生這種情況。然後這個節點需要被移動到down或者removed狀態(見下節的成員關係生命週期),並且這個actor system需要在重新加入叢集之前重啟。

 

leader   流言收斂之後,可以確定一個叢集的leader。

  並不存在leader選舉的過程,無論何時發生了流言的收斂,任何節點都總能確定地識別leader.leader只是一個角色,任何節點都可以成為leader,並且它可以在收斂的輪次之間改變。leader僅僅是可以承擔領導角色的節點進行排序後的第一個節點,leader更傾向於的成員狀態是up和leaving(參見下面關於成員關係生命週期的一節中關於成員狀態的內容)

  leader的職責是把成員移進和移出叢集,將加入叢集的成員改為up狀態或者把成員移出至removed狀態。當前leader的行為只在流言收斂,接收到一個新的叢集狀態時被觸發。

  如果進行了配置,leader也可以擁有"自動關閉“(auto-down)一個故障檢測器認為不可達的節點的權利。這意味著在配置的不可達時間過去後,自動設定不可達節點的狀態為down。

 

種子節點    種子節點是被配置為作為新節點加入叢集時的聯絡點。當一個新節點啟動時,它給所有的種子節點傳送一條訊息,然後傳送join命令給第一個回覆的種子節點。

  種子節點的配置信對於執行中的叢集沒有任何影響,它僅與新節點的加入有關,因為它幫助新節點發現聯絡點來傳送join命令。新成員可以傳送join命令到叢集的任何當前成員,而不僅是種子節點。

 

Gossip協議   一個push-pull gossip的變種被用來減少在叢集中傳播的gossip資訊的大小。在push-pull gossip中,傳送的是代表當前版本的摘要,而不是實際的值。gossip的接收者可以傳送回它擁有的更新版本的值,也可以請求它持有的過期版本的新值。Akka使用單一的共享狀態,以一個向量時鐘來做版本控制。所以,Akka所使用的push-pull gossip的變種使用此版本來只在需要時推送實際狀態。

  每隔一段時間,預設為1秒,每個節點選擇另一個隨機的節點來發起一輪gossip.如果在seen set中的節點少於一半,那麼叢集會每秒gossip三次而不是一次。這樣調整了gossip的間隔,使得在狀態改變後,收斂過程的早期傳播階段加速。

  選擇與哪個節點gossip是隨機的,但是傾向於可能還沒見到過當前狀態版本的節點。在每輪gossip中,如果沒有收斂,就使用0.8(可配置)的概率來與一個不在seen set中的節點gossip,也就是,它可能有一個更舊版本的狀態。否則的話,就隨機與任何活著的節點gossip.

  這種存在偏向的選擇是一種在狀態改變後,在接下來的傳播階段的後期加速收斂的方法。

  對於大於400(可配置, 建議根據實際表現配置)個節點的叢集,0.8這個概率逐漸降低以避免太多併發的gossip請求壓倒單個落後的節點。gossip的接收者通過丟棄進入mailbox時間太長的訊息,來保護自己不受過多同時到達的gossip訊息的損害。

  當叢集處於收斂狀態時,參與gossip的人只傳送包含gossip版本的很小的gossip status資訊給被選擇的節點。只要叢集有所改變(也就是不收斂),那麼就會立即變回有偏向的gossip。

  gossip state或者gossip status(譯註指某個節點在其叢集成員關係生命週期中處於的狀態)的接收者可以使用gossip版本(向量時鐘)來決定:

  1. 它有一個gossip state的新版本,此時它會把這個新的gossip state傳送給gossip的傳送者。

  2. 它有一個過期的版本。此時,接受者會把它的gossip state給傳送者來請求當前狀態。

  3. 它有一個有衝突的版本,此時,這些不同的版本會被合併,然後傳送回。

  如果傳送者和接收者的版本相同,那麼gossip state不會被髮送或者請求。

  gossip的週期性性質對於狀態變化有優良的批量效應,比如,往一個節點很快地連續加入多個節點將僅會帶來一個要傳播到叢集其它節點的狀態改變

  gossip訊息使用protobuf序列化,並且使用gzip壓縮以減少負載大小。

 

成員關係生命週期  

 節點開始時處於joinning狀態。一旦所有節點都看到了這個新節點在嘗試加入(通過gossip收斂),leader將會把這個節點的成員狀態設為up.

  如果一個節點使用安全的、符合預期的方式離開叢集,它會切換為leaving狀態。一旦leader看到對於處於節點的狀態收斂於leaving狀態,leader將會把它置於exiting狀態。一旦所有節點都看到了這個exiting狀態(收斂), leader將會把這個節點從叢集移除,把它標記為removed狀態。

  如果有節點不可達,那麼gossip收斂就不可能,因此任何leader行為也都不可能(比如,允許一個節點成為叢集的一部分)為了能夠繼續,不可達節點的狀態必須改變。它必須再次可達,或者被標記為down。如果節點想要重新加入叢集,它的actor system必須重啟並且重新經歷加入叢集的步驟。叢集可以通過leader,在不可達一段時間以後,auto-down一個節點。

 


 

注意: 當你啟用了auto-down以及故障探測器,如果你不採用措施來關閉不可達的(shut down)節點,那麼隨著時間推移,你可能會得到很多單節點的叢集。這遵循以下事實:不可達的節點很可能也會把叢集中的其它節點視為不可達,因此成為自己的leader, 並且形成自已的叢集。 


 

 

成員狀態的狀態圖

成員狀態

  • joining 加入叢集時的短暫狀態
  • up 正常工作狀態
  • leaving/exiting 優雅地脫離叢集時的狀態
  • down 被標記為關閉(從此與叢集的決定無關)
  • removed 墓碑狀態(不再是成員)

使用者動作

  • join 加入一個節點至叢集中-可以是顯式地,或者,如果在配置中指明瞭要加入的節點那麼可以在啟動時自動加入。
  • leave 讓一個節點優雅地離開叢集
  • down 把一個節點標記為down

Leader動作    leader有以下職責:

  • 將成員移入移出叢集

    --   joinging -> up

    --  exiting -> removed

故障探測以及不可達性

  • fd*    某個監視者節點的故障探測器被觸發,使得被監視的節點被標記為不可達
  • 不可達*    不可達並不真的是一個成員狀態,它更像是一個成員狀態的一個額外標誌,標識叢集不能被這個節點通訊。在不可達之後,故障探測器可以探測到它重新可達並且去除不可達標誌。

5.1.2 未來對叢集的增強和新增

目標

在提供成員關係之外,提供actor的自動分割槽(auto partitioning)[*], 切換(handoff)[*],以及叢集重新平衡[*]功能

附加術語

這些附加的術語在本節中被使用

分割槽(partition) [*]  被分佈到叢集中的Akka應用中的一個actor或一個actor子樹

分割槽點(parition point) [*]  位於分割槽頭部的actor。一個分割槽圍繞著這個點形成。

分割槽路徑(parition path) [*]  也被稱為actor地址。有actor1/actor2/actor3這樣的格式。

例項計數(instance count) [*]  一個分割槽在叢集中的例項數量。也被稱為分割槽的N值。

例項節點(instance node) [*]  某個actor例項被分配到的那個節點。

分割槽表(partition table) [*]  從分割槽路徑到例項節點集的對映(把node排序,使用排序後的順序位置來表示一個節點)

 

分割槽[*]

 


 注意:actor分割槽還沒有實現


 

  actor system中的每個分割槽(一個actor或者actor子樹)被分配置叢集中的一個節點集。這個分割槽頭部的actor被稱為分割槽點(parition point)。從分割槽路徑(像"a/b/c"這樣的actor地址)到節點例項們的對映被存在分割槽表裡,並且被使用gossip協議作為叢集狀態的一部分維護。分割槽表僅由leader節點更新。當前只有路徑節點(routed actors)可以被作為分割槽點。

  路由節點可以有一個大於1的例項計數。例項計數也被稱為N值。如果N值大於1,那麼在分割槽表裡會給出一個例項節點集合。

  注意在第一版實現裡可能會限制只有頂級的分割槽是可行的(只使用能用的最高的分割槽點,子分割槽是不被允許的)。還需要做更詳細的探討。

  Cluster使用兩個座標來決定一個分割槽的當前例項數:容錯和擴充套件。

  容錯決定了一個路由actor的最小例項數目(允許在N-1個節點崩潰時,至少有一個執行中的actor例項)。使用者可以指定一個從當前節點數目至可允許的失效節點數的函式:n: Int => f: Int where f < n. 

  擴充套件性反應了維持良好的吞量所需要的例項數,受到系統的度量結果的影響,特別是mailbox大小的歷史,CPU負載,以及GC百分比。它也可能會接受使用者輸入的對擴充套件性的暗示,這些暗示標識了期望負載。

  分割槽的平衡在第一版實現中可以用一個非常簡單的方式確定,在這種方式中分割槽的重疊被最小化。分割槽被以迴圈的方式分佈在叢集環中,每個例項節點在第一個可用的空間中。如果,一個叢集有10個節點和三個分割槽,A,B,C, 它們的N值分別是4, 3, 5;分割槽A的例項將會在節點1-4上;分割槽B的例項將會在節點5-7上;分割槽C的例項將會在節點8-10和1-2上。唯一的重疊在節點1和2.

  但是,分割槽的分佈沒有限制,不限於把例項分佈在在排序後的環(譯註:指cluster ring)的相鄰節點上。每個例項都可以被分配給任意節點,更加先進的負載平衡演算法可以利用這點。分割槽表包含從路徑來例項節點的對映。上面例子的分割槽將是:

A -> { 1, 2, 3, 4 }
B -> { 5, 6, 7 }
C -> { 8, 9, 10, 1, 2 }

 如果5個新節點加入叢集,並且按照排序後的順序,這些節點分別在當前節點2,4,5,7和8之後,那麼分割槽表將會被更新為如下,每個例項還在之前的物理節點上。

A -> { 1, 2, 4, 5 }
B -> { 7, 9, 10 }
C -> { 12, 14, 15, 1, 2 }

當需要再重平衡時,leader將安排切換,gossip待進行的改變,等到每個改變都完成,leader就會更新分割槽表。 

 

leader的額外職責

在把一個節點從joining狀態入成up狀態以後,leader可以分配分割槽[*]給新節點,當一個節點正在離開(is leaving),leader將會把分割槽在叢集中重新分配[*](也可能正在離開的節點自己是leader)。當所有分割槽切換[*]已經完成,節點會變為exiting狀態。

在收斂時,leader可以在叢集中安排重平衡(rebalancing),但是使用者也可以通過指定如何遷移(migration)來重新平衡叢集,或者根據成員節點的度量資料自動地重新平衡叢集。度量結果可以通過gossip協議傳播,也可以一個隨機弦方法(random chord method)更有效地傳播,即,leader聯絡叢集環(cluster ring)上的一些隨機的節點,每個節點收集它的直接相鄰節點的資料,這樣可以得到關於負載資訊的隨機取樣。

 

切換(Handoff)

  基於actor的系統進行切換,和基於資料的系統進行切換是不同的。最重要的一點是訊息順序(從一個特定節點到一個特定的actor例項)可能需要被保持。如果這個actor是一個單例actor(在整個叢集中只能有一個例項),那麼叢集需要確定在任何時間只有一個這樣的節點在活動。這些情況都可以通過在切換時轉發以及快取訊息來解決。

  給定一個前宿舍節點N1,一個新宿主節點N2,一個需要從N1遷移到N2的actor分割槽A,那麼一個優雅地切換有以下主要結構:

  1. leader設定一個把A從N1切換到N2上的待辦改變(pending change)
  2. N1注意到了這個待辦改變,向N2傳送一個初始化(initialization)訊息
  3. 作為迴應,N2建立A, 傳送回一個就緒(ready)訊息
  4. 在收到就緒訊息以後,N1把改變標記為完成,並且關閉A
  5. leader觀察到遷移已經完成,更新分割槽表
  6. 所有節點最終看到新的分割槽,並且使用N2

轉變(Transitions) 在切換過程中有數個轉變期,其中不同的步驟可以用來提供不同的保證。

遷移轉變(Migration Transition)  第一個轉變起始於N1初始化對A的轉移,結束於N1收到了就緒訊息,被稱為遷移轉變。

第一個問題是:在遷移轉變中,應該:

  • N1繼續處理A的訊息?
  • 或者,是否需要一旦遷移開始,就不在N1上處理A的訊息

  如果允許之前的宿主節點N1在遷移過程中處理訊息,那麼此時就沒什麼好做的。

  如果在遷移過程中訊息不在前宿主節點處理,那麼接下來有兩種可能性:訊息被轉發到新宿主並且緩衝,直到actor就緒;或者,簡單地關閉actor,使得訊息被丟充,並且允許使用正常的dead letter處理過程。

 更新轉變(Update Transition)  第二次轉變起始於遷移被標記為完成,結束於所有節點獲得到更新後的分割槽表(當所有節點都採用N2做為A的宿主,比如,收斂後),被稱為更新轉變。

  一旦更新轉變開始,N1可以把它收到的任何發給A的訊息轉發給新宿主N2.現在的問題是訊息的順序是否需要保留。如果傳送給先前宿主N1的訊息被轉發,那麼可能一個發給N1的訊息在一個直接發給新節點N2的訊息之後才被轉發,這樣就破壞了從客戶端到actorA的訊息順序。

  在這種情況下,N2可以儲存一個對應於每個傳送節點的緩衝區。當一個確認(ACK)訊息收到後,對應的快取區被flush以及移除。叢集中每個節點看到分割槽更新以後,首先傳送一個確認訊息給先前的宿主節點N1,然後再使用N2做為A的新宿主。任何從客戶節點直接發給N2的訊息會被放進緩衝區。N1可以遞減ACK的數量來確認不再需要轉發。來自於任何節點的ACK訊息總是在其它發給N1的訊息之後。當N1收到ACK訊息之後,它同樣把它轉發給N2, 同樣的,這個ACK訊息將來在任何已經替A轉發的其它訊息之後。當N2收到ACK訊息之後,用於傳送者節點的緩衝區就可以flush以及移除了。任何後續的從這個傳送節點傳送的訊息就可以正常的進行佇列。一旦所叢集中的所有節點都已經確認了分割槽的變化,並且N2已經清空了所有的緩衝區,那麼切換就完成了,並且訊息順序也得到了保留。在實際上,緩衝區應該保持較小,因為只有那些在ACK被轉發前直接傳送給N2的訊息才需要被緩衝。

優雅切換(Granceful Handoff)  一個更加完整的優雅切換的流程是:

  1. leader設定一個待辦的改變,要求N1把A切給至N2
  2. N1注意到了這個待辦的改變,傳送給N2一個初始化訊息。選擇有:

(a) 使A仍然在N1在保持活躍,繼續像平常一樣處理訊息

(b) N1把所有發給A的訊息轉發給N2

(c) N1丟棄所有發給A的訊息(關閉A,使發給A的訊息變為dead letter)

  3.  作為迴應,N2新建A,傳送回ready訊息。選擇有:

  (a) N2像平常一樣處理髮送給A的訊息

(b) N2為每個發訊息到A的節點建立一個緩衝區。當來自於某個傳送節點的確認訊息收到以後(通過N1),open(flush並移除)相應的緩衝區。

4.  在收到ready訊息後,N1把改變標記為已完成。選擇有:

(a) 在更新轉變(udpate transition)中,N1把所有發給A的訊息轉發給N2

(b) N1丟棄所有傳送給A的訊息(關閉A,所有發給A的訊息成為dead letter)

5.  leader發現遷移已經完成,更新分割槽表

6.  所有節點最終看到新的分割槽,並且使用N2

(a) 每個節點一個確認訊息給N1

(b) 當N1收到一個確認訊息,它就把待確認的節點數減1,當所有節點都確認,它就不再轉發訊息

(c) 當N2收到了確認訊息,它就可以open相應傳送者節點的緩衝區(如果使用了緩衝區的話)

 

預設的方法是採用2a, 3a和4a選項-允許位於N1上的A在遷移期間繼續處理訊息,然後在更新轉換過程中轉發所有訊息。這樣是基於對無狀態actor的假設,假設actor並不依賴於對於任何給定訊息源的訊息的順序。

  • 如果actor有持久化的狀態,那麼只要遷移actor就行了,別的都不需要做
  • 如果在更新轉換期間需要保持訊息的順序,那麼可以採用3b的選項,為每個傳送節點建立一個緩衝區
  • 如果actor對於訊息傳送失敗是魯棒的,那麼可以採用丟棄訊息的方案(不需要轉發,也不需要緩衝)
  • 如果actor是單例的(在整個叢集中只有一個),並且在遷移初始化時狀態被轉移了,那麼需要採用2b和3b選項。

有狀態actor的複製(Stateful Actor Replication)[*]


 注意:有狀態的actor的複製尚未實現。


[*] 尚未實現

  • Actor分割槽
  • Actor切換
  • Actor再平衡
  • 有狀態actor的複製 

 

相關文章