上一篇文章我們講了Akka Remote,理解了Akka中的遠程通信,其實Akka Cluster可以看成Akka Remote的擴展,由原來的兩點變成由多點組成的通信網絡,這種模式相信大家都很了解,就是集群,它的優勢主要有兩點:系統伸縮性高,容錯性更好。
集群概念
很多人很容易把分布式和集群的概念搞錯,包括我也是,我一開始也以為它們兩個是一樣的概念,只是叫法不同而已,但其實不然,雖然它們在實際場景中都是部署在不同的機器上,但它們所提供的功能并不是一樣的。舉個簡單的例子來看看它們之間的不同:
為了保持整個系列連續性,我又以抽獎為基礎舉一個例子:
假定我們現在抽獎流程包括,抽獎分配獎品和用戶根據鏈接領取指定獎品,用戶先抽獎然后獲取獎品鏈接,點擊鏈接填寫相應信息領取獎品。
1.分布式:
我們現在把抽獎分配獎品和用戶根據鏈接領取指定獎品分別部署在兩臺機器上,突然有一天很不幸,抽獎活動進行到一半,抽獎分配獎品那臺機子所在的區域停電了,很顯然,后續的用戶參與抽獎就不能進行了,因為我們只有一臺抽獎分配獎品的機子,但由于我們將領取獎品的業務部署在另一臺機器上,所以前面那些中獎的用戶還是可以正常的領取獎品。
2.集群:
現在我們還是有兩臺機器,但是我們在兩個機器上都部署了抽獎分配獎品和用戶根據鏈接領取指定獎品的業務邏輯,突然有一天,有一臺所在的區域停電了,但這時我們并擔心,因為另一臺服務器還是可以正常的運行處理用戶的所有請求。
它們的各自特點:
- 分布式:將一個業務分離成各個子業務模塊部署在不同服務器上,每個服務器完成的功能不一樣;
- 集群: 將同一個業務部署在不同的服務器上,每個服務器的功能相同;
總的來說: 分布式是以分離任務縮短時間來提高效率,而集群是在單位時間內處理更多的任務來提高效率。
Akka Cluster
在前面的文章Akka Actor的工作方式,我們可以將一個任務分解成一個個小任務,然后分配給它的子Actor執行,其實這就可以看成一個小的分布式系統,那么在Akka中,集群又是一種怎樣的概念呢?
其實往簡單里說,就是一些相同的ActorSystem的組合,它們具有著相同的功能,我們需要執行的任務可以隨機的分配到目前可用的ActorSystem上,這點跟Nginx的負載均衡很類似,根據算法和配置將請求轉發給運行正常的服務器去,Akka集群的表現形式也是這樣,當然它背后的理論基礎是基于gossip協議的,目前很多分布式的數據庫的數據同步都采用這個協議,有興趣的同學可以自己去研究研究,只是我也是一知半解,這里就不寫了,怕誤導了大家。
下面我來講講Akka Cluster中比較重要的幾個概念:
Seed Nodes
Seed Nodes可以看過是種子節點或者原始節點,它的一個主要作用用于可以自動接收新加入集群的節點的信息,并與之通信,使用方式可以用配置文件或者運行時指定,推薦使用配置文件方式,比如:
akka.cluster.seed-nodes = [
"akka.tcp://ClusterSystem@host1:2552",
"akka.tcp://ClusterSystem@host2:2552"]
seed-nodes列表中的第一個節點會集群啟動的時候初始化,而其他節點則是在有需要時再初始化。
當然你也可以不指定seed nodes,但你可以需要手動或者在程序中寫相關邏輯讓相應的節點加入集群,具體使用方式可參考官方文檔。
Cluster Events
Cluster Events字面意思是集群事件,那么這是什么意思呢?其實它代表著是一個節點的各種狀態和操作,舉個例子,假設你在打一局王者5v5的游戲,那么你可以把十個人看成一個集群,我們每個人都是一個節點,我們的任何操作和狀態都能被整個系統捕獲到,比如A殺了B、A超神了,A離開了游戲,A重新連接了游戲等等,這些狀態和操作在Cluster Events中就相當于節點之于集群,那么它具體是怎么使用的呢?
首先我們必須將節點注冊到集群中,或者說節點訂閱了某個集群,我們可以這么做:
cluster.subscribe(self, classOf[MemberEvent], classOf[UnreachableMember])
具體代碼相關的使用我會再下面寫一個demo例子,來說明是如何具體使用它們的。
從上面的代碼我們可以看到有一個MemberEvent的概念,這個其實就是每個成員所可能擁有的events,那么一個成員在它的生命周期中有以下的events
- ClusterEvent.MemberJoined - 新的節點加入集群,此時的狀態是Joining;
- ClusterEvent.MemberUp - 新的節點加入集群,此時的狀態是Up;
- ClusterEvent.MemberExited - 節點正在離開集群,此時的狀態是Exiting;
- ClusterEvent.MemberRemoved - 節點已經離開集群,此時的狀態是Removed;
- ClusterEvent.UnreachableMember - 節點被標記為不可觸達;
- ClusterEvent.ReachableMember - 節點被標記為可觸達;
狀態說明:
- Joining: 加入集群的瞬間狀態
- Up: 正常服務狀態
- Leaving / Exiting: 正常移出中狀態
- Down: 被標記為停機(不再是集群決策的一部分)
- Removed: 已從集群中移除
Roles
雖然上面說到集群中的各個節點的功能是一樣的,其實并不一定,比如我們將分布式和集群融合到一起,集群中的一部分節點負責接收請求,一部分用于計算,一部分用于數據存儲等等,所以Akka Cluster提供了一種Roles的概念,用來表示該節點的功能特性,我們可以在配置文件中指定,比如:
akka.cluster.roles = request
akka.cluster.roles = compute
akka.cluster.roles = store
ClusterClient
ClusterClient是一個集群客戶端,主要用于集群外部系統與集群通信,使用它非常方便,我們只需要將集群中的任意指定一個節點作為集群客戶端,然后將其注冊為一個該集群的接待員,最后我們就可以在外部系統直接與之通信了,使用ClusterClient需要做相應的配置:
akka.extensions = ["akka.cluster.client.ClusterClientReceptionist"]
假設我們現在我一個接待的Actor,叫做frontend,我們就可以這樣做:
val frontend = system.actorOf(Props[TransformationFrontend], name = "frontend")
ClusterClientReceptionist(system).registerService(frontend)
Akka Cluster例子
上面講了集群概念和Akka Cluster中相對重要的概念,下面我們就來寫一個Akka Cluster的demo,
demo需求:
線假設需要執行一些相同任務,頻率為2s一個,現在我們需要將這些任務分配給Akka集群中的不同節點去執行,這里使用ClusterClient作為集群與外部的通信接口。
首先我們先來定義一些命令:
package sample.cluster.transformation
final case class TransformationJob(text: String) // 任務內容
final case class TransformationResult(text: String) // 執行任務結果
final case class JobFailed(reason: String, job: TransformationJob) //任務失敗相應原因
case object BackendRegistration // 后臺具體執行任務節點注冊事件
然后我們實現具體執行任務邏輯的后臺節點:
class TransformationBackend extends Actor {
val cluster = Cluster(context.system)
override def preStart(): Unit = cluster.subscribe(self, classOf[MemberEvent]) //在啟動Actor時將該節點訂閱到集群中
override def postStop(): Unit = cluster.unsubscribe(self)
def receive = {
case TransformationJob(text) => { // 接收任務請求
val result = text.toUpperCase // 任務執行得到結果(將字符串轉換為大寫)
sender() ! TransformationResult(text.toUpperCase) // 向發送者返回結果
}
case state: CurrentClusterState =>
state.members.filter(_.status == MemberStatus.Up) foreach register // 根據節點狀態向集群客戶端注冊
case MemberUp(m) => register(m) // 將剛處于Up狀態的節點向集群客戶端注冊
}
def register(member: Member): Unit = { //將節點注冊到集群客戶端
context.actorSelection(RootActorPath(member.address) / "user" / "frontend") !
BackendRegistration
}
}
相應節點的配置文件信息,我這里就不貼了,請從相應的源碼demo里獲取。源碼鏈接
接著我們來實現集群客戶端:
class TransformationFrontend extends Actor {
var backends = IndexedSeq.empty[ActorRef] //任務后臺節點列表
var jobCounter = 0
def receive = {
case job: TransformationJob if backends.isEmpty => //目前暫無執行任務節點可用
sender() ! JobFailed("Service unavailable, try again later", job)
case job: TransformationJob => //執行相應任務
jobCounter += 1
implicit val timeout = Timeout(5 seconds)
val backend = backends(jobCounter % backends.size) //根據相應算法選擇執行任務的節點
println(s"the backend is ${backend} and the job is ${job}")
val result = (backend ? job)
.map(x => x.asInstanceOf[TransformationResult]) // 后臺節點處理得到結果
result pipeTo sender //向外部系統發送執行結果
case BackendRegistration if !backends.contains(sender()) => // 添加新的后臺任務節點
context watch sender() //監控相應的任務節點
backends = backends :+ sender()
case Terminated(a) =>
backends = backends.filterNot(_ == a) // 移除已經終止運行的節點
}
}
最后我們實現與集群客戶端交互的邏輯:
class ClientJobTransformationSendingActor extends Actor {
val initialContacts = Set(
ActorPath.fromString("akka.tcp://ClusterSystem@127.0.0.1:2551/system/receptionist"))
val settings = ClusterClientSettings(context.system)
.withInitialContacts(initialContacts)
val c = context.system.actorOf(ClusterClient.props(settings), "demo-client")
def receive = {
case TransformationResult(result) => {
println(s"Client response and the result is ${result}")
}
case Send(counter) => {
val job = TransformationJob("hello-" + counter)
implicit val timeout = Timeout(5 seconds)
val result = Patterns.ask(c,ClusterClient.Send("/user/frontend", job, localAffinity = true), timeout)
result.onComplete {
case Success(transformationResult) => {
self ! transformationResult
}
case Failure(t) => println("An error has occured: " + t.getMessage)
}
}
}
}
下面我們開始運行這個domo:
object DemoClient {
def main(args : Array[String]) {
TransformationFrontendApp.main(Seq("2551").toArray) //啟動集群客戶端
TransformationBackendApp.main(Seq("8001").toArray) //啟動三個后臺節點
TransformationBackendApp.main(Seq("8002").toArray)
TransformationBackendApp.main(Seq("8003").toArray)
val system = ActorSystem("OTHERSYSTEM")
val clientJobTransformationSendingActor =
system.actorOf(Props[ClientJobTransformationSendingActor],
name = "clientJobTransformationSendingActor")
val counter = new AtomicInteger
import system.dispatcher
system.scheduler.schedule(2.seconds, 2.seconds) { //定時發送任務
clientJobTransformationSendingActor ! Send(counter.incrementAndGet())
}
StdIn.readLine()
system.terminate()
}
}
運行結果:
從結果可以看到,我們將任務根據算法分配給不同的后臺節點進行執行,最終返回結果。
本文目的
- 掌握集群基本概念
- 了解學習Akka cluster的工作方式和主要角色
- 嘗試自己寫一個Akka cluster的相關例子
- 下一步進階了解Akka cluster的背后原理
本文的demo例子已上傳github:源碼鏈接