參加了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,那么一個優雅地切換有以下主要結構:
- leader設置一個把A從N1切換到N2上的待辦改變(pending change)
- N1注意到了這個待辦改變,向N2發送一個初始化(initialization)消息
- 作為回應,N2創建A, 發送回一個就緒(ready)消息
- 在收到就緒消息以后,N1把改變標記為完成,並且關閉A
- leader觀察到遷移已經完成,更新分區表
- 所有節點最終看到新的分區,並且使用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) 一個更加完整的優雅切換的流程是:
- leader設置一個待辦的改變,要求N1把A切給至N2
- 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的復制