Spring Boot集成Akka Cluster快速入门Demo

1.什么是Akka Cluster?

Akka Cluster将多个JVM连接整合在一起,实现消息地址的透明化和统一化使用管理,集成一体化的消息驱动系统。最终目的是将一个大型程序分割成若干子程序,部署到很多JVM上去实现程序的分布式并行运算(单机也可以起很多节点构成集群)。更重要的是, Akka Cluster集群构建与Actor编程没有直接的联系,集群构建是在ActorSystem层面上,实现了Actor消息地址的透明化,无需考虑目标运行环节是否分布式,可以按照正常的Actor编程模式进行开发。 我们知道,分布式集群是由若干节点组成的,那么节点的发现及状态管理是分布式系统一个比较重要的任务。Akka Cluster中将节点的生命周期划分为:

  • joining - 当尝试加入集群时的初始状态
  • up - 加入集群后的正常状态
  • leaving / exiting - 节点退出集群时的中间状态
  • down - 集群无法感知某节点后,将其标记为down
  • removed - 从集群中被删除,以后也无法再加入集群

其实当参数akka.cluster.allow-weakly-up-members启用时(默认是启用的),还有个weakly up,它是用于集群出现分裂时,集群无法收敛,则leader无法将状态置为up的临时状态。这个后面再解释。 图中还有两个特殊的名词:

  • fd* - 这个表示akka的错误检测机制Faiulre Detector被触发后,将节点标记为unreachable
  • unreachable* - unreachable不是一个真正的节点状态,更多的像是一个flag,用来描述集群无法与该节点进行通讯。当错误检测机制侦测到这个节点又能正常通讯时,会移除这个flag。

市面上大多数产品的分布式管理一般用的是注册中心机制,例如zk、consul或etcd。其实是节点把自己的信息注册到所使用的注册中心里,而master通过接受注册中心的通知得知新节点信息。显然本质上是一种master/slave的架构。这种架构有两个问题:

  1. master节点一般是单一的,一旦挂了影响就比较大(所以很多master都采用了HA机制),也就是所谓的系统单点故障;
  2. 通常节点的地址发现是要走master去获取的,当系统并发大时,master节点就可能成为性能瓶颈,即单点性能瓶颈。

Akka可能就是考虑这两点,采用了P2P的模式,这样任何一个节点都可以作为"master",任何的节点都可以用来寻找其他节点地址。那它是怎么做到的呢?答案是Gossip协议和CRDT。这里不做过多解释,感兴趣的话可以自己去翻阅相关介绍

2.代码工程

实验目的

搭建一个简单akka custer集群

pom.xml

复制代码
<!-- Akka Cluster dependency -->
<dependency>
    <groupId>com.typesafe.akka</groupId>
    <artifactId>akka-cluster-typed_2.13</artifactId>
    <version>2.6.0</version>
</dependency>

cluster

node1.conf

复制代码
akka {
  actor {
    provider = "cluster"  
  }

  remote {
    artery {
      canonical.hostname = "127.0.0.1"
      canonical.port = 2551 
    }
  }

  cluster {
    seed-nodes = [
      "akka://ClusterSystem@127.0.0.1:2551",
      "akka://ClusterSystem@127.0.0.1:2552"
    ]
  }
}

node2.conf

复制代码
akka {
  actor {
    provider = "cluster"
  }

  remote {
    artery {
      canonical.hostname = "127.0.0.1"
      canonical.port = 2552  
    }
  }

  cluster {
    seed-nodes = [
      "akka://ClusterSystem@127.0.0.1:2551",
      "akka://ClusterSystem@127.0.0.1:2552"
    ]
  }
}

集群监听器

复制代码
package com.et.akka.cluster;

import akka.actor.typed.Behavior;
import akka.actor.typed.javadsl.AbstractBehavior;
import akka.actor.typed.javadsl.ActorContext;
import akka.actor.typed.javadsl.Behaviors;
import akka.actor.typed.javadsl.Receive;
import akka.cluster.typed.Cluster;
import akka.cluster.typed.Subscribe;
import akka.cluster.ClusterEvent;

public class ClusterListener extends AbstractBehavior<ClusterEvent.ClusterDomainEvent> {

    public ClusterListener(ActorContext<ClusterEvent.ClusterDomainEvent> context) {
        super(context);

        Cluster cluster = Cluster.get(context.getSystem());
        cluster.subscriptions().tell(Subscribe.create(getContext().getSelf(), ClusterEvent.ClusterDomainEvent.class));
    }

    @Override
    public Receive<ClusterEvent.ClusterDomainEvent> createReceive() {
        return newReceiveBuilder()
                .onMessage(ClusterEvent.MemberUp.class, this::onMemberUp)
                .onMessage(ClusterEvent.MemberRemoved.class, this::onMemberRemoved)
                .onAnyMessage(event -> {
                    System.out.println("Received cluster event: " + event);
                    return this;
                })
                .build();
    }

    private Behavior<ClusterEvent.ClusterDomainEvent> onMemberUp(ClusterEvent.MemberUp memberUp) {
        System.out.println("Member is Up: " + memberUp.member());
        return this;
    }

    private Behavior<ClusterEvent.ClusterDomainEvent> onMemberRemoved(ClusterEvent.MemberRemoved memberRemoved) {
        System.out.println("Member is Removed: " + memberRemoved.member());
        return this;
    }

    public static Behavior<ClusterEvent.ClusterDomainEvent> create() {
        return Behaviors.setup(ClusterListener::new);
    }
}

启动集群

复制代码
package com.et.akka.cluster;

import akka.actor.typed.ActorSystem;
import akka.cluster.ClusterEvent;
import com.typesafe.config.Config;
import com.typesafe.config.ConfigFactory;

import java.io.File;

public class ClusterApp {

    public static void main(String[] args) {
        Config configNode1 = ConfigFactory.parseFile(new File("D:/IdeaProjects/ETFramework/akka/src/main/resources/node1.conf"))
            .withFallback(ConfigFactory.load());

        ActorSystem<ClusterEvent.ClusterDomainEvent> systemNode1 = ActorSystem.create(ClusterListener.create(), "ClusterSystem", configNode1);
        System.out.println("Node 1 started with config from node1.conf");

        Config configNode2 = ConfigFactory.parseFile(new File("D:/IdeaProjects/ETFramework/akka/src/main/resources/node2.conf"))
            .withFallback(ConfigFactory.load());

        ActorSystem<ClusterEvent.ClusterDomainEvent> systemNode2 = ActorSystem.create(ClusterListener.create(), "ClusterSystem", configNode2);
        System.out.println("Node 2 started with config from node2.conf");
    }
}

以上只是一些关键代码,所有代码请参见下面代码仓库

代码仓库

3.测试

启动集群(执行ClusterApp里面的main方法),查看日志可以看到2个节点都起来了

复制代码
23:00:19.201 [ClusterSystem-akka.actor.default-dispatcher-6] INFO akka.cluster.Cluster - Cluster Node [akka://ClusterSystem@127.0.0.1:2552] - Welcome from [akka://ClusterSystem@127.0.0.1:2551]
Member is Up: Member(address = akka://ClusterSystem@127.0.0.1:2551, status = Up)
Received cluster event: MemberJoined(Member(address = akka://ClusterSystem@127.0.0.1:2552, status = Joining))
Received cluster event: LeaderChanged(Some(akka://ClusterSystem@127.0.0.1:2551))
Received cluster event: RoleLeaderChanged(dc-default,Some(akka://ClusterSystem@127.0.0.1:2551))
Received cluster event: SeenChanged(true,Set(akka://ClusterSystem@127.0.0.1:2551, akka://ClusterSystem@127.0.0.1:2552))
Received cluster event: ReachabilityChanged()
Received cluster event: SeenChanged(true,Set(akka://ClusterSystem@127.0.0.1:2551, akka://ClusterSystem@127.0.0.1:2552))
Received cluster event: ReachabilityChanged()
23:00:19.645 [ClusterSystem-akka.actor.default-dispatcher-5] INFO akka.cluster.Cluster - Cluster Node [akka://ClusterSystem@127.0.0.1:2551] - Leader is moving node [akka://ClusterSystem@127.0.0.1:2552] to [Up]
Member is Up: Member(address = akka://ClusterSystem@127.0.0.1:2552, status = Up)
Received cluster event: SeenChanged(false,Set(akka://ClusterSystem@127.0.0.1:2551))
Member is Up: Member(address = akka://ClusterSystem@127.0.0.1:2552, status = Up)
Received cluster event: ReachabilityChanged()
Received cluster event: SeenChanged(true,Set(akka://ClusterSystem@127.0.0.1:2551, akka://ClusterSystem@127.0.0.1:2552))
Received cluster event: ReachabilityChanged()

4.引用

相关推荐
月落星还在12 分钟前
Spring MVC 与 Spring Boot:从“手动挡”到“自动驾驶”的进化论,兼谈前后端分离的哲学
spring boot·spring·云原生·mvc
蓝田~25 分钟前
大模型本地部署与远程调用 — 从 API 到 Agent
java·人工智能·claude·claude code
码农进化录32 分钟前
Java 程序员的 AI 进化论 | Spring Boot 接入 OpenAI 的六个坑,全帮你踩了
java·spring boot·openai
她说彩礼65万35 分钟前
Asp.net core 模型绑定
后端·asp.net
用户342323237631739 分钟前
BLE 传感器节点实战
后端
疯狂成瘾者40 分钟前
Java 常见集合方法
java·windows·python
从此以后自律41 分钟前
ConcurrentHashMap 超详细详解
java
fliter1 小时前
Go 1.26 新增 `bytes.Buffer.Peek`:只看数据,不移动读取位置
后端
羑悻1 小时前
我受够了 Coding Agent 的“假聪明,真串行”,于是做了 Meta-Orchestrator!
后端
神奇小汤圆1 小时前
字节二面挂了!被问“订单超时自动取消”
后端