23、 聊聊akka(三) 集羣&持久化

Akka集羣支持去中心化的基於P2P的集羣服務,沒有單點故障(SPOF)問題,它主要是通過Gossip協議來實現。對於集羣成員的狀態,Akka提供了一種故障檢測機制,能夠自動發現出現故障而離開集羣的成員節點,通過事件驅動的方式,將狀態傳播到整個集羣的其它成員節點。

集羣概念

節點(node):集羣中的邏輯成員。允許一臺物理機上有多個節點。由元組hostname:port:uid唯一確定。
集羣(cluster):由成員關係服務構建的一組節點。
領導(leader):集羣中唯一扮演領導角色的節點。
種子節點(seed node):作爲其他節點加入集羣的連接點的節點。實際上,一個節點可以通過向集羣中的任何一個節點發送Join(加入)命令加入集羣。

這裏以Akka官網提供的成員狀態狀態圖爲例,如圖1所示。

圖1展示了狀態轉換的兩個因素:動作和狀態。

狀態

joining:節點正在加入集羣時的狀態。
weekly up:配置了akka.cluster.allow-weakly-up-members=on時,啓用的狀態。
up:集羣中節點的正常狀態。
leaving/exiting:優雅的刪除節點時,節點的狀態。
down:標記爲已下線的狀態。
removed:墓碑狀態,表示已經不再是集羣的成員。

動作

join:加入集羣。
leave:告知節點優雅的離開集羣。
down:標記集羣爲已下線。

配置

本節將要展示構建集羣所需要的最基本的配置, application.conf文件的內容如下:
akka {  
  actor {  
    provider = "akka.cluster.ClusterActorRefProvider"  
  }  
  remote {  
    log-remote-lifecycle-events = off  
    netty.tcp {  
      hostname = "127.0.0.1"  
      port = 2551  
    }  
  }  

  cluster {  
    seed-nodes = [  
      "akka.tcp://[email protected]:2551",  
      "akka.tcp://[email protected]:2552"
    ]

    # auto downing is NOT safe for production deployments. 
    auto-down-unreachable-after = 10s  

    # Disable legacy metrics in akka-cluster.   metrics 指標
    metrics.enabled=off    
  }  
}  

首先任何一個集羣都需要種子節點,作爲基本的加入集羣的連接點。本例中以我本地的兩個節點(分別監聽2551和2552端口)作爲種子節點。無論配置了多少個種子節點,除了在seed-nodes中配置的第一個種子節點需要率先啓動之外(否則其它種子節點無法初始化並且其它節點也無法加入),其餘種子節點都是啓動順序無關的。第一個節點需要率先啓動的另一個原因是如果每個節點都可以率先啓動,那麼有可能造成一個集羣出現幾個種子節點都啓動並且加入了自己的集羣,此時整個集羣實際上分裂爲幾個集羣,造成孤島。當你啓動了超過2個以上的種子節點,那麼第一個啓動的種子節點是可以關閉下線的。如果第一個種子節點重啓了,它將不會在自己創建集羣而是向其它種子節點發送Join消息加入已存在的集羣。

注意:除了akka.remote.netty.tcp.port配置項指定的端口不同,所有加入集羣節點的application.conf可以完全一樣。如果akka.remote.netty.tcp.port未指定,那麼Akka會爲你隨機選擇其他未佔用的端口。

集羣監聽器

創建一個簡單的集羣監聽器SimpleClusterListener(實際上是一個Actor,因爲繼承了UntypedActor),它向集羣訂閱MemberEvent(成員事件)和UnreachableMember(不可達成員)兩種消息,來對集羣成員進行管理(打印)

Cluster cluster = Cluster.get(getContext().system());  
public void preStart() {  
    cluster.subscribe(getSelf(),  
        ClusterEvent.initialStateAsEvents(), 
        MemberEvent.class, 
        UnreachableMember.class);  
}

