Akka是一个用于构建高并发、分布式和可扩展的基于事件驱动的应用的工具包。Akka是使用scala开发的库,同时可以使用scala和Java语言来开发基于Akka的应用程序。
以下图片说明了Akka Actor的并发编程模型的基本流程:
Akka中,也是基于Actor来进行编程的。类似于之前学习过的Actor。但是Akka的Actor的编写、创建方法和之前有一些不一样。
ActorSystem
在Akka中,ActorSystem是一个重量级的结构,它需要分配多个线程,所以在实际应用中,ActorSystem通常是一个单例对象,可以使用这个ActorSystem创建很多Actor。它负责创建和监督actor
Actor中获取ActorSystem
直接使用context.system就可以获取到管理该Actor的ActorSystem的引用
实现Actor类
加载Akka Actor
Actor Path
每一个Actor都有一个Path,就像使用Spring MVC编写一个Controller/Handler一样,这个路径可以被外部引用。路径的格式如下:
Actor类型 | 路径 | 示例 |
---|---|---|
本地Actor | akka://actorSystem名称/user/Actor名称 | akka://SimpleAkkaDemo/user/senderActor |
远程Actor | akka.tcp://my-sys@ip地址:port/user/Actor名称 | akka.tcp://192.168.10.17:5678/user/service-b |
案例说明
基于Akka创建两个Actor,Actor之间可以互相发送消息。
<properties>
<maven.compiler.source>1.8maven.compiler.source>
<maven.compiler.target>1.8maven.compiler.target>
<encoding>UTF-8encoding>
<scala.version>2.11.12scala.version>
<scala.compat.version>2.11scala.compat.version>
properties>
<dependencies>
<dependency>
<groupId>org.scala-langgroupId>
<artifactId>scala-libraryartifactId>
<version>${scala.version}version>
dependency>
<dependency>
<groupId>com.typesafe.akkagroupId>
<artifactId>akka-actor_2.11artifactId>
<version>2.3.14version>
dependency>
<dependency>
<groupId>com.typesafe.akkagroupId>
<artifactId>akka-remote_2.11artifactId>
<version>2.3.14version>
dependency>
dependencies>
<build>
<sourceDirectory>src/main/scalasourceDirectory>
<testSourceDirectory>src/test/scalatestSourceDirectory>
<plugins>
<plugin>
<groupId>net.alchim31.mavengroupId>
<artifactId>scala-maven-pluginartifactId>
<version>3.2.2version>
<executions>
<execution>
<goals>
<goal>compilegoal>
<goal>testCompilegoal>
goals>
<configuration>
<args>
<arg>-dependencyfilearg>
<arg>${project.build.directory}/.scala_dependenciesarg>
args>
configuration>
execution>
executions>
plugin>
<plugin>
<groupId>org.apache.maven.pluginsgroupId>
<artifactId>maven-shade-pluginartifactId>
<version>2.4.3version>
<executions>
<execution>
<phase>packagephase>
<goals>
<goal>shadegoal>
goals>
<configuration>
<filters>
<filter>
<artifact>*:*artifact>
<excludes>
<exclude>META-INF/*.SFexclude>
<exclude>META-INF/*.DSAexclude>
<exclude>META-INF/*.RSAexclude>
excludes>
filter>
filters>
<transformers>
<transformer implementation="org.apache.maven.plugins.shade.resource.AppendingTransformer">
<resource>reference.confresource>
transformer>
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<mainClass>mainClass>
transformer>
transformers>
configuration>
execution>
executions>
plugin>
plugins>
build>
使用Akka需要导入Akka库,我们这里使用Maven来管理项目
创建两个Actor
创建Actor
!
发送异步消息参考代码
case class SubmitTaskMessage(msg:String)
case class SuccessSubmitTaskMessage(msg:String)
// 注意:要导入的是Akka下的Actor
object SenderActor extends Actor {
override def preStart(): Unit = println("执行SenderActor的preStart()方法")
override def receive: Receive = {
case "start" =>
val receiveActor = this.context.actorSelection("/user/receiverActor")
receiveActor ! SubmitTaskMessage("请完成#001任务!")
case SuccessSubmitTaskMessage(msg) =>
println(s"接收到来自${sender.path}的消息: $msg")
}
}
object ReceiverActor extends Actor {
override def preStart(): Unit = println("执行ReceiverActor()方法")
override def receive: Receive = {
case SubmitTaskMessage(msg) =>
println(s"接收到来自${sender.path}的消息: $msg")
sender ! SuccessSubmitTaskMessage("完成提交")
case _ => println("未匹配的消息类型")
}
}
object SimpleAkkaDemo {
def main(args: Array[String]): Unit = {
val actorSystem = ActorSystem("SimpleAkkaDemo", ConfigFactory.load())
val senderActor: ActorRef = actorSystem.actorOf(Props(SenderActor), "senderActor")
val receiverActor: ActorRef = actorSystem.actorOf(Props(ReceiverActor), "receiverActor")
senderActor ! "start"
}
}
程序输出:
接收到来自akka://SimpleAkkaDemo/user/senderActor的消息: 请完成#001任务!
接收到来自akka://SimpleAkkaDemo/user/receiverActor的消息: 完成提交
如果我们想要使用Akka框架定时的执行一些任务,该如何处理呢?
Akka中,提供一个scheduler对象来实现定时调度功能。使用ActorSystem.scheduler.schedule方法,可以启动一个定时任务。
schedule方法针对scala提供两种使用形式:
第一种:发送消息
def schedule(
initialDelay: FiniteDuration, // 延迟多久后启动定时任务
interval: FiniteDuration, // 每隔多久执行一次
receiver: ActorRef, // 给哪个Actor发送消息
message: Any) // 要发送的消息
(implicit executor: ExecutionContext) // 隐式参数:需要手动导入
第二种:自定义实现
def schedule(
initialDelay: FiniteDuration, // 延迟多久后启动定时任务
interval: FiniteDuration // 每隔多久执行一次
)(f: ⇒ Unit) // 定期要执行的函数,可以将逻辑写在这里
(implicit executor: ExecutionContext) // 隐式参数:需要手动导入
示例说明
参考代码
// 1. 创建一个Actor,用来接收消息,打印消息
object ReceiveActor extends Actor {
override def receive: Receive = {
case x => println(x)
}
}
// 2. 构建ActorSystem,加载Actor
def main(args: Array[String]): Unit = {
val actorSystem = ActorSystem("actorSystem", ConfigFactory.load())
val receiveActor = actorSystem.actorOf(Props(ReceiveActor))
// 3. 启动scheduler,定期发送消息给Actor
// 导入一个隐式转换
import scala.concurrent.duration._
// 导入隐式参数
import actorSystem.dispatcher
actorSystem.scheduler.schedule(0 seconds,
1 seconds,
receiveActor, "hello")
}
示例说明
参考代码
object SechdulerActor extends Actor {
override def receive: Receive = {
case "timer" => println("收到消息...")
}
}
object AkkaSchedulerDemo {
def main(args: Array[String]): Unit = {
val actorSystem = ActorSystem("SimpleAkkaDemo", ConfigFactory.load())
val senderActor: ActorRef = actorSystem.actorOf(Props(SechdulerActor), "sechdulerActor")
import actorSystem.dispatcher
import scala.concurrent.duration._
actorSystem.scheduler.schedule(0 seconds, 1 seconds) {
senderActor ! "timer"
}
}
}
[!NOTE]
- 需要导入隐式转换
import scala.concurrent.duration._
才能调用0 seconds方法- 需要导入隐式参数
import actorSystem.dispatcher
才能启动定时任务
基于Akka实现在两个进程间发送、接收消息。Worker启动后去连接Master,并发送消息,Master接收到消息后,再回复Worker消息。
步骤
akka.actor.provider = "akka.remote.RemoteActorRefProvider"
akka.remote.netty.tcp.hostname = "127.0.0.1"
akka.remote.netty.tcp.port = "9999"
参考代码
Worker.scala
val workerActorSystem = ActorSystem("actorSystem", ConfigFactory.load())
val workerActor: ActorRef = workerActorSystem.actorOf(Props(WorkerActor), "WorkerActor")
// 发送消息给WorkerActor
workerActor ! "setup"
WorkerActor.scala
object WorkerActor extends Actor{
override def receive: Receive = {
case "setup" =>
println("WorkerActor:启动Worker")
}
}
步骤
akka.actor.provider = "akka.remote.RemoteActorRefProvider"
akka.remote.netty.tcp.hostname = "127.0.0.1"
akka.remote.netty.tcp.port = "8888"
参考代码
Master.scala
val masterActorSystem = ActorSystem("MasterActorSystem", ConfigFactory.load())
val masterActor: ActorRef = masterActorSystem.actorOf(Props(MasterActor), "MasterActor")
MasterActor.scala
object MasterActor extends Actor{
override def receive: Receive = {
case "connect" =>
println("2. Worker连接到Master")
sender ! "success"
}
}
WorkerActor.scala
object WorkerActor extends Actor{
override def receive: Receive = {
case "setup" =>
println("1. 启动Worker...")
val masterActor = context.actorSelection("akka.tcp://[email protected]:9999/user/MasterActor")
// 发送connect
masterActor ! "connect"
case "success" =>
println("3. 连接Master成功...")
}
}
模拟Spark的Master与Worker通信
项目使用Maven搭建工程
步骤
工程名 | 说明 |
---|---|
spark-demo-common | 存放公共的消息、实体类 |
spark-demo-master | Akka Master节点 |
spark-demo-worker | Akka Worker节点 |
分别构建Master和Worker,并启动测试
步骤
参考代码
Master.scala
val sparkMasterActorSystem = ActorSystem("sparkMaster", ConfigFactory.load())
val masterActor = sparkMasterActorSystem.actorOf(Props(MasterActor), "masterActor")
MasterActor.scala
object MasterActor extends Actor{
override def receive: Receive = {
case x => println(x)
}
}
Worker.scala
val sparkWorkerActorSystem = ActorSystem("sparkWorker", ConfigFactory.load())
sparkWorkerActorSystem.actorOf(Props(WorkerActor), "workerActor")
WorkerActor.scala
object WorkerActor extends Actor{
override def receive: Receive = {
case x => println(x)
}
}
在Worker启动时,发送注册消息给Master
步骤
参考代码
MasterActor.scala
object MasterActor extends Actor{
private val regWorkerMap = collection.mutable.Map[String, WorkerInfo]()
override def receive: Receive = {
case WorkerRegisterMessage(workerId, cpu, mem) => {
println(s"1. 注册新的Worker - ${workerId}/${cpu}核/${mem/1024.0}G")
regWorkerMap += workerId -> WorkerInfo(workerId, cpu, mem, new Date().getTime)
sender ! RegisterSuccessMessage
}
}
}
WorkerInfo.scala
/**
* 工作节点信息
* @param workerId workerid
* @param cpu CPU核数
* @param mem 内存多少
* @param lastHeartBeatTime 最后心跳更新时间
*/
case class WorkerInfo(workerId:String, cpu:Int, mem:Int, lastHeartBeatTime:Long)
MessagePackage.scala
/**
* 注册消息
* @param workerId
* @param cpu CPU核数
* @param mem 内存大小
*/
case class WorkerRegisterMessage(workerId:String, cpu:Int, mem:Int)
/**
* 注册成功消息
*/
case object RegisterSuccessMessage
WorkerActor.scala
object WorkerActor extends Actor{
private var masterActor:ActorSelection = _
private val CPU_LIST = List(1, 2, 4, 6, 8)
private val MEM_LIST = List(512, 1024, 2048, 4096)
override def preStart(): Unit = {
masterActor = context.system.actorSelection("akka.tcp://[email protected]:7000/user/masterActor")
val random = new Random()
val workerId = UUID.randomUUID().toString.hashCode.toString
val cpu = CPU_LIST(random.nextInt(CPU_LIST.length))
val mem = MEM_LIST(random.nextInt(MEM_LIST.length))
masterActor ! WorkerRegisterMessage(workerId, cpu, mem)
}
...
}
Worker接收到Master返回注册成功后,发送心跳消息。而Master收到Worker发送的心跳消息后,需要更新对应Worker的最后心跳时间。
步骤
参考代码
ConfigUtil.scala
object ConfigUtil {
private val config: Config = ConfigFactory.load()
val `worker.heartbeat.interval` = config.getInt("worker.heartbeat.interval")
}
MessagePackage.scala
package com.itheima.spark.common
...
/**
* Worker心跳消息
* @param workerId
* @param cpu CPU核数
* @param mem 内存大小
*/
case class WorkerHeartBeatMessage(workerId:String, cpu:Int, mem:Int)
WorkerActor.scala
object WorkerActor extends Actor{
...
override def receive: Receive = {
case RegisterSuccessMessage => {
println("2. 成功注册到Master")
import scala.concurrent.duration._
import context.dispatcher
context.system.scheduler.schedule(0 seconds,
ConfigUtil.`worker.heartbeat.interval` seconds){
// 发送心跳消息
masterActor ! WorkerHeartBeatMessage(workerId, cpu, mem)
}
}
}
}
MasterActor.scala
object MasterActor extends Actor{
...
override def receive: Receive = {
...
case WorkerHeartBeatMessage(workerId, cpu, mem) => {
println("3. 接收到心跳消息, 更新最后心跳时间")
regWorkerMap += workerId -> WorkerInfo(workerId, cpu, mem, new Date().getTime)
}
}
}
如果某个worker超过一段时间没有发送心跳,Master需要将该worker从当前的Worker集合中移除。可以通过Akka的定时任务,来实现心跳超时检查。
步骤
参考代码
ConfigUtil.scala
object ConfigUtil {
private val config: Config = ConfigFactory.load()
// 心跳检查时间间隔
val `master.heartbeat.check.interval` = config.getInt("master.heartbeat.check.interval")
// 心跳超时时间
val `master.heartbeat.check.timeout` = config.getInt("master.heartbeat.check.timeout")
}
MasterActor.scala
override def preStart(): Unit = {
import scala.concurrent.duration._
import context.dispatcher
context.system.scheduler.schedule(0 seconds,
ConfigUtil.`master.heartbeat.check.interval` seconds) {
// 过滤出来超时的worker
val timeoutWorkerList = regWorkerMap.filter {
kv =>
if (new Date().getTime - kv._2.lastHeartBeatTime > ConfigUtil.`master.heartbeat.check.timeout` * 1000) {
true
}
else {
false
}
}
if (!timeoutWorkerList.isEmpty) {
regWorkerMap --= timeoutWorkerList.map(_._1)
println("移除超时的worker:")
timeoutWorkerList.map(_._2).foreach {
println(_)
}
}
if (!regWorkerMap.isEmpty) {
val sortedWorkerList = regWorkerMap.map(_._2).toList.sortBy(_.mem).reverse
println("可用的Worker列表:")
sortedWorkerList.foreach {
var rank = 1
workerInfo =>
println(s"<${rank}> ${workerInfo.workerId}/${workerInfo.mem}/${workerInfo.cpu}")
rank = rank + 1
}
}
}
}
...
}
修改配置文件,启动多个worker进行测试。
步骤