我们先设计两个Actor,一个是Master 另一个Worker
Worker :
- 必须像master发送心跳信息,所以我要早Worker中获取Master的引用
- 第一次通讯的时候我应该先到Master注册自己的信息,方便Master来调度
- 定时向master发送心跳信息,防止Worker节点崩溃,Master却没有感知到的情况
Master - 接收Worker发送的信息存到一个Map里面
- 检测客户端的状态,将异常的节点从Map里面移除
- 接收客户端的心跳信息,加入接收到心跳的时间,这里Actor是一个单线程,所以就没有必要加锁,向worker发送注册成功的信息
现在两端的工作清楚了,就可以进入到编程阶段了,我为了方便进行通讯,我们可以使用类而不是利用一段字符串来进行,所以我先声明在通讯中所需要的样例类
package com.ly.scala.Test5_Spard
/**
* work-->master
*/
//worker 向master注册自己
case class RegisterWorkerInfo(id :String,core :Int,ram: Int)
//worker给master发送心跳信息
case class HearBeat(id:String) //发送服务器的 用 serverActorRef -》服务器地址 ip,端口 ,服务名
//worker 发送给自己的消息 告诉自己说要开始周期性向 master发送心跳信息
case object SendHeartBeat //发送给自己 所以要用自己的 actorRef
/**
* master -> worker
*/
// master 向 worker发送注册成功消息
case object RegisterWorkerInfo
//master 发送给自己发送一个检查超时worker的信息 并启动一个调度器 scheduler 周期检查删除超时worker
case object CheckTimeOutWorker
//master 发送给自己的消息 删除超时的worker
case object RemoveTimeOutWorker
//存储 worker信息的类
class WorkerInfo(val id:String,core :Int,ram :Int){
var lastHeartBeatTime:Long=_ //上一次 心跳记录的时间
override def toString: String = id +"\tcpu:"+core+",mem:"+ram
}
开始写Master层 各个代码我都做类比较详细的解释,可以直接阅读代码
package com.ly.scala.Test5_Spard
import akka.actor.{Actor, ActorSystem, Props}
import com.typesafe.config.ConfigFactory
import scala.concurrent.duration._
class SparkMaster extends Actor{
//存储 worker注册过来的信息
val id2WorkerInfo=collection.mutable.HashMap[String,WorkerInfo]()
//接收另一个actor的信息
override def receive: Receive = {
//收到 worker注册过来的信息 提取 这里是上面步骤的第4步,注意样例类
case RegisterWorkerInfo(wkId,core,ram)=>{
//将 worker的信息存储起来 一起存储到HashMap
if (!id2WorkerInfo.contains(wkId)){
//声明一个工作节点的信息,就是存储Worker工作节点的信息,这就是Actor的好处,两个Actor是单线程操作,所以不必担心出现线程争抢的问题
val workerInfo=new WorkerInfo(wkId,core,ram)
id2WorkerInfo +=((wkId,workerInfo))//将 worker发送过的消息存起来
//master存储完 worker注册数据之后 Master告诉worker说你已经注册成功
// sender() 当前接收的这条数据的发送者 Worker的引用 因为Actor在发送消息的时候会将自己的引用也传过去
sender() ! RegisterWorkerInfo //此时worker会收到注册成功消息
}
}
//接收到心跳信息 这是第6步
case HearBeat(wkId)=>{
//master 收到worker的心跳信息之后 更新worker的上一次心跳信息
val workerInfo=id2WorkerInfo(wkId)
//更改心跳时间
val currentTime=System.currentTimeMillis()
workerInfo.lastHeartBeatTime=currentTime
}
//检查客户端状态 CheckTimeOutWorker 第5步 采用轮询的策略检测异常的工作节点
case CheckTimeOutWorker=>{
import context.dispatcher //使用调度器的时候必须要导入 dispather
//这个调度器的作用是每隔6秒中清楚一次出现异常的节点
context.system.scheduler.schedule(0 millis,6000 millis,self,RemoveTimeOutWorker)
}
//检测超时的节点 承接第5步
case RemoveTimeOutWorker=>{
//将 hashMap入所有的value都拿出开 查看当前 时间和上一次心跳时间的差是 3000 这是work节点向master发送信息的时间间隔
val workerInfo=id2WorkerInfo.values
val currenttime=System.currentTimeMillis()
//过滤掉超时的节点
workerInfo.filter(wkInfo=>currenttime-wkInfo.lastHeartBeatTime>3000).foreach(wk=>id2WorkerInfo.remove(wk.id))
println(s"=====还剩${id2WorkerInfo.size} 存活的worker======")
workerInfo.foreach(println)
println("============================")
}
}
}
object SparkMaster {
private var name=""
private val age=100
def main(args: Array[String]): Unit = {
var host="localhost"
var port ="10000"
var masterName="master1"
//这里是在命令行传参
if(args.length==3){
host=args(0)
port=args(1)
masterName=args(2)
}
val config=ConfigFactory.parseString(
s"""
|akka.actor.provider="akka.remote.RemoteActorRefProvider"
|akka.remote.netty.tcp.hostname=$host
|akka.remote.netty.tcp.port=$port
|""".stripMargin
)
//指定 ip端口
val actorSystem=ActorSystem("sparkMaster",config)
//注意 这里创建了一个名字为 robot_server 的服务器 Actor发的引用 这里是可以创建多个的 名字不同的接口
val masterActorRef=actorSystem.actorOf(Props[SparkMaster],masterName)
//发送消息给自己 开始检监控work节点是否注册
masterActorRef ! CheckTimeOutWorker
}
}
==worker=:注意,worker端第一次发起通讯也就是建立连接需要之名是与那个Actor的连接,所以要有Master的url 详情请看代码
package com.ly.scala.Test5_Spard
import java.util.UUID
import akka.actor.{Actor, ActorSelection, ActorSystem, Props}
import com.ly.scala.Test4_Spart.SparkWorker
import com.typesafe.config.ConfigFactory
import scala.concurrent.duration._
class SparkWorker(masterUrl:String) extends Actor{
//这里很关键 声明Master的引用对象 也就是获取Master的代理 这样的话我可以发送第一条信息 这里对应步骤一
var masterProxy:ActorSelection= _
val workId=UUID.randomUUID().toString //客户端的id
//生命周期函数 这个函数在最前面调用
override def preStart(): Unit = {
//根据masterUrl获取到master的代理
masterProxy=context.actorSelection(masterUrl)
}
override def receive: Receive = {
//worker开始工作
case "started" => {
//自己已就绪
//向master注册自己的信息 id,core,ram
//TODO:获取自己的cpu和内存
println("客户端"+workId+"启动.....")
//这里步骤二 开始建立与Master的连接了
masterProxy ! RegisterWorkerInfo(workId,4,32*1024)//此时向 master发送注册信息
}
//步骤三,当Master接收注册成功返回给Worker的注册成功
case RegisterWorkerInfo=>{//master 通过响应发送给自己注册成功信息 ,接收、这个信息之后则定时发送心跳信息给服务器
//worker 启动一个定时器 定时向 master发送心跳
import context.dispatcher //因为 schedule中需要参数 implicit executor
context.system.scheduler.schedule(0 millis,1500 millis,self,SendHeartBeat)
}
//开始发送心跳信息
case SendHeartBeat =>{
//开始向 master发送心跳了
println(s"==========$workId 发送心跳===========")
//向Master发送心跳信息
masterProxy ! HearBeat(workId)//此时master就会接收到信息
}
}
}
object SparkWorker{
def main(args: Array[String]): Unit = {
var host="localhost"
var port ="10002"
//工作节点的名字
var workName="spark_worker"
//master节点的地址
var masterURL="akka.tcp://sparkMaster@localhost:10000/user/master1"//获取Master代理的地址
//检验命令行参数
if(args.length==4) {
host = args(0)
port = args(1)
workName = args(2)
masterURL = args(3)
}
val config=ConfigFactory.parseString(
s"""
|akka.actor.provider="akka.remote.RemoteActorRefProvider"
|akka.remote.netty.tcp.hostname=$host
|akka.remote.netty.tcp.port=$port
|""".stripMargin
)
//指定 ip端口
val actorSystem: ActorSystem =ActorSystem("sparkWorker",config)
//注意 这里创建了一个名字为 robot_server 的服务器 Actor发的引用 这里是可以创建多个的 名字不同的接口
val workerActorRef=actorSystem.actorOf(Props(new SparkWorker(masterURL)),workName)
//worker发送给自己的一条信息 ,告诉自己开始建立与Master的连接
workerActorRef ! "started"
}
}