@Override
public void onReceive(Object message) {
    if (message instanceof MemberUp) { //ClusterEvent.**
        MemberUp mUp = (MemberUp) message;
        log.info("Member is Up: {}", mUp.member());

    } else if (message instanceof UnreachableMember) { //ClusterEvent.UnreachableMember
        UnreachableMember mUnreachable = (UnreachableMember) message;
        log.info("Member detected as unreachable: {}", mUnreachable.member());

    } else if (message instanceof MemberRemoved) {  //ClusterEvent.**
        MemberRemoved mRemoved = (MemberRemoved) message;
        log.info("Member is Removed: {}", mRemoved.member());

    } else if (message instanceof MemberEvent) { //ClusterEvent.**
        // ignore  
    } else {
        unhandled(message);
    }
}    

21、聊聊akka(一)使用及集羣調用(負載)中集羣啓動日誌.
各個節點的狀態遷移信息,第一個種子節點正在加入自身創建的集羣時的狀態時JOINING,由於第一個種子節點將自己率先選舉爲Leader,因此它還將自己的狀態改變爲Up。後面它還將第二個種子節點和第三個節點從JOINING轉換到Up狀態。
關閉2553,其狀態首先被標記爲Down,最後被轉換爲Removed。

指定集羣中的角色

roles = [client] //服務消費端
roles = [backend] //服務提供端

TransformationMessages.java

/**
  * 服務提供方向服務調用方註冊
  */
public static final int BACKEND_REGISTRATION = 1;

MyAkkaClusterServer.java
在preStart方法中訂閱了集羣的MemberUp事件,自然會受到 onReceive 方法中的 :

else if (message instanceof ClusterEvent.MemberUp) {
    ClusterEvent.MemberUp mUp = (ClusterEvent.MemberUp) message;
    register(mUp.member());
}

  /**
   * 如果是客戶端角色,則向客戶端註冊自己的信息。客戶端收到消息以後會將這個服務端存到本機服務列表中
   */
  void register(Member member) {
      if (member.hasRole("client"))
          getContext().actorSelection(member.address() + "/user/myAkkaClusterClient").tell(BACKEND_REGISTRATION, getSelf());
  }

actorSelection

ActorSelection greeter = system.actorSelection("akka.tcp://MySystem@machine2:2552/user/greeter");

上面的幾行代碼簡單展示了akka中的分佈式環境中不同機器節點之間actor的相互通信方式,可以看出和Erlang很類似,即屏蔽底層節點之間的通信細節,然後提供簡單API接口。
actorOf / actorSelection / actorFor的區別:

actorOf 創建一個新的actor,創建的actor爲調用該方法所屬的context的直接子actor。
actorSelection 查找現有actor,並不會創建新的actor。
actorFor 查找現有actor,不創建新的actor,已過時。

一般做法:

 getContext().actorSelection(member.address() + "/user/myAkkaClusterClient")

這裏的myAkkaClusterClient 爲 member 服務的首字母小寫.

ActorSystem.actorSelection或ActorContext.actorSelection,可在任何角色內部通過context.actorSelection得到該對象的引用。在ActorSystem中,一個角色選擇就像產生了一個它的雙胞胎兄弟,而不是從啓動它的角色所在角色樹的根查找。路徑元素中包含兩個點(”..”)可以用來訪問父角色。你可以像下面的例子一樣向它的兄弟發送一條消息:
context.actorSelection(“../serviceA”) ! msg

通常情況下也可以通過絕對路徑在上下文中進行查找:
context.actorSelection(“/user/serviceA”) ! msg

它們都能正常的工作。

客戶端

客戶端除了監聽端口不同外,也需要增加akka.cluster.roles配置項,我們指定爲client。

