Akka 學習筆記

筆尖的痕發表於2016-02-29

容錯

容錯:不是抓住系統所有的錯誤並恢復,而是將錯誤(崩潰)孤立出來,不會導致整個系統崩潰(隔離故障元件),備份元件可以替換崩潰元件(冗餘)(可恢復性) 
容錯方式:Restart, Resume, Stop, Escalate 
let it crash原則 
崩潰原因:網路,第三方服務,硬體故障 
Akka容錯:分離業務邏輯(receive)和容錯邏輯(supervisorStrategy) 
父actor自動成為子actor的supervisor 
supervisor不fix子actor,而是簡單的呈現如何恢復的一個判斷==> 
List(Restart, //重啟並替換原actor,mailbox訊息可繼續傳送, 
//但是接收會暫停至替換完成,重啟預設重啟所有子actor 
Resume, //同一個actor不重啟,忽略崩潰,繼續處理下一個訊息 
Stop, //terminated 不再處理任何訊息,剩餘訊息會進入死信信箱 
Escalate//交給上層處理 

Akka提供兩種恢復策略: 
OneForOneStrategy: 只針對單獨actor

  1. override def supervisorStrategy = OneForOneStrategy() {
  2. case _:XXXException => Restart
  3. }

AllForOneStrategy: 對所有節點,一個節點重啟,其它節點全部重啟 
不處理TheadDeath OOM等JVM Error,一直往上拋直到actorSystem頂層actor user guardian 會shutdown ,可通過配置使其優雅shutdown


擴充套件

scaling up 垂直擴充套件:某節點可以在單機(單JVM)上執行更多的數量 
scaling out 水平擴充套件:某節點可以在多臺機器上執行 
Akka 按地址(local, remote)投遞message, 可以方便實現scaling out 
actor不知道是否是和remote actor通訊,local, remote區別在於配置,所以Akka可以透明的從scaling up轉向scaling out(僅通過修改幾行程式碼) 所以可以像積木一樣隨意組裝 
/-- 
在單JVM中上鎖訪問的可變狀態(hhtp-session) 在scaling out時,直接寫入一個資料庫是最簡單的方式(同庫整合) 
但是這意味著修改大量程式碼; 
Akka用不可變訊息解決了以上問題 
Akka actor 在dispatcher(底層排程執行pool)之上執行,dispatcher和配置直接對映(不同dispatcher型別可被選擇),所以在scaling up時,只修改配置,不需要動程式碼,其次actor和執行緒不是一一對映導致其非常輕量(比執行緒佔用空間更少) 
/--- 
RPC 缺點 點對點通訊不適合大規模叢集(網路拓撲結構會很複雜,並伴隨負載) 
面向訊息的中介軟體可以解決這個問題,but代價是應用層混入訊息系統(訊息中介軟體必須跟著應用演變) 
Akka 使用分散式程式設計模型(Actor(抽象了本地和分散式環境))在scaling out時,頂層看起來是一樣的(透明) 
分散式術語: 
節點:通過網路通訊的應用 
節點角色:不同的節點執行不同的任務 
通訊協議:訊息被編碼和解碼為特定通訊協議的報文用於節點的通訊 
序列化和反序列化:訊息的編碼和解碼 
membership:同一個分散式系統的節點(可動態可靜態) 
dynamic membership:節點數動態變化 
Q:分散式環境難在哪裡? 
A:時延,記憶體訪問,區域性失敗和併發性

  1. //Akka remote example
  2. object TestRemote extends App {
  3. //////////////backend
  4. val confBackend =
  5. """
  6. akka {
  7. actor {
  8. provider = "akka.remote.RemoteActorRefProvider"
  9. }
  10. remote {
  11. enabled-transports = ["akka.remote.netty.tcp"]
  12. netty.tcp {
  13. hostname = "127.0.0.1"
  14. port = 2551
  15. }
  16. }
  17. }
  18. """
  19. val configBackend = ConfigFactory parseString confBackend
  20. val backend = ActorSystem("backend", configBackend)//backend listens on address akka.tcp://backend@127.0.0.1:2551
  21. backend.actorOf(Props[ConsoleActor], "console")
  22. //////////////frontend
  23. val confFrontend =
  24. """
  25. akka {
  26. actor {
  27. provider = "akka.remote.RemoteActorRefProvider"
  28. }
  29. remote {
  30. enabled-transports = ["akka.remote.netty.tcp"]
  31. netty.tcp {
  32. hostname = "127.0.0.1"
  33. port = 2552
  34. }
  35. }
  36. }
  37. """
  38. val configFrontend = ConfigFactory parseString confFrontend
  39. val frontend = ActorSystem("frontend", configFrontend)//frontend listens on address akka.tcp://backend@127.0.0.1:2552
  40. val path = "akka.tcp://backend@127.0.0.1:2551/user/console"
  41. val console = frontend.actorSelection(path)
  42. console ! "Hello World"
  43. }
  44. //
  45. class ConsoleActor extends Actor {
  46. def receive = {
  47. case m => println(s"received $m")
  48. }
  49. }

actorSelection 
actorSelection方法會在本地建立一個遠端actor的代理,代理來處理和監聽遠端actor的重啟訊息接收等,可以有容錯作用,通過actorSelection直接獲取actorRef 也可直接通訊,但是遠端actor崩潰重啟,actorRef不會自動定位(容錯不好,效能不錯) 
Akka 還可以遠端部署actor 
可通過配置和程式碼兩種方式,遠端actor崩潰重啟,actorRef不會自動定位 
遠端部署actor容錯:需要自己watch actor, 並且在遠端actor重啟時,重新deploy and watch 
採用非遠端部署通過actorSelection獲取遠端actorRef無法實現watch 
通過actorSelection獲得遠端actorRef方法:

val console = context.system.actorSelection(path) 
val actorRef = Await.result(console.resolveOne(), 5.seconds)


Futures

函式式併發 
Actor基於訊息,他們是長久存在的物件,當某事(訊息)發生時作出行為 
Future用函式替代物件,是某函式非同步執行的結果的佔位符

  1. import scala.concurrent._
  2. import ExecutionContext.Implicits.global //提供執行執行緒池 隱轉
  3. val res = future {
  4. 1
  5. } map { r =>
  6. "res=" + r
  7. } foreach { r =>
  8. println(r)
  9. }
  10. //Future[Int] ==> Future[String]
  11. //Future[T]和Option[T]類似

Future 錯誤處理 
future {...}程式碼塊內報異常 後續操作將直接不進行 
要想獲取錯誤值用onComplete方法

  1. val fres = future {
  2. throw new Exception("error")
  3. 1
  4. } map { r =>
  5. "res=" + r
  6. } onComplete {
  7. case Success(r) => println(r)
  8. case Failure(NonFatal(e)) => println(e)
  9. }//和ajax類似
  10. while(true){} //需要堵塞

Future 恢復(容錯) 
通過recover方法 可以定義在發生特定錯誤時的處理邏輯

  1. val f = future {
  2. throw new IllegalArgumentException("error")
  3. 1
  4. } map { r =>
  5. "res=" + r
  6. } recover {
  7. case e:IllegalArgumentException => 2 //返回某預設值
  8. } onComplete {
  9. case Success(r) => println(r)
  10. case Failure(NonFatal(e)) => e.printStackTrace()
  11. }
  12. while(true){}
  13. ////////
  14. val f1 = doAsyc1()
  15. val f2 = f1.flatMap { r =>
  16. doAsyc2(r).recover {
  17. case e:XXXException => r
  18. }
  19. }
  20. //onSuccess & onFailure
  21. val f: Future[Int] = future {
  22. val source = scala.io.Source.fromFile("myText.txt")
  23. source.toSeq.indexOfSlice("myKeyword")
  24. }
  25. f onSuccess {
  26. case idx => println("The keyword first appears at position: " + idx)
  27. }
  28. f onFailure {
  29. case t => println("Could not process file: " + t.getMessage)
  30. }

Future 組合

  1. //1. firstCompletedOf
  2. val f1 = future{1}
  3. val f2 = future{"res="}
  4. val f3 = Seq(f1, f2)
  5. val f4 = Future.firstCompletedOf(f3) //取先執行完的結果
  6. f4.onComplete {
  7. case Success(r) => println(r)
  8. case Failure(NonFatal(e)) => e.printStackTrace()
  9. }
  10. //2. zip
  11. //Future[Int] zip Future[String] ==> Future[(Int, String)]
  12. val f1 = future{1}
  13. val f2 = future{"res="}
  14. f1 zip f2 foreach(println) //等待一起執行完 並組合結果
  15. ===> (1,res=)
  16. val f3 = f1 zip f2 map { case(i,s) =>
  17. s+i
  18. }
  19. //3. sequence traverse
  20. //Seq[Future[Int]] ==> Future[Seq[Int]] 多值對映為單值
  21. val f = Seq(1,2,3) map { i =>
  22. future{i}
  23. }
  24. Future.sequence(f) foreach println
  25. /////////
  26. val f = Future.traverse(Seq(1,2,3)) { i =>
  27. future{i}
  28. }
  29. f foreach println
  30. //4. fold
  31. val f = Seq(f1, f2)
  32. Future.fold(f)(0) { (sum:Int, i:Int) =>
  33. ...
  34. }
  35. //5.andThen
  36. val allposts = mutable.Set[String]()
  37. future {
  38. session.getRecentPosts
  39. } andThen {
  40. posts => allposts ++= posts
  41. } andThen {
  42. posts =>
  43. clearAll()
  44. for (post <- allposts) render(post)
  45. }
  46. /**
  47. * 組合器andThen的用法是出於純粹的side-effecting目的。
  48. * 經andThen返回的新Future無論原Future成功或失敗都會返回與原Future一模一樣
  49. * 的結果。一旦原Future完成並返回結果,andThen後跟的程式碼塊就會被呼叫,
  50. * 且新Future將返回與原Future一樣的結果,這確保了多個andThen呼叫的順序執行
  51. */
  52. //6. for
  53. val f1 = future { connection.getCurrentValue(USD) }
  54. val f2 = future { connection.getCurrentValue(CHF) }
  55. val f3 = for {
  56. usd <- f1
  57. chf <- f2
  58. if isProfitable(usd, chf)
  59. } yield connection.buy(amount, chf)
  60. f3 onSuccess {
  61. case _ => println("Purchased " + amount + " CHF")
  62. }
  63. //f3只有當f1和f2都完成計算以後才能完成--
  64. //它以其他兩個Future的計算值為前提所以它自己的計算不能更早的開始。

與Akka的結合 主要通過 akka.pattern包來實現 
1.ask 
actor.ask / ?

  1. import akka.pattern.ask
  2. implicit val timeout = Timeout(5 seconds)
  3. val f = (actor ? Message).mapTo[Int]
  4. f foreach println

2.pipe 
用於actor 處理Future的結果

  1. val f = (actor1 ? Message).pipeTo(actor2)
  2. //把上一個結果傳遞給下一個actor; actor2 接收的訊息是值 不是Future引用

Await, Promise with Future

  1. val f = future {
  2. 1
  3. } map { r =>
  4. "res=" + r
  5. } recover {
  6. case e:Exception => 2
  7. }
  8. val i = Await.result(f, 5.seconds) //同步等待 直到futrue執行結束 返回值
  9. //promise是一個可寫的,可以實現一個future的單一賦值容器
  10. val p = promise[T]
  11. val f = p.future
  12. val prod = future { //生產者
  13. val r = calcResult()
  14. p success r //此時f被寫入值
  15. //do other things 同時 下面第19行程式碼的回撥可以執行了
  16. }
  17. val cons = future {
  18. //do something...
  19. f onSuccess { //獲取寫入值
  20. case r => doSomethingWithRes(r)
  21. }
  22. }

future help doc url: http://docs.scala-lang.org/overviews/core/futures.html 
or zh: https://code.csdn.net/DOC_Scala/chinese_scala_offical_document/file/Futures-and-Promises-cn.md


Akka配置

--載入 
----預設載入applaction.conf, 程式碼不寫則載入akka-actor包下的reference.conf 
----載入配置:val sys = ActorSystem("sys",ConfigFactory.load("sys")) 
----獲取配置:sys.setting.config.getString("myApp.name")

  1. """sys.conf"""
  2. myApp {
  3. name = "mySystem"
  4. }

--為不同子系統定義配置 
----1.提取共享配置:baseConfig.conf 
----2.子系統include "baseConfig" 並定義自己的部分 或覆蓋公共部分

--akka應用日誌 
----1.在Actor中用 val log = Logging(context.system, this) 
----或者:with ActorLogging 
----2.配置

  1. akka {
  2. event-handlers = ["akka.event.Logging$DefaultLogger"] #僅僅輸出到STDOUT
  3. loglevel = "DEBUG"
  4. }

----自定義日誌handler

  1. class MyEventListener extends Actor {
  2. def receive = {
  3. case InistializeLogger(_) => //系統訊息
  4. sender ! LoggerInitialized
  5. case Error(cause, logSource, logClass, message) =>
  6. println("ERROR " + message)
  7. case Warning(logSource, logClass, message) =>
  8. println("WARN " + message)
  9. case Info(logSource, logClass, message) =>
  10. println("INFO " + message)
  11. case Debug(logSource, logClass, message) =>
  12. println("DEBUG " + message)
  13. }
  14. }

使用slf4j的eventHandler

  1. akka {
  2. event-handlers = ["akka.event.slf4j.Slf4jEventHandler"]
  3. loglevel = "DEBUG"
  4. }

--配置akka日誌

  1. akka {
  2. loglevel = DEBUG #*必須設定成DEBUG 下面的debug才可以用
  3. log-config-on-start = on #啟動時顯示用了哪個配置檔案
  4. debug {
  5. receive = on #記錄actor 接收的訊息(user-level級)由akka.event.LoggingReceive處理
  6. autoreceive = on #記錄所有自動接收的訊息(Kill, PoisonPill
  7. lifecycle = on #記錄actor lifecycle changes
  8. fsm = on #狀態機相關
  9. event-stream = on #記錄eventSteam (subscribe/unsubscribe)
  10. }
  11. remote {
  12. log-sent-messages = on #記錄出站的訊息
  13. log-received-messages = on #記錄進站的訊息
  14. }
  15. }
  1. class MyActor extends Actor with ActorLogging {
  2. def receive = LoggingReceive { //記錄接收訊息
  3. cae ... => ...
  4. }
  5. }

系統架構 與 Akka

1.管道和過濾器 
--管道Pips:一個程式或執行緒將處理結果傳給下一個程式做額外處理的能力 
(Unix |符) 
--Pipeline:多個管道組合,大都為序列,akka提供並行 
--過濾器:傳遞給下一個proccess之前的驗證邏輯 
--Pip&Filter模式:input -pip-> check -pip-> check -> output 
每個Filter包含3部分:進站pipe,處理器,出站pipe 
進站訊息和出站訊息必須是一樣的(交給下一個處理),所以過濾器的順序是可以對調的, 
--Akka的實現

  1. class Filter1(pipe: ActorRef) extend Actor {
  2. def receive = {
  3. case msg:Message =>
  4. if...cond1
  5. pipe ! msg
  6. }
  7. }
  8. class Filter2(pipe: ActorRef) extend Actor {
  9. def receive = {
  10. case msg:Message =>
  11. if...cond2
  12. pipe ! msg
  13. }
  14. }

2.發散聚合模式 
並行的處理問題分 分發器和聚合器兩部分 map reduce 
Akka 實現 Scatter & Gather 模式

  1. class Filter1(pipe: ActorRef) extend Actor {
  2. def receive = {
  3. case msg:Message =>
  4. val res1 = proccess...
  5. pipe ! res1
  6. }
  7. }
  8. class Filter2(pipe: ActorRef) extend Actor {
  9. def receive = {
  10. case msg:Message =>
  11. val res2 = proccess...
  12. pipe ! res2
  13. }
  14. }
  15. //Scatter
  16. class RecipientList(recipientList: Seq[ActorRef]) extend Actor {
  17. def receive = { //分發給不同的收件人(pipe)
  18. case msg:AnyRef => recipientList.foreach(_ ! msg)
  19. }
  20. }
  21. //Gather 會快取訊息,當訊息都接收完了之後再處理,併發給下一個process
  22. class Aggregator(timeout:Duration, pipe:ActorRef) extends Actor {
  23. val msgs = new ListBuffer[Mssage] //快取訊息
  24. def receive = {
  25. case msg:Message => {
  26. msgs.find(_.id == msg.id) match { //當有兩條一樣時傳送
  27. case Some(existMsg) => {
  28. pipe ! existMsg
  29. msgs -= existMsg
  30. }
  31. case None => msgs += msg
  32. }
  33. }
  34. }
  35. }

Message Channels

1.點對點 Actor預設投遞方式 
單鏈式:sender -3-2-1-> p2pchannel -3-2-1-> receiver保證訊息順序不會亂 
多接收者:sender -3-2-1-> p2pchannel 1->receiver1 | 2->receiver2 | 3->receiver3 每個接收者只處理一種訊息 
2.釋出訂閱 
概述:發給多個接收者,並且不知道接收者是誰,channel負責跟蹤接收者,多個接收者處理同個訊息,接收者的數量可動態變動(unSubscribe/Subscribe) 訊息有序 
場景:發貨和庫存更新 需要同一份訂單訊息,並且可並行執行 
EventStream: 支援釋出訂閱channel, 支援接收者動態變動, 可看成多個Publish-Subscribe channels的管理者 
Actor接收任何來自EventStream的訊息,不需要額外編碼,只用receive方法就行 
註冊和登出自己:

  1. system.eventStream.subscribe(slef, classOf[Message])
  2. system.eventStream.unsubscribe(slef, classOf[Message])

釋出訊息

  1. system.eventStream.publish(msg)

可訂閱多種訊息,登出時需要一一登出 
2.5. EventBus 一個概念而已 
3.自定義EventBus 
需要實現三種實體: 
Event 訊息 
Subscriber (提供被註冊的能力,EventStream裡面就是ActorRef) 
Classifier(選擇訂閱者分發事件,EventStream裡面Classifier 就是訊息的型別) 
Akka有三種可組合的traits 可用來追蹤訂閱者 
LookupClassification:基本分類器,維護了訂閱者集合,實現classify方法來從事件中提取classifier 
SubchannelClassification: 它希望監聽者不只是葉子節點,實現有層級的結構 
ScanningClassification:最複雜的一個 
他們實現了unSubscribe/Subscribe方法,但是有別的方法需要實現 
classify(event:Event):Classifier //從事件中提取分類器 
compareSubscribers(a:Subscriber, b:Subscriber):Int //訂閱者的比較器 
publish(event:Event, subscriber:Subscriber) // 
mapSize:Int //返回不同classifier的數目

  1. class MessageBus extends EventBus with LookupClassification {
  2. type Event = Message
  3. type Classifier = Boolean //自定義 這裡以訊息的number欄位是不是>1
  4. def mapSize = 2 //true,false 兩種
  5. //按型別來,就不需要自定義了
  6. protected def classify(event:MessageBus#Event) = {
  7. event.number > 1
  8. }
  9. }
  10. //test
  11. val bus = new MessageBus
  12. bus.subscribe(actorRef1, false) //監聽 < =1
  13. bus.subscribe(actorRef2, true) //監聽 > 1
  14. bus.publish(Message(1))
  15. bus.publish(Message(2))
  1. DeadLetter(死信) channel 
    概念:只有失敗的訊息才會進入,監聽這裡可以幫助發現系統的問題,不可以通過它傳送訊息 
    死信:所有不能被處理或者投遞(達到)的訊息 
    Akka用EventStream去實現死信佇列
  1. system.eventStream.subscribe(monitorRef, classOf[DeadKetter])
  2. actor1 ! PoisonPill //自殺
  3. actor1 ! Message //接收不到
  4. val dead = monitorRef.expectMsgType[DeadLetter]
  5. dead.message
  6. dead.sender //傳送人
  7. dead.recipient //收信人
  8. //當Actor不知道怎麼處理訊息時可以顯示傳送給deadLetter
  9. system.deadLetters ! msg

5.Cuaranteed deliver channel (屬於點對點channel) 
概念:保證所有訊息傳送和投遞 分幾個等級,但是akka不保證訊息100%投遞成功 
構建系統時,我們需要考慮怎樣的程度的擔保是足夠的 
一般規則:訊息至多投遞一次 
Akka保證訊息投遞一次或者投遞失敗(不是很好) 但是有兩種解決方式: 
a. 傳送本地訊息不太會失敗(單JVM傳送訊息只是方法呼叫,失敗就是報異常了,此時會有別的策略去處理(容錯),訊息確實不需要到達了),所以單JVM訊息投遞是可靠的 
b. remote actors, 非常可能丟失訊息,尤其是在不可靠網路之上,ReliableProxy被用來解決這個問題,它使得傳送訊息如傳送local message一樣可靠。 
唯一的顧慮是傳送和接收者所在JVM 
Q: How does the ReliableProxy work? 
A: 當開啟ReliableProxy(Actor) 在不同的節點間建立了一個隧道 
{client-node: sender -> ReliableProxy} ---> {server-node: egress -> service } 
Egress是一個被ReliableProxy啟動的Actor 兩個Actor都實現了check resend 功能去跟蹤哪個訊息被遠端接收者接收; 
當訊息投遞失敗時,ReliableProxy會重試,直到Egress接收到,Egress轉發給真正的接收者; 
當目標actor終止時,本地ReliableProxy將會終止 
ReliableProxy限制:只能正對一個接收者,並且單向(若要回發,會再啟兩個代理)

  1. val remoteActorRef = system.actorFor(path)
  2. val proxy = system.actorOf(Props(new ReliableProxy(remoteActorRef,500.millis)), "proxy")
  3. proxy ! Message
  4. //akka 2.3.8之後不可用system.actorFor方法,只能用actorSelection方法,而Selection底層就是用的Proxy

STM (借鑑Clojure的東西)

  1. val s = Ref(Set(1,2,3))
  2. val reservedS = atomic { implicit tx = {
  3. val head = s().head
  4. s() = s().tail
  5. head
  6. }}

原始碼分析

actors 
address --> ActorRef 替換直接引用(記憶體引用) 
mailboxes

ActorCell.newActor 會委託props.newActor建立,並將behaviorStack(為毛是棧?為了實現become和unbecome)head 設定為instance.receive。 
actorRef ! Message ; 呼叫ActorCell.tell --> dispatcher.dispath(分發)-->將訊息放入ActorCell的佇列mbox.enqueue裡,並executor.execute(mbox) -->執行mbox的run方法(mailbox被分發到某執行緒上)-->先處理系統訊息(mailBox.processAllSystemMessages),再處理使用者訊息(mailBox.processMailbox) --> dequeue佇列獲得訊息--> actor invoke next --> ActorCell.recevieMessage(msg) --> Actor.aroundReceive(behaviorStack.head, msg)執行receive方法 
...

叢集

目標:全自動的管理actor叢集, 負載均衡,故障處理 
目前的features: 
--membership:容錯的membership 
--負載均衡:根據路由演算法 路由actors的message 
--節點分割槽:節點可以有自己的角色,使路由器可以配置為只給某角色的節點發訊息 
--Partition points(分割槽點):一個ActorSystem 可以有一部分子樹在別的節點上(目前只有top level actors可以這樣) 
不提供: 狀態冗餘,重分割槽,重負載 
最適用:單一目的的資料處理應用(影象處理,實時資料分析)

種子節點(是一種節點角色): 
a.最基本的用於啟動叢集的節點(其實是冗餘的控制節點); b.沒有任何的actors,是個純的節點; 
c.是其它節點的中間聯絡人 
d.要啟動akka cluster 必須配置一系列的種子節點,並且第一個種子節點有特殊的角色,First seed Node必須最先啟動; 
e.其它種子節點可以和第一種子節點一起啟動,它們會等待第一種子節點啟動好 
f.其它成功join啟動叢集后,第一種子節點可以安全的離開叢集 
注:種子節點不是必須的,你可以手動啟動一個節點,讓它自己加入自己程式設計叢集,這樣就是手動需要了解地址等

配置:

  1. akka {
  2. loglevel = INFO
  3. stdout-loglevel = INFO
  4. event-handlers = ["akka.event.Logging$DefaultLogger"]
  5. actor { #修改provider
  6. provider = "akka.cluster.ClusterActorRefProvider"
  7. }
  8. remote {
  9. enabled-transports = ["akka.remote.netty.tcp"]
  10. log-remote-lifecycle-events = off
  11. netty.tcp {
  12. hostname = ""
  13. host = ${HOST} #每個節點的主機
  14. port = ${PORT} #每個節點的監聽埠
  15. }
  16. }
  17. cluster {
  18. seed-nodes = ["akka.tcp://words@127.0.0.1:2551",#First seed Node
  19. "akka.tcp://words@127.0.0.1:2552", #actorsystem名稱必須相同
  20. "akka.tcp://words@127.0.0.1:2553"] #種子節點列表
  21. roles = ["seed"]
  22. }
  23. }

此時啟動三個JVM配置不同的port,就建立三個seed node了,並且自動組成cluster

  1. //first seed node
  2. val seedConfig = ConfigFactory.load("seed")
  3. val seedSystem = ActorSystem("words", seedConfig)
  4. //啟動後自己join自己
  5. //狀態變化:Join -> Up
  6. //同樣的程式碼啟動其它兩個種子節點
  7. //first seed node 離開叢集
  8. val address = Cluster(seedSystem).selfAddress
  9. Cluster(seedSystem).leave(address)
  10. //狀態變為Leaving -> Exiting -> Unreachable

在first seed node Exiting狀態時,seed node2自動變成leader(處理Join請求) 
節點的狀態(Joining,Up...)變化通知,會在所有節點間傳遞

Gossip(閒聊) 協議: 
在節點之間傳遞節點狀態的協議,每個節點描述自己的狀態和它看到的別的節點的狀態,最後節點的狀態會收斂成一致狀態,每次收斂之後都可以確定Leader

first seed node退出後,不能Cluster(seedSystem).join(selfAddress),而是應該重啟

  1. seedSystem.shutdown
  2. val seedSystem = ActorSystem("words", seedConfig)
  3. //一個actorsystem只能加入cluster一次,但是可以以相同的配置啟動一個新actorsystem

Akka Cluster會探測Unreachable的節點,並解除安裝(down),leader節點在Unreachable無法正常工作,可以用down方法,解除安裝任意一個節點

  1. val address = Address("akka.tcp", "words", "127.0.0.1", 2551)
  2. Cluster(seedSystem).down(address)

節點狀態轉換: 
-join-> [Joining] -leader action-> [Up] -leave-> [Leaving] -leader action-> 
[Exiting] -leader action-> [Unreachable] -down-> [Down] --> Removed

訂閱Cluster Domain Events

  1. class ClusterDomainEventListener extends Actor with ActorLogging {
  2. Cluster(context.system).subscribe(self, classOf[ClusterDomainEvent])
  3. def receive ={
  4. case MemberUp(member) => log.info(s"$member UP.")
  5. case MemberExited(member) => log.info(s"$member EXITED.")
  6. case MemberRemoved(m, previousState) =>
  7. if(previousState == MemberStatus.Exiting) {
  8. log.info(s"Member $m gracefully exited, REMOVED.")
  9. } else {
  10. log.info(s"$m downed after unreachable, REMOVED.")
  11. }
  12. case UnreachableMember(m) => log.info(s"$m UNREACHABLE")
  13. case ReachableMember(m) => log.info(s"$m REACHABLE")
  14. case s: CurrentClusterState => log.info(s"cluster state: $s")
  15. }
  16. override def postStop(): Unit = {
  17. Cluster(context.system).unsubscribe(self)
  18. super.postStop()
  19. }
  20. }

為MemberUp事件配置最小數量節點

  1. #在master上配置
  2. role {
  3. worker.min-nr-of-members = 2 #當worker節點數超過兩個時 處罰 memberup事件
  4. }
  1. object Main extends App {
  2. val config = ConfigFactory.load()
  3. val system = ActorSystem("words", config)
  4. println(s"Starting node with roles: ${Cluster(system).selfRoles}")
  5. val roles = system.settings.config
  6. .getStringList("akka.cluster.roles")
  7. if(roles.contains("master")) { //當是master節點時 註冊事件
  8. Cluster(system).registerOnMemberUp { //worker節點2個時觸發
  9. val receptionist = system.actorOf(Props[JobReceptionist],
  10. "receptionist")
  11. println("Master node is ready.")
  12. }
  13. }
  14. }

Routers 叢集的路由 
和用本地的Router是一樣的,用路由去worker node上建立worker actor

  1. trait CreateWorkerRouter { this: Actor =>
  2. def createWorkerRouter: ActorRef = {
  3. context.actorOf(
  4. ClusterRouterPool(BroadcastPool(10), //用Pool來建立 廣播路由
  5. ClusterRouterPoolSettings(
  6. totalInstances = 1000, //叢集中最多多少個
  7. maxInstancesPerNode = 20, //每個節點最多多少個
  8. allowLocalRoutees = false, //不在本地節點建立, 只在worker節點上建立
  9. useRole = None
  10. )
  11. ).props(Props[JobWorker]),
  12. name = "worker-router")
  13. }
  14. }

編寫master

  1. class JobMaster extends Actor
  2. with ActorLogging
  3. with CreateWorkerRouter {
  4. // inside the body of the JobMaster actor..
  5. val router = createWorkerRouter
  6. def receive = idle
  7. def idle: Receive = {
  8. case StartJob(jobName, text) =>
  9. textParts = text.grouped(10).toVector //分割文字
  10. val cancel = system.scheduler.schedule( 0 millis, #定期給路由發
  11. 1000 millis,
  12. router,
  13. Work(jobName, self))
  14. //發Work訊息建立worker
  15. become(working(jobName, sender, cancel))
  16. }
  17. // more code

//TODO

Persistence

當有狀態的actors崩潰時,做資料恢復 
點對點訊息保證一次投遞

Scheduler

JDK ScheduledThreadPoolExecutor 通過DelayedWorkQueue 是一個PriorityQueue 
(take 是會堵塞的)優先順序為Delay時間, 每次新增task都會重排序,時間短排前面,排程時根據Delay時間判斷是否執行 
Akka Scheduler 則通過Netty HashedWheelTimer ,通過設定Tick Duration,掃一次Task佇列,(會Thread.sleep(sleepMs)) 

哪個效能更好??? 不知道


https://www.zybuluo.com/MiloXia/note/60638