Akka基础
- 参照: http://www.importnew.com/16479.html
- Akka笔记之Actor简介
Akka中的Actor遵循Actor模型。你可以把Actor当作是人。这些人不会亲自去和别人交谈。他们只通过邮件来交流。
1. 消息传递 2. 并发 3. 异常处理 4. 多任务 5. 消息链
消息发送给actor代理;
消息是不可变对象(可带有属性的case class);
分发器dispatcher和邮箱: dispatcher从actorRef取出一条消息放在目标actor邮箱中,然后放mailbox放在一个Thread上;当MailBox的run方法运行的时候,它会从队列中取出一条消息, 然后将它传给Actor去处理。在Actor的世界中,邮箱一有机会就会要求Actor去完成自己的任务。
使用slf4j打印日志:Akka通过一个叫做ActorLogging的特质(trait)来实现的这一功能。可以这个trait混入(mixin)到类中。当我们要打印一条消息的时候,ActorLogging中的日志方法会将日志信息发布到一个EventStream流中。没错,我的确说的是发布。
EventStream:EventStream就像是一个我们用来发布及接收消息的消息代理。它与常见的消息中间件的根本区别在于EventStream的订阅者只能是一个Actor。DefaultLogger默认订阅这些消息并打印到标准输出。
akka{
loggers = ["akka.event.slf4j.Slf4jLogger"]
loglevel = "DEBUG"
logging-filter = "akka.event.slf4j.Slf4jLoggingFilter"
}
技术上来讲,消息发送给Actor就是希望能有副作用的。设计上便是如此。目标Actor可以不做响应,也可以做如下两件事情——
1. 给发送方回复一条响应(在本例中,TeacherActor会将一句名言回复给StudentActor)
2. 将响应转发给其它的目标受众Actor,后者也可以进行响应/转发/产生副作用。Router和Supervisor就是这种情况。
配置管理:applicaiton.conf
调度器: 一次调度和循环调度。import context.dispatcher这条语句非常重要。schedule方法需要一个非常重要的隐式参数——ExecutionContext。schedule方法只是把消息发送封装到了一个Runnable中,而它最终是由传进来的ExecutionContext来执行的。
preStart: Actor重启的时候(比如说崩溃了之后重启)通过调用preStart方法还能重新初始化。而构造方法则实现不了这点(只会初始化一次)。
postStop: ActorSystem.stop(), ActorContext.stop(), PoisonPill 都可以终止一个actor,关闭时回调用postStop()
Actor是纯粹的分层结构。你所创建出来的Actor必定是某个Actor的子Actor。actorRef.path可以获取到actor路径。
子Actor:当某个任务由一个或多个子任务所组成的时候通常就会创建子Actor。或者当某个任务由父Actor执行比较容易出错,而你希望将它进行隔离的时候,也可以使用子Actor(这样当子Actor崩溃的时候,你还能够恢复它)。如果不存在父子Actor关系,就不要创建子Actor。
监控(Watch):不管Actor是怎么挂掉的,系统里面会有些Actor希望能够知晓这一情况。ActorContext.watch和ActorContext.unwatch就是监控与取消监控的方法了。进行了监控之后,监控者会收到已停止的Actor发来的一条Terminated消息,它们只需要把这个消息放到receive函数的处理逻辑里就好了。
监督(Supervision):只存在于父子关系的actor之间。
Actor模型
Actor模型的要点
- 基于Actor的系统中 Actor是最小的抽象单元, 就像object 之于 oop
- 一个Actor封装了状态和行为
- 外界不能进入Actor以获取其状态、字段、执行方法,和Actor的交互只能通过message
- 一个Actor有一个信箱mailbox,将外部发送来的消息msg存到队列中
- Actor的终生就是在等待msg,并依次取出mailbox中的消息进行处理
Actor模型的组织观
- 将Actor系统视为一个公司,从上到下有严格的层级关系,公司中员工是person,也是actor
- 一个Actor仅有一个上级主管 称为 supervisor,就是create这个Actor的 那个Actor
- 一个Actor可能有多个下属小兵,真正干着脏累差活的actor
- 一个Actor可能有多个同级的兄弟部门actor
开发actor系统的关键
- 委托 委托 委托!!! 以做的更多!
- 公司老总不能事无巨细全部承担,他的主要工作就是分配公司工作和监督
- 各部门主管收到任务,要么自己干,要么再细分任务,然后分配到自己的下属手中并监督
- 如果公司还有更多的层级,则继续上面这个主管的龌龊勾当
- 最底层的员工收到细粒度任务,干着自己最擅长的事情
Actor的失效处理
- 人无完人,Actor也不可能100%完成任务
- 如果任务执行失败,一个Actor会挂起自己及其下属,然后发消息告诉其上级主管“我失败了”!
- 上级主管收到下属的“失败”消息时,可以有如下反应:
- 失败就失败,就这样吧,没太大关系:保持当前状态,恢复Actor,继续工作
- 没成功啊,那重新做一遍吧 :重置状态,重启该Actor
- 没机会了,失败了你就滚!解雇下属:关闭 终结 该 Actor
- 这事我也决定不了,我请示我的上级:向上级Actor报告
Akka实现的Actor模型的一些附加特性
- 实例化一个Actor时,返回一个ActorRef,相当于 邮箱地址,并不能通过这个获取Actor的状态信息
- Actor模型是线程的更高层抽象,最终是跑在java的线程中的
- 多个Actor可能共享一个线程,这是由akka保障的
- Actor的信箱有多种实现方式:无限变量信箱 有限信箱 带优先级信箱, 还可以自定义实现
- akka没有让Actor扫描信箱的message
- 一个Actor终结(无论是正常还是非常的)它的信箱中的msg进入akka系统的“死信箱dead letter mailbox”中
Akka framework现在已经是Scala语言的一部分了,用它编写分布式程序是相当简单的,本文将一步一步地讲解如何做到scale up & scale out。
先从一个简单的单线程程序PerfectNumber.scala开始,这个程序是找出2到100范围内所有的“完美数”(真约数之和恰好等于此数自身)
- package com.newegg.demo
-
- import scala.concurrent.duration._
- import scala.collection.mutable.ListBuffer
-
- object PerfectNumber {
- def sumOfFactors(number: Int) = {
- (1 /: (2 until number)) { (sum, i) => if (number % i == 0) sum + i else sum }
- }
-
- def isPerfect(num: Int): Boolean = {
- num == sumOfFactors(num)
- }
-
- def findPerfectNumbers(start: Int, end: Int) = {
- require(start > 1 && end >= start)
- val perfectNumbers = new ListBuffer[Int]
- (start to end).foreach(num => if (isPerfect(num)) perfectNumbers += num)
- perfectNumbers.toList
- }
-
- def main(args: Array[String]): Unit = {
- val list = findPerfectNumbers(2, 100)
- println("\nFound Perfect Numbers:" + list.mkString(","))
- }
- }
Scala编写并发程序的基础是Actor模型,与Actor交互的唯一途径是“消息传递”,你根本不用考虑“进程”,“线程”,“同步”,“锁”等等一些冷冰冰的概念,你可以把Actor看做是一个“人”,你的程序是一个“组织”内的一群“人”之间以“消息传递”的方式在协作。
这个示例中要用到的“消息”定义在Data.scala文件中,内容如下:
- package com.newegg.demo
-
- import akka.actor.ActorRef
-
- sealed trait Message
- case class StartFind(start: Int, end: Int, replyTo: ActorRef) extends Message
- case class Work(num: Int, replyTo: ActorRef) extends Message
- case class Result(num: Int, isPerfect: Boolean) extends Message
- case class PerfectNumbers(list: List[Int]) extends Message
用面向对象的方式把程序改造一下,把PerfectNumber.scala其中的部分代码抽取到一个单独的Worker.scala文件中:
- package com.newegg.demo
-
- import akka.actor.Actor
- import akka.actor.ActorRef
- class Worker extends Actor {
- private def sumOfFactors(number: Int) = {
- (1 /: (2 until number)) { (sum, i) => if (number % i == 0) sum + i else sum }
- }
-
- private def isPerfect(num: Int): Boolean = {
- num == sumOfFactors(num)
- }
-
- def receive = {
- case Work(num: Int, replyTo: ActorRef) =>
- replyTo ! Result(num, isPerfect(num))
- print("[" + num + "] ")
- }
- }
一部分代码抽取到Master.scala文件中:
- package com.newegg.demo
-
- import scala.collection.mutable.ListBuffer
- import akka.actor.Actor
- import akka.actor.ActorRef
- import akka.actor.Props
- import akka.routing.FromConfig
- import akka.routing.ConsistentHashingRouter.ConsistentHashableEnvelope
-
- sealed class Helper(count: Int, replyTo: ActorRef) extends Actor {
- val perfectNumbers = new ListBuffer[Int]
- var nrOfResult = 0
-
- def receive = {
- case Result(num: Int, isPerfect: Boolean) =>
- nrOfResult += 1
- if (isPerfect)
- perfectNumbers += num
- if (nrOfResult == count)
- replyTo ! PerfectNumbers(perfectNumbers.toList)
- }
- }
-
- class Master extends Actor {
- val worker = context.actorOf(Props[Worker].withRouter(FromConfig()), "workerRouter")
-
- def receive = {
- case StartFind(start: Int, end: Int, replyTo: ActorRef) if (start > 1 && end >= start) =>
- val count = end - start + 1
- val helper = context.actorOf(Props(new Helper(count, replyTo)))
- (start to end).foreach(num => worker ! Work(num, helper))
- }
- }
这里用到了一个“可变”的变量nrOfResult,有时候,要完全不用“可变”的变量是相当难以做到的,只要将“可变”的副作用很好地进行“隔离”还是可以的。Scala语言既提倡使用“不变”变量,也容忍使用“可变”变量,既提倡“函数式”编程风格,也兼容面向对象编程,它并不强迫你一开始就完全放弃你所熟悉的编程习惯,我很喜欢这种比较中庸的语言。
那个单线程程序的主函数改造如下:
- package com.newegg.demo
-
- import scala.concurrent.duration._
- import scala.collection.mutable.ListBuffer
- import akka.actor.ActorSystem
- import akka.actor.Props
- import akka.actor.Actor
- import com.typesafe.config.ConfigFactory
- import akka.routing.FromConfig
-
- object PerfectNumber {
-
- def main(args: Array[String]): Unit = {
- val system = ActorSystem("MasterApp", ConfigFactory.load.getConfig("multiThread"))
- system.actorOf(Props(new Actor() {
- val master = context.system.actorOf(Props[Master], "master")
- master ! StartFind(2, 100, self)
- def receive = {
- case PerfectNumbers(list: List[Int]) =>
- println("\nFound Perfect Numbers:" + list.mkString(","))
- system.shutdown()
- }
- }))
- }
- }
程序中用到的配置application.conf文件中的内容如下:
- multiThread{
- akka.actor.deployment./master/workerRouter{
- router="round-robin"
- nr-of-instances=10
- }
- }
这样,单线程程序就完全改造成了一个可以充分利用计算机上所有的CPU核的多线程程序,根据计算机的硬件能力只需调整nr-of-instances配置参数就可以调整并发的能力。
现在,我们进一步改造,把它变成一个可以跨JVM,或者说跨计算机运行的分布式程序。
新建一个MasterApp.scala文件:
- package com.newegg.demo
-
- import com.typesafe.config.ConfigFactory
- import akka.actor.Actor
- import akka.actor.ActorRef
- import akka.actor.ActorSelection.toScala
- import akka.actor.ActorSystem
- import akka.actor.Props
- import akka.kernel.Bootable
- import akka.cluster.Cluster
-
- class Agent extends Actor {
- var master = context.system.actorSelection("/user/master")
-
- def receive = {
- case StartFind(start: Int, end: Int, replyTo: ActorRef) if (start > 1 && end >= start) =>
- master ! StartFind(start, end, sender)
- }
- }
-
- class MasterDaemon extends Bootable {
- val system = ActorSystem("MasterApp", ConfigFactory.load.getConfig("remote"))
- val master = system.actorOf(Props[Master], "master")
-
- def startup = {}
- def shutdown = {
- system.shutdown()
- }
- }
-
- object MasterApp {
- def main(args: Array[String]) {
- new MasterDaemon()
- }
- }
application.conf文件中加入一个remote的section配置块:
- akka {
- actor {
- provider = "akka.remote.RemoteActorRefProvider"
- deployment{
- /remoteMaster{
- router="round-robin"
- nr-of-instances=10
- target{
- nodes=[
- "akka.tcp://MasterApp@127.0.0.1:2551",
- "akka.tcp://MasterApp@127.0.0.1:2552"
- ]
- }
- }
- /master/workerRouter{
- router="round-robin"
- nr-of-instances=10
- }
- }
- }
-
- remote {
- transport = "akka.remote.netty.NettyRemoteTransport"
- netty.tcp {
- hostname = "127.0.0.1"
- port = 2551
- }
- }
- }
在Terminal中运行命令java -cp ".:../../lib/*" com.newegg.demo.MasterApp,可以看一个守护程序正在监听2551端口,修改上述配置端口为2552,在另一个Terminal中运行同样的命令,另一个守护程序正在监听2552端口。
修改PerfectNumber.scala中的main函数为:
- def main(args: Array[String]): Unit = {
- val system = ActorSystem("MasterApp", ConfigFactory.load.getConfig("remote"))
- system.actorOf(Props(new Actor() {
- val agent = context.system.actorOf(Props(new Agent()).withRouter(FromConfig()), "remoteMaster")
- dispatch
-
- private def dispatch = {
- val remotePaths = context.system.settings.config.getList("akka.actor.deployment./remoteMaster.target.nodes")
- val count = end - start + 1
- val piece = Math.round(count.toDouble / remotePaths.size()).toInt
- println("%s pieces per node".format(piece))
- var s = start
- while (end >= s) {
- var e = s + piece - 1
- if (e > end)
- e = end
- agent ! StartFind(s, e, self)
- s = e + 1
- }
- println(agent.path)
- }
-
- def receive = {
- case PerfectNumbers(list: List[Int]) =>
- println("\nFound Perfect Numbers:" + list.mkString(","))
- system.shutdown()
- }
- }))
- }
修改配置端口为2553,在Terminal中运行命令java -cp ".:../../lib/*" com.newegg.demo.PerfectNumber,可以看到两个守护程序中均有Worker在工作,总的计算任务得到了分担。
这种分布式程序实现起来简单,但是有个缺点:参与分担任务的守护程序的地址必须全部在target.nodes配置中列出,且一旦其中有守护程序宕掉,整体将不能正确地对外服务。
因此,我们需要有一个有更具扩展能力和高容错能力的集群式应用。
在application.conf文件中新加一个cluser配置区块:
- akka {
- actor {
- provider = "akka.cluster.ClusterActorRefProvider"
- deployment {
- /master/workerRouter {
- router = "consistent-hashing"
- nr-of-instances = 10
- cluster {
- enabled = on
- max-nr-of-instances-per-node = 3
- allow-local-routees = on
- }
- }
- }
- }
- remote {
- log-remote-lifecycle-events = off
- netty.tcp {
- hostname = "127.0.0.1"
- port = 2551
- }
- }
- cluster {
- min-nr-of-members = 2
- seed-nodes = [
- "akka.tcp://MasterApp@127.0.0.1:2551",
- "akka.tcp://MasterApp@127.0.0.1:2552"]
-
- auto-down=on
- }
- }
注意其中有两个seed-nodes,是集群的“首脑”,某节点会加入的是哪个集群,正是因为参照这个seed-nodes来的。
这里采用的是consistent-hashing Router,集群节点之间有心跳检测,集群实现中内部采用的是与Cassandra一样的Gossip协议,用一致性哈希来维护集群节点“环”。
改造Master.scala文件中其中一行代码,将
- worker ! Work(num, helper)
改为:
- worker.tell(ConsistentHashableEnvelope(Work(num,helper), num), helper)
这样,发送到集群中的消息支持一致性哈希,在整个集群节点中分散任务。
改造上面的MasterApp.scala,在其中代码行val master = system.actorOf(Props[Master], "master")下面增加两行代码:
- val agent = system.actorOf(Props(new Agent), "agent")
- Cluster(system).registerOnMemberUp(agent)
将分布式的守护程序改成了集群式的守护程序(其实这段代码没必要放到Bootable类中),以上述同样的方式运行MasterApp,以不同的端口,跑起任意个守护程序,它们均会join到同一集群中,只要有一个seed-nodes存在,集群就能正常对外提供服务。
新建一个ClusterClient.scala文件,内容如下:
- package com.newegg.demo
-
- import scala.concurrent.forkjoin.ThreadLocalRandom
-
- import akka.actor.Actor
- import akka.actor.ActorRef
- import akka.actor.ActorSelection.toScala
- import akka.actor.Address
- import akka.actor.RelativeActorPath
- import akka.actor.RootActorPath
- import akka.cluster.Cluster
- import akka.cluster.ClusterEvent.CurrentClusterState
- import akka.cluster.ClusterEvent.MemberEvent
- import akka.cluster.ClusterEvent.MemberRemoved
- import akka.cluster.ClusterEvent.MemberUp
- import akka.cluster.MemberStatus
-
- class ClusterClient extends Actor {
- val cluster = Cluster(context.system)
- override def preStart(): Unit = cluster.subscribe(self, classOf[MemberEvent])
- override def postStop(): Unit = cluster unsubscribe self
-
- var nodes = Set.empty[Address]
-
- val servicePath = "/user/agent"
- val servicePathElements = servicePath match {
- case RelativeActorPath(elements) => elements
- case _ => throw new IllegalArgumentException(
- "servicePath [%s] is not a valid relative actor path" format servicePath)
- }
-
- def receive = {
- case state: CurrentClusterState =>
- nodes = state.members.collect {
- case m if m.status == MemberStatus.Up => m.address
- }
- case MemberUp(member) =>
- nodes += member.address
- case MemberRemoved(member, _) =>
- nodes -= member.address
- case _: MemberEvent =>
- case PerfectNumbers(list: List[Int]) =>
- println("\nFound Perfect Numbers:" + list.mkString(","))
- cluster.down(self.path.address)
- context.system.shutdown()
- case StartFind(start: Int, end: Int, resultTo: ActorRef) =>
- println("node size:" + nodes.size)
- nodes.size match {
- case x: Int if x < 1 =>
- Thread.sleep(1000)
- self ! StartFind(start, end, resultTo)
- case _ =>
- val address = nodes.toIndexedSeq(ThreadLocalRandom.current.nextInt(nodes.size))
- val service = context.actorSelection(RootActorPath(address) / servicePathElements)
- service ! StartFind(start, end, resultTo)
- println("send to :" + address)
- }
- }
- }
上面的代码是比较固定的写法,此Actor的作用是:加入集群,订阅集群中有关节点增减变化的消息,维护一个集群中存活节点的地址列表,将任务消息发到集群节点中。
改造上述main函数如下:
- val system = ActorSystem("MasterApp", ConfigFactory.load.getConfig("cluster"))
- system.actorOf(Props(new Actor() {
- context.system.actorOf(Props[ClusterClient], "remoteMaster") ! StartFind(2, 100, self)
- ...
运行此集群客户端程序,可以看到客户端也join到集群中,集群中所有的节点都在分担计算任务,任意增减集群节点数目都是如此。