@Override
    public void onReceive(Object message) {
        if ((message instanceof TransformationMessages.TransformationJob) && backends.isEmpty()) {//無服務提供者
            TransformationMessages.TransformationJob job = (TransformationMessages.TransformationJob) message;
            getSender().tell(
                    new TransformationMessages.JobFailed("Service unavailable, try again later", job),
                    getSender());

        } else if (message instanceof TransformationMessages.TransformationJob) {
            TransformationMessages.TransformationJob job = (TransformationMessages.TransformationJob) message;
            /**
             * 這裏在客戶端業務代碼裏進行負載均衡操作。實際業務中可以提供多種負載均衡策略,並且也可以做分流限流等各種控制。
             */
            jobCounter++;
            backends.get(jobCounter % backends.size()).forward(job, getContext());
        } else if (message instanceof  Integer && BACKEND_REGISTRATION == (int)message) { //服務提供方的註冊信息
            getContext().watch(getSender());//這裏對服務提供者進行watch
            backends.add(getSender());

        } else if (message instanceof Terminated) {
            Terminated terminated = (Terminated) message;
            backends.remove(terminated.getActor()); //移除服務提供者
        } else {
            unhandled(message);
        }
    }

持久化與快照

持久化 的目的,同存儲數據,來記錄歷史操作,及事故補償或會滾 .

<dependency>
    <groupId>com.typesafe.akka</groupId>
    <artifactId>akka-persistence_2.11</artifactId>
    <version>2.4.16</version>
</dependency>
<dependency>
  <groupId>org.iq80.leveldb</groupId>
  <artifactId>leveldb</artifactId>
  <version>0.7</version>
</dependency>
<dependency>
  <groupId>org.fusesource.leveldbjni</groupId>
  <artifactId>leveldbjni-all</artifactId>
  <version>1.8</version>
</dependency>

有關Akka的日誌持久化和快照持久化的配置如下:

# 持久化相關
akka.persistence.journal.plugin = "akka.persistence.journal.leveldb"  //akka.persistence.journal.inmem 
akka.persistence.journal.leveldb.dir = "target/example/journal"
akka.persistence.journal.leveldb.native = false //本地並沒有安裝leveldb,所以這個屬性置爲false
akka.persistence.snapshot-store.plugin = "akka.persistence.snapshot-store.local"

日誌插件使用了leveldb,leveldb的存儲目錄爲當前項目編譯路徑下的example/journal路徑下。快照插件使用了local,存儲路徑與前者相同。
參看 https://blog.csdn.net/beliefer/article/details/53925622
https://segmentfault.com/a/1190000010309436
1.akka-persistence-sql-async: 支持MySQL和PostgreSQL,另外使用了全異步的數據庫驅動,提供異步非阻塞的API,我司用的就是它的變種版,6的飛起。
2.akka-persistence-cassandra: 官方推薦的插件,使用寫性能very very very fast的cassandra數據庫,是幾個插件中比較流行的一個,另外它還支持persistence query。
3.akka-persistence-redis: redis應該也很符合Akka persistence的場景,熟悉redis的同學可以使用看看。
4.akka-persistence-jdbc: 怎麼能少了jdbc呢?不然怎麼對的起java爸爸呢,支持scala和java哦。
https://github.com/okumin/akka-persistence-sql-async
https://github.com/hootsuite/akka-persistence-redis
https://github.com/krasserm/akka-persistence-cassandra
https://github.com/dnvriend/akka-persistence-jdbc

批量持久化
上面說到我司用的是akka-persistence-sql-async插件,所以我們是將事件和快照持久化到數據庫的,一開始我也是像上面demo一樣,每次事件都會持久化到數據庫,但是後來在性能測試的時候,因爲本身業務場景對數據庫的壓力也比較大,在當數據庫到達每秒1000+的讀寫量後,另外說明一下使用的是某雲數據庫,性能中配以上,發現每次持久化的時間將近要15ms,這樣換算一下的話Actor每秒只能處理60~70個需要持久化的事件,而實際業務場景要求Actor必須在3秒內返回處理結果,這種情況下導致大量消息處理超時得不到反饋,另外還有大量的消息得不到處理,導致系統錯誤暴增,用戶體驗下降,既然我們發現了問題,那麼我們能不能進行優化呢?事實上當然是可以,既然單個插入慢,那麼我們能不能批量插入呢,Akka persistence爲我們提供了persistAll方法.
https://github.com/godpan/akka-demo

參看:https://blog.csdn.net/beliefer/article/details/53887181

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章