刚接触akka不久,之前看spark源码的时候,发现spark的master 跟 worker之间消息传递等都是用的akka。所以决定先了解下akka再说。作为typesafe公司下的三个产品(play! ,scala,akka)之一,akka的强大不必另说。使用“let it crash”模型以及强大的抽象actor,akka是我们开发高容错性以及并发行好助手。
这个小例子是利用akka进行简单的word count。刚刚写好,有意见可以交流。此为前言。
我会一边展示程序一边讲解akka使用。
一:主程序
def calcultate(): Unit ={ //create system actor val system = ActorSystem("WordCountwithakka") //create listener val listener = system.actorOf(Props[Listener],name = "WCListener") //create master val master= system.actorOf(Props(new Master(listener,4)),name="WCMaster") println("master inited") //begin to calculate master ! Map }
在主程序中定义ActorSystem,首先,什么是actor?
Actor是封装状态和行为的对象,他们的唯一通讯方式是交换消息,交换的消息存放在接收方的邮箱里。从某种意义上来说,actor是面向对象的最严格的形式,在使用actor来对解决方案建模时,可以把actor想象成一群人,把子任务分配给他们,将他们的功能整理成一个有组织的结构。每个actor监督他所生成的子actor。
然后我们分别定义了一个listener以及一个master。
Listener负责返回输出结果以及关闭最后的系统。
class Listener extends Actor{ override def receive= { case FinalRes(du,res) =>{ println("\n\tCalculation time: \t%s".format(du)) println("\n\tRes: \t%s".format(res)) context.system.shutdown() } } }
master ! Map : 代表我们向master发送了map消息。master在接受到消息后则开始工作。
二:Master
Master总体来说负责分配任务,收集各个worker的工作结果并组装最后结果。
class Master(listener: ActorRef,numWorker : Int=4) extends Actor{ //original data val data = "hello world hello this is my first akka app plz use scala Cause scala is really smart " + "I have used it so far in two real projects very successfully. both are in the near real-time traffic " + "information field (traffic as in cars on highways), distributed over several nodes, integrating messages" val dataArray = data.split(" ") val sumSize = dataArray.size val jobPerWork = sumSize/numWorker val workerRouter = context.actorOf(Props[Worker].withRouter(RoundRobinRouter(numWorker)), name = "workerRouter") var totalRes = new mutable.HashMap[String,Int] var receivedMsg = 0 val start: Long = System.currentTimeMillis def receive = { case Map =>{ println("begin to map") for(i <- 1 to numWorker){ var endIndex = 0 if (i ==numWorker){ endIndex = sumSize-1 }else{ endIndex = i*jobPerWork } val sendData = dataArray.slice((i-1)*jobPerWork,endIndex) println("send worker "+i+" to work") workerRouter ! Task(sendData) } } case Reduce(partData) =>{ println("received one res") totalRes = mergeRes(totalRes,partData) receivedMsg = receivedMsg + 1 if (receivedMsg == numWorker){ println("all job finished") listener ! FinalRes(du = (System.currentTimeMillis - start).millis,totalRes.toList.toString()) // Stops this actor and all its supervised children context.stop(self) } } }
这里首先生成我们需要计算的word的data,然后根据worker的数目,计算每个worker需要处理的单词数目。
在分发任务的时候,使用worker router进行分发,分发的算法是轮训算法,每个节点都轮流进行计算。
Receive 方法接受到的消息有两种情况:
Map:为接受到主函数的消息,接受到后则进行消息分发。
Reducer:接受到worker的计算结果后,进行合并。然后通知listener并关闭自己。
三:Worker
worker是负责统计工作的,在本例中则是计算分配的数据中每个单词出现的次数,并返回给worker。
class Worker() extends Actor{ def wordCount(data:Array[String]):HashMap[String,Int]={ val count = new mutable.HashMap[String,Int]() data.foreach(word =>{ val numExisted = count.getOrElse(word,0) val newNum = numExisted + 1 count.put(word,newNum) }) count } override def receive= { case Task(splitedData) =>{ println("one to work") sender ! Reduce(wordCount(splitedData)) } } }
其中,receive代表接受到Task任务后,所做的动作。
sender表示task任务的发送方(也就是master)。
计算完毕后,给sender回复计算结果。
四:消息定义
worker,master之间都是通过消息互相通讯。
//all messages sealed trait wcMessage case class FinalRes(du:Duration,res:String=null) extends wcMessage case object Map extends wcMessage case class Task(splitedData:Array[String]) extends wcMessage case class Reduce(partData:mutable.HashMap[String,Int]) extends wcMessage
Tips: 在scala当中,使用sealed trait wcMessage ,则在case当中,只能匹配extends wcMessage的类或者object的实例,否则编译会出错。
这样可以保证排除来历不明的消息。
另外,源码下载在 https://github.com/YulinGUO/akkaSimpleWordCount