Akka RPC通信案例代码
1. 背景
- akka作为一个分布式异步通信框架,可以很好解决分布式集群中的异步信息同步问题
- akka是一个针对java和scala程序的框架,所以可以使用java也可以使用scala的api。
- 本文使用scala接口来进行akka简单master和worker通信的示例代码
- 环境准备
- idea 2020版本
- maven3.6.3
- scala 2.12.12
- jdk 1.8
2. 代码一
2.1 创建maven项目
maven的pom依赖文件
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.doit</groupId>
<artifactId>akka-rpc</artifactId>
<version>1.0-SNAPSHOT</version>
<!-- 常量 -->
<properties>
<!-- jdk 1.8版本-->
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
<!-- 字符编码是utf8 -->
<encoding>UTF-8</encoding>
<!-- scala版本是2.12.12,最新的2.13.3的版本了 -->
<scala.version>2.12.12</scala.version>
<scala.compat.version>2.12</scala.compat.version>
<!-- akka的版本是2.4.17,最新的现在是2.6.9 -->
<akka.version>2.4.17</akka.version>
</properties>
<dependencies>
<!-- scala的依赖 -->
<dependency>
<groupId>org.scala-lang</groupId>
<artifactId>scala-library</artifactId>
<!-- 这里取得是变量 -->
<version>${scala.version}</version>
</dependency>
<!-- akka actor依赖 -->
<dependency>
<groupId>com.typesafe.akka</groupId>
<artifactId>akka-actor_${scala.compat.version}</artifactId>
<version>${akka.version}</version>
</dependency>
<!-- akka远程通信依赖 -->
<dependency>
<groupId>com.typesafe.akka</groupId>
<artifactId>akka-remote_${scala.compat.version}</artifactId>
<version>${akka.version}</version>
</dependency>
</dependencies>
<build>
<pluginManagement>
<plugins>
<!-- 编译scala的插件 -->
<plugin>
<groupId>net.alchim31.maven</groupId>
<artifactId>scala-maven-plugin</artifactId>
<version>3.2.2</version>
</plugin>
<!-- 编译java的插件 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.5.1</version>
</plugin>
</plugins>
</pluginManagement>
<plugins>
<plugin>
<groupId>net.alchim31.maven</groupId>
<artifactId>scala-maven-plugin</artifactId>
<executions>
<execution>
<id>scala-compile-first</id>
<phase>process-resources</phase>
<goals>
<goal>add-source</goal>
<goal>compile</goal>
</goals>
</execution>
<execution>
<id>scala-test-compile</id>
<phase>process-test-resources</phase>
<goals>
<goal>testCompile</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<executions>
<execution>
<phase>compile</phase>
<goals>
<goal>compile</goal>
</goals>
</execution>
</executions>
</plugin>
<!-- 打包包含依赖的jar包插件 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>2.4.3</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<filters>
<filter>
<artifact>*:*</artifact>
<excludes>
<exclude>META-INF/*.SF</exclude>
<exclude>META-INF/*.DSA</exclude>
<exclude>META-INF/*.RSA</exclude>
</excludes>
</filter>
</filters>
<transformers>
<transformer
implementation="org.apache.maven.plugins.shade.resource.AppendingTransformer">
<resource>reference.conf</resource>
</transformer>
<!-- 指定maven-main方法 -->
<!-- <transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">-->
<!-- <mainClass>cn._51doit.rpc.Master</mainClass>-->
<!-- </transformer>-->
</transformers>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
2.2 scala代码思路
整体代码思路,就是这个图里面的
- 先启动主节点,然后启动从节点
- 从节点连接主节点,并发送注册信息
- 主节点接收并保存从节点的注册信息
- 主节点返回一个注册成功信息给从节点
- 从节点开始发送心跳信息给主节点
- 主节点定时检查心跳,如果出现超时连接补上的从节点就剔除
注意,akka遵循actor模型,这里面不管是master主节点还是worker从节点,都会有一个actorsystem单例对象,这个对象负责分发和创建actor对象。实际信息传输是发生在各个actor之间。
2.3 scala代码
2.3.1 master节点代码
package com.doit.akka.rpc.model
import akka.actor.{Actor, ActorRef, ActorSystem, Props}
import com.typesafe.config.{Config, ConfigFactory}
import scala.concurrent.duration._
import scala.collection.mutable
class Master extends Actor {
val map = new mutable.HashMap[String, WorkerInfo]()
override def preStart(): Unit = {
// 这里启动定时器,检查从节点的连接
import context.dispatcher
// 第一个参数是什么时候启动,第二个参数是间隔,第三个参数是发给哪个actor,第四个是发送的信息
context.system.scheduler.schedule(0.millisecond, 15000.millisecond, self, CheckTimeOut)
}
// master接收到数据后的匹配处理
override def receive: Receive = {
// worker注册的消息
case WorkerRegisterInfo(workerid, memory, cores) => {
// 保存起来
if (!map.contains(workerid)) {
println("收到注册信息")
// 因为master节点需要保存worker从节点发送的注册信息,使用case calss进行保存
// case class自己实现了很多方法,如序列化,apply,getter,setter,equals,hashcode等等
val workerInfo: WorkerInfo = WorkerInfo(workerid, memory, cores)
// 使用map来保存注册信息
map.put(workerid, workerInfo)
// 告诉从节点,注册成功
sender() ! RegisterSuccess
}
}
// 检查超时
case CheckTimeOut => {
// 查看一下worker字典中信息,看一下时间是否超时了,超时就将其从字典中删除
val workerInfoes: Iterable[WorkerInfo] = map.values.filter(w => System.currentTimeMillis() - w.lastHeartbeatTime > 10000)
// 删除超时的
workerInfoes.foreach(w => {
map.remove(w.workerid)
})
println("存活worker个数:" + map.values.size)
}
// 心跳
case Heartbeat(workid) => {
// 收到心跳包之后,更新对应心跳的时间
map(workid).lastHeartbeatTime = System.currentTimeMillis()
println("心跳时间:" + map(workid).lastHeartbeatTime)
}
}
}
object Master {
def main(args: Array[String]): Unit = {
val masterHost = "localhost"
val masterPort = 9999
val configStr =
s"""
|akka.actor.provider = "akka.remote.RemoteActorRefProvider"
|akka.remote.netty.tcp.hostname = $masterHost
|akka.remote.netty.tcp.port = $masterPort
|""".stripMargin
val config: Config = ConfigFactory.parseString(configStr)
// 创建master节点上的actorsystem对象,单例
val actorSystem: ActorSystem = ActorSystem.apply("Master-Actor-System", config)
// 使用actorsystem创建actor对象
val actorRef: ActorRef = actorSystem.actorOf(Props[Master], "Master-Actor")
}
}
2.3.2 worker节点代码
package com.doit.akka.rpc.model
import akka.actor.{Actor, ActorRef, ActorSelection, ActorSystem, Props}
import com.typesafe.config.{Config, ConfigFactory}
import scala.concurrent.duration._
class Worker extends Actor {
var actorSelection: ActorSelection = null;
override def preStart(): Unit = {
// 建立连接
actorSelection = context.actorSelection("akka.tcp://Master-Actor-System@localhost:9999/user/Master-Actor")
// 发送注册的信息,workerid,内存,cpu核心数
actorSelection ! WorkerRegisterInfo("3", 4096, 4)
}
// 这里接收收到的消息
override def receive: Receive = {
// 注册成功消息
case RegisterSuccess => {
// 发送一个给自己的消息,要发心跳了
println("注册成功")
import context.dispatcher
context.system.scheduler.schedule(0.millisecond, 10000.millisecond, self, SendHeartBeat)
}
// 发心跳的信号
case SendHeartBeat => {
println("发送心跳信号")
// 需要发送给主节点,注意不要使用sender(),因为SendHeartBeat其实是worker自己发给自己的
actorSelection ! Heartbeat("3")
}
}
}
object Worker {
def main(args: Array[String]): Unit = {
// 这里进行worker的创建
val workerHost = "localhost"
val workerPort = 8888
val confiStr =
s"""
|akka.actor.provider = "akka.remote.RemoteActorRefProvider"
|akka.remote.netty.tcp.hostname = $workerHost
|akka.remote.netty.tcp.port = $workerPort
|""".stripMargin
val config: Config = ConfigFactory.parseString(confiStr)
val actorSystem: ActorSystem = ActorSystem.apply("Worker-Actor-System", config)
val actorRef: ActorRef = actorSystem.actorOf(Props[Worker], "Worker-Actor")
}
}
2.3.3 各个case class、case object等代码
- 检查超时的actor信息,这是master节点发送给自己的信息。当master节点启动后,就会开启一个定时器,定时发送这个信息给自己,自己接收到这个信息后,再receive方法中进行处理。
package com.doit.akka.rpc.model
case object CheckTimeOut
- 心跳actor信息,当worker节点建立起和master节点的连接,注册成功后,就会开始定时发送心跳信息。这个是worker节点发送给master节点的信息。
package com.doit.akka.rpc.model
case class Heartbeat(workid: String)
- 这是worker建立起和master节点,注册成功后,master节点发送给worker节点的注册成功信息。
package com.doit.akka.rpc.model
case object RegisterSuccess
- 这是worker节点接收到master节点返回的注册成功信息后,worker节点自己发给自己的actor信息,告诉自己可以开始往master节点发送心跳信息了。
package com.doit.akka.rpc.model
case object SendHeartBeat
- 这是主节点接收到worker节点发送的注册信息后,因为master节点需要保存worker节点的各种信息,所以建立的一个case class,用来保存信息的。
package com.doit.akka.rpc.model
case class WorkerInfo(workerid:String, memory:Int, cpuCores:Int){
var lastHeartbeatTime: Long = _
}
- 这是worker节点建立起和master节点的连接后,worker节点发送给master节点的注册信息,里面会包含worker节点的id,内存,cpu等信息。
package com.doit.akka.rpc.model
case class WorkerRegisterInfo(workerid:String, memory:Int, cpuCores:Int){}
注意
- 这里面使用case class,case object来进行信息传递和数据存储,这是因为本身case class和case object已经由scala实现了很多方法,如序列化,equals、hashcode,getter,setter等等。但这并不是一定的,其实还可以使用更高效的数据传输方式,这一点akka其实有警告信息提示
[WARN] [SECURITY][09/14/2020 19:59:39.082] [Master-Actor-System-akka.remote.default-remote-dispatcher-17] [akka.serialization.Serialization(akka://Master-Actor-System)] Using the default Java serializer for class [com.doit.akka.rpc.model.RegisterSuccess$] which is not recommended because of performance implications. Use another serializer or disable this warning using the setting 'akka.actor.warn-about-java-serializer-usage'
- akka里使用的是actor通信模型,也会有发送者和接收者概念,还是用到了隐式参数,所以隐式context中其实会包含各种需要的信息和对象。包括定时器,发送者,接收者等等。
- akka其实也是有生命周期,所以这里的prestart、receive都是其中的生命周期函数之一。同样的也会有结束/关闭前 的方法
如下图:- 如果跟hdfs的namenode和datanode节点之间的通信机制进行对比,会发现其实2者非常相近,非常非常像。
- 注意,本身akka是用于分布式的异步通信框架或者工具,这里演示是使用的本地模式来掩饰,可以将程序打成jar 包,发散到各个集群节点中进行通信,不过代码中写死的参数就需要做一定调整,改为从外部参数输入或者读取配置文件等方式来保证参数可修改。
- akka其实应用于spark、flink框架,但在国内,直接用于应用层的案例并不多,因为要做到分布式集群通信有更加通用并且验证有效的解决方案。但使用与底层通信会更多一些,这一点从spark、flink采用akka来通信就可知一二。
3. 代码二
这里是基于上述代码,对代码做改进,将写死的代码改为外部传入,冗余代码做合并。
- 定义的case class、case object合并到一个文件中
package com.doit.akka.rpc2.model
// 超时检查信号,master-》自己
case object CheckTimeOut
// 心跳信号,worker-》master
case class Heartbeat(workid: String)
// 注册成功,master-》worker
case object RegisterSuccess
// 发送心跳,worker->自己
case object SendHeartBeat
// master节点保存worker信息的case class;
// 注意这里还有一个lastHeartbeatTime,属于变量,master定期收到心跳信息,收到时,就更新一下这个心跳时间。
case class WorkerInfo(workerid:String, memory:Int, cpuCores:Int){
var lastHeartbeatTime: Long = _
}
// worker节点信息,worker->master的注册信息
case class WorkerRegisterInfo(workerid:String, memory:Int, cpuCores:Int){}
- worker代码
package com.doit.akka.rpc2.model
import java.util.UUID
import akka.actor.{Actor, ActorRef, ActorSelection, ActorSystem, Props}
import com.typesafe.config.{Config, ConfigFactory}
import scala.concurrent.duration._
class Worker(val masterDomain:String, val masterPort:String, val memory:Int, val cpuCores:Int) extends Actor {
var actorSelection: ActorSelection = null;
// workerid使用随机字符串,真实情况可能会按照需求从配置文件中读取
val workerid = UUID.randomUUID().toString
override def preStart(): Unit = {
// 建立连接
actorSelection = context.actorSelection(s"akka.tcp://${Worker.Worker_Actor_System}@${masterDomain}:${masterPort}/user/${Worker.Worker_Actor}")
// 发送注册的信息,workerid,内存,cpu核心数
actorSelection ! WorkerRegisterInfo(workerid, memory, cpuCores)
}
// 这里接收收到的消息
override def receive: Receive = {
// 注册成功消息
case RegisterSuccess => {
// 发送一个给自己的消息,要发心跳了
println("注册成功")
import context.dispatcher
context.system.scheduler.schedule(0.millisecond, 10000.millisecond, self, SendHeartBeat)
}
// 发心跳的信号
case SendHeartBeat => {
println("发送心跳信号")
// 需要发送给主节点,注意不要使用sender(),因为SendHeartBeat其实是worker自己发给自己的
actorSelection ! Heartbeat(workerid)
}
}
}
object Worker {
val Worker_Actor_System = "Worker-Actor-System"
val Worker_Actor = "Worker-Actor"
def main(args: Array[String]): Unit = {
// main方法这里进行worker的创建
// val masterDomain:String, val masterPort:String, val memory:Int, val cpuCores:Int
val workerHost = args(0) // 如"localhost"
val workerPort = args(1)// 如8888
val masterDomain = args(2)
val masterPort = args(3)
val memory = args(4).toInt
val cpuCores = args(5).toInt
val confiStr =
s"""
|akka.actor.provider = "akka.remote.RemoteActorRefProvider"
|akka.remote.netty.tcp.hostname = $workerHost
|akka.remote.netty.tcp.port = $workerPort
|""".stripMargin
// 根据配置字符串,创建config对象
val config: Config = ConfigFactory.parseString(confiStr)
// 创建worker的actorSystem单例对象
val actorSystem: ActorSystem = ActorSystem.apply(Worker_Actor_System, config)
// 创建Props对象
val props: Props = Props(new Worker(masterDomain, masterPort, memory, cpuCores))
// 创建actor对象
val actorRef: ActorRef = actorSystem.actorOf(props, Worker_Actor)
}
}
- master代码
package com.doit.akka.rpc2.model
import akka.actor.{Actor, ActorRef, ActorSystem, Props}
import com.typesafe.config.{Config, ConfigFactory}
import scala.concurrent.duration._
import scala.collection.mutable
class Master extends Actor {
val map = new mutable.HashMap[String, WorkerInfo]()
override def preStart(): Unit = {
// 这里启动定时器,检查从节点的连接
import context.dispatcher
// 第一个参数是什么时候启动,第二个参数是间隔,第三个参数是发给哪个actor,第四个是发送的信息
context.system.scheduler.schedule(0.millisecond, 15000.millisecond, self, CheckTimeOut)
}
// master接收到数据后的匹配处理
override def receive: Receive = {
// worker注册的消息
case WorkerRegisterInfo(workerid, memory, cores) => {
// 保存起来
if (!map.contains(workerid)) {
println("收到注册信息")
// 因为master节点需要保存worker从节点发送的注册信息,使用case calss进行保存
// case class自己实现了很多方法,如序列化,apply,getter,setter,equals,hashcode等等
val workerInfo: WorkerInfo = WorkerInfo(workerid, memory, cores)
// 使用map来保存注册信息
map.put(workerid, workerInfo)
// 告诉从节点,注册成功
sender() ! RegisterSuccess
}
}
// 检查超时
case CheckTimeOut => {
// 查看一下worker字典中信息,看一下时间是否超时了,超时就将其从字典中删除
val workerInfoes: Iterable[WorkerInfo] = map.values.filter(w => System.currentTimeMillis() - w.lastHeartbeatTime > 10000)
// 删除超时的
workerInfoes.foreach(w => {
map.remove(w.workerid)
})
println("存活worker个数:" + map.values.size)
}
// 心跳
case Heartbeat(workid) => {
// 收到心跳包之后,更新对应心跳的时间
map(workid).lastHeartbeatTime = System.currentTimeMillis()
println("心跳时间:" + map(workid).lastHeartbeatTime)
}
}
}
object Master {
val Master_Actor_System = "Master-Actor-System"
val Master_Actor = "Master-Actor"
def main(args: Array[String]): Unit = {
val masterHost = args(0) // 如 "localhost"
val masterPort = args(1) // 如9999
val configStr =
s"""
|akka.actor.provider = "akka.remote.RemoteActorRefProvider"
|akka.remote.netty.tcp.hostname = $masterHost
|akka.remote.netty.tcp.port = $masterPort
|""".stripMargin
val config: Config = ConfigFactory.parseString(configStr)
// 创建master节点上的actorsystem对象,单例
val actorSystem: ActorSystem = ActorSystem.apply(Master_Actor_System, config)
// 使用actorsystem创建actor对象
val actorRef: ActorRef = actorSystem.actorOf(Props[Master], Master_Actor)
}
}
- 打成jar包
original的是不带依赖的jar包 - 调用未指定main方法jar包的方法
java -cp xxx.jar yy.MainClass arg0 arg1 arg2...
- 实际运行效果
master节点的全类名和参数
com.doit.akka.rpc2.model.Master
192.168.133.2
9999
worker节点的全类名和参数