文章目录
Akka并发编程框架简介
Akka介绍
Akka是一个用于构建高并发、分布式和可扩展的基于事件驱动的应用的工具包。Akka是使用scala开发的库,同时可以使用scala和Java语言来开发基于Akka的应用程序。
Akka特性
- 提供基于异步非阻塞、高性能的事件驱动编程模型
- 内置容错机制,允许Actor在出错时进行恢复或者重置操作
- 超级轻量级的事件处理(每GB堆内存几百万Actor)
- 使用Akka可以在单机上构建高并发程序,也可以在网络中构建分布式程序。
Akka通信过程
以下图片说明了Akka Actor的并发编程模型的基本流程:
- 学生创建一个ActorSystem
- 通过ActorSystem来创建一个ActorRef(老师的引用),并将消息发送给ActorRef
- ActorRef将消息发送给Message Dispatcher(消息分发器)
- Message Dispatcher将消息按照顺序保存到目标Actor的MailBox中
- Message Dispatcher将MailBox放到一个线程中
- MailBox按照顺序取出消息,最终将它递给TeacherActor接受的方法中
创建Actor
Akka中,也是基于Actor来进行编程的。类似于之前学习过的Actor。但是Akka的Actor的编写、创建方法和之前有一些不一样。
API介绍
ActorSystem
在Akka中,ActorSystem是一个重量级的结构,它需要分配多个线程,所以在实际应用中,ActorSystem通常是一个单例对象,可以使用这个ActorSystem创建很多Actor。它负责创建和监督actor
Actor中获取ActorSystem
直接使用context.system就可以获取到管理该Actor的ActorSystem的引用
实现Actor类
- 继承Actor(注意:要导入akka.actor包下的Actor)
- 实现receive方法,receive方法中直接处理消息即可,不需要添加loop和react方法调用。Akka会自动调用receive来接收消息
- 【可选】还可以实现preStart()方法,该方法在Actor对象构建后执行,在Actor声明周期中仅执行一次
加载Akka Actor
- 要创建Akka的Actor,必须要先获取创建一个ActorSystem。需要给ActorSystem指定一个名称,并可以去加载一些配置项(后面会使用到)
- 调用ActorSystem.actorOf(Props(Actor对象), “Actor名字”)来加载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之间可以互相发送消息。
实现步骤
- 创建Maven模块
- 创建并加载Actor
- 发送/接收消息
<properties>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
<encoding>UTF-8</encoding>
<scala.version>2.11.12</scala.version>
<scala.compat.version>2.11</scala.compat.version>
</properties>
<dependencies>
<dependency>
<groupId>org.scala-lang</groupId>
<artifactId>scala-library</artifactId>
<version>${scala.version}</version>
</dependency>
<dependency>
<groupId>com.typesafe.akka</groupId>
<artifactId>akka-actor_2.11</artifactId>
<version>2.3.14</version>
</dependency>
<dependency>
<groupId>com.typesafe.akka</groupId>
<artifactId>akka-remote_2.11</artifactId>
<version>2.3.14</version>
</dependency>
</dependencies>
<build>
<sourceDirectory>src/main/scala</sourceDirectory>
<testSourceDirectory>src/test/scala</testSourceDirectory>
<plugins>
<plugin>
<groupId>net.alchim31.maven</groupId>
<artifactId>scala-maven-plugin</artifactId>
<version>3.2.2</version>
<executions>
<execution>
<goals>
<goal>compile</goal>
<goal>testCompile</goal>
</goals>
<configuration>
<args>
<arg>-dependencyfile</arg>
<arg>${project.build.directory}/.scala_dependencies</arg>
</args>
</configuration>
</execution>
</executions>
</plugin>
<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>
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<mainClass></mainClass>
</transformer>
</transformers>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
1. 创建Maven模块
使用Akka需要导入Akka库,我们这里使用Maven来管理项目
- 创建Maven模块
- 打开pom.xml文件,导入akka Maven依赖和插件
2. 创建并加载Actor
创建两个Actor
- SenderActor:用来发送消息
- ReceiveActor:用来接收,回复消息
创建Actor
- 创建ActorSystem
- 创建自定义Actor
- ActorSystem加载Actor
3. 发送/接收消息
- 使用样例类封装消息
- SubmitTaskMessage——提交任务消息
- SuccessSubmitTaskMessage——任务提交成功消息
- 使用类似于之前学习的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框架定时的执行一些任务,该如何处理呢?
使用方式
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) // 隐式参数:需要手动导入
示例一
示例说明
- 定义一个Actor,每1秒发送一个消息给Actor,Actor收到后打印消息
- 使用发送消息方式实现
参考代码
// 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")
}
示例二
示例说明
- 定义一个Actor,每1秒发送一个消息给Actor,Actor收到后打印消息
- 使用自定义方式实现
参考代码
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消息。
1. Worker实现
步骤
- 创建一个Maven模块,导入依赖和配置文件
- 创建启动WorkerActor
- 发送"setup"消息给WorkerActor,WorkerActor接收打印消息
- 启动测试
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")
}
}
2. Master实现
步骤
- 创建Maven模块,导入依赖和配置文件
- 创建启动MasterActor
- WorkerActor发送"connect"消息给MasterActor
- MasterActor回复"success"消息给WorkerActor
- WorkerActor接收并打印接收到的消息
- 启动Master、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://MasterActorSystem@127.0.0.1:9999/user/MasterActor")
// 发送connect
masterActor ! "connect"
case "success" =>
println("3. 连接Master成功...")
}
}
简易版spark通信框架案例
案例介绍
模拟Spark的Master与Worker通信
- 一个Master
- 管理Worker
- 若干个Worker(Worker可以按需添加)
- 注册
- 发送心跳
实现思路
- 构建Master、Worker阶段
- 构建Master ActorSystem、Actor
- 构建Worker ActorSystem、Actor
- Worker注册阶段
- Worker进程向Master注册(将自己的ID、CPU核数、内存大小(M)发送给Master)
- Worker定时发送心跳阶段
- Worker定期向Master发送心跳消息
- Master定时心跳检测阶段
- Master定期检查Worker心跳,将一些超时的Worker移除,并对Worker按照内存进行倒序排序
- 多个Worker测试阶段
- 启动多个Worker,查看是否能够注册成功,并停止某个Worker查看是否能够正确移除
1. 工程搭建
项目使用Maven搭建工程
步骤
- 分别搭建几下几个项目
工程名 | 说明 |
---|---|
spark-demo-common | 存放公共的消息、实体类 |
spark-demo-master | Akka Master节点 |
spark-demo-worker | Akka Worker节点 |
- 导入依赖(资料包中的pom.xml)
- master/worker添加common依赖
- 导入配置文件(资料包中的application.conf)
- 修改Master的端口为7000
- 修改Worker的端口为7100
2. 构建Master和Worker
分别构建Master和Worker,并启动测试
步骤
- 创建并加载Master Actor
- 创建并加载Worker Actor
- 测试是否能够启动成功
参考代码
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)
}
}
3. Worker注册阶段实现
在Worker启动时,发送注册消息给Master
步骤
- Worker向Master发送注册消息(workerid、cpu核数、内存大小)
- 随机生成CPU核(1、2、3、4、6、8)
- 随机生成内存大小(512、1024、2048、4096)(单位M)
- Master保存Worker信息,并给Worker回复注册成功消息
- 启动测试
参考代码
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://sparkMaster@127.0.0.1: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)
}
...
}
4. Worker定时发送心跳阶段
Worker接收到Master返回注册成功后,发送心跳消息。而Master收到Worker发送的心跳消息后,需要更新对应Worker的最后心跳时间。
步骤
- 编写工具类读取心跳发送时间间隔
- 创建心跳消息
- Worker接收到注册成功后,定时发送心跳消息
- Master收到心跳消息,更新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)
}
}
}
5. Master定时心跳检测阶段
如果某个worker超过一段时间没有发送心跳,Master需要将该worker从当前的Worker集合中移除。可以通过Akka的定时任务,来实现心跳超时检查。
步骤
- 编写工具类,读取检查心跳间隔时间间隔、超时时间
- 定时检查心跳,过滤出来大于超时时间的Worker
- 移除超时的Worker
- 对现有Worker按照内存进行降序排序,打印可用Worker
参考代码
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
}
}
}
}
...
}
6. 多个Worker测试阶段
修改配置文件,启动多个worker进行测试。
步骤
- 测试启动新的Worker是否能够注册成功
- 停止Worker,测试是否能够从现有列表删除