Akka系列(十):Akka集群之Akka Cluster

上一篇文章我們講了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.png

從結果可以看到,我們將任務根據算法分配給不同的后臺節點進行執行,最終返回結果。

本文目的

  • 掌握集群基本概念
  • 了解學習Akka cluster的工作方式和主要角色
  • 嘗試自己寫一個Akka cluster的相關例子
  • 下一步進階了解Akka cluster的背后原理

本文的demo例子已上傳github:源碼鏈接

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,327評論 6 537
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,996評論 3 423
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 177,316評論 0 382
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,406評論 1 316
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,128評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,524評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,576評論 3 444
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,759評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,310評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,065評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,249評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,821評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,479評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,909評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,140評論 1 290
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,984評論 3 395
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,228評論 2 375

推薦閱讀更多精彩內容