Akka调度模式

时间:2015-05-25 21:54:40

标签: scala akka

考虑经典的“字数”计划。它计算某些目录中所有文件中的单词数。 Master接收一些目录并在Worker actor之间拆分作业(每个worker使用一个文件)。这是伪代码:

class WordCountWorker extends Actor {

  def receive = {
    case FileToCount(fileName:String) =>
      val count = countWords(fileName)
      sender ! WordCount(fileName, count)
  }
}

class WordCountMaster extends Actor {
  def receive = {
    case StartCounting(docRoot) => // sending each file to worker
      val workers = createWorkers()
      fileNames = scanFiles(docRoot)
      sendToWorkers(fileNames, workers)
    case WordCount(fileName, count) => // aggregating results
      ...

  }
}

但是我想按计划运行这个Word Count程序(例如每1分钟),提供不同的扫描目录。

Akka为调度消息传递提供了很好的方法:

system.scheduler.schedule(0.seconds, 1.minute, wordCountMaster , StartCounting(directoryName))

但是上面的调度程序的问题在调度程序通过tick发送新消息时启动,但是之前的消息尚未处理(例如我发送消息来扫描一些大目录,1秒后我发送另一条消息来扫描另一个目录,所以第一目录的处理操作尚未完成)。因此,我的WordCountMaster将收到来自正在处理不同目录的工作人员的WordCount条消息。

作为一种解决方法而不是安排消息发送,我可以安排执行某些代码块,每次 new WordCountMaster时都会创建。即一个目录=一个WordCountMaster。但我认为它效率低下,而且我需要谨慎为WordCountMaster提供唯一的名称以避免InvalidActorNameException

所以我的问题是:我是否应该像上面提到的那样为每个刻度创建新的WordCountMaster?或者有一些更好的想法/模式如何重新设计这个程序以支持日程安排?

一些更新: 如果每个目录创建一个主演员,我有一些问题:

  1. 命名演员的问题
  2.   

    InvalidActorNameException:actor name [WordCountMaster]不是唯一的!

      

    InvalidActorNameException:actor名称[WordCountWorker]不是   独特!

    我可以克服这个问题而不提供演员姓名。但在这种情况下,我的演员会收到自动生成的名称,例如$a$b等。这对我不利。

    1. 配置问题:
    2. 我想将路由器的配置排除在application.conf之外。即我想为每个WordCountWorker路由器提供相同的配置。但由于我不控制演员姓名,所以我不能使用下面的配置,因为我不知道演员姓名:

        /wordCountWorker{
          router = smallest-mailbox-pool
          nr-of-instances = 5
          dispatcher = word-counter-dispatcher
        }
      

4 个答案:

答案 0 :(得分:4)

我不是Akka专家,但我认为每个聚合都有一个演员的方法效率不高。您需要以某种方式保持并发聚合的分离。你可以给每个聚合一个id,所以让它们在一个唯一的主演员中用id分隔,或者你可以使用Akka actor命名和实时循环逻辑,并将每个计数轮的每个聚合委托给一个活着的演员只是为了那个聚合逻辑。

对我来说,每个聚合使用一个演员似乎更优雅。

另请注意,Akka有一个聚合模式的实现,如here

所述

答案 1 :(得分:3)

就个人而言,我根本不会使用演员来解决这个聚合问题,但无论如何,这里也是如此。

我认为没有一种合理的方法可以按照您建议的方式同时处理多个目录的字数统计。你应该有一个监督计数器的“主 - 主”演员。所以相反,你有三个演员类:

  • FileCounter:它接收一个要读取的文件并进行处理。完成后,它会将结果发送回发件人。
  • CounterSupervisor:这个跟踪哪个FileCounter已经完成了他们的工作,并将结果发送回WordCountForker。
  • WordCountForker:此演员将跟踪哪个子系统完成了他们的任务,如果他们都忙,请创建一个新的CounterSupervisor来解决问题。

文件计数器必须是最容易编写的。

class FileCounter() extends Actor with ActorLogging {

    import context.dispatcher

    override def preStart = {
        log.info("FileCounter Actor initialized")
    }

    def receive = {
        case CountFile(file) =>
            log.info("Counting file: " + file.getAbsolutePath)

            FileIO.readFile(file).foreach { data =>
                val words = data
                    .split("\n")
                    .map { _.split(" ").length }
                    .sum

                context.parent ! FileCount(words)
            }
    }
}

现在是监督文件计数器的演员。

class CounterSupervisor(actorPool: Int) extends Actor with ActorLogging {

    var total = 0
    var files: Array[File] = _
    var pendingActors = 0

    override def preStart = {
        for(i <- 1 to actorPool)
            context.actorOf(FileCounter.props(), name = s"counter$i")
    }

    def receive = {
        case CountDirectory(base) =>
            log.info("Now counting starting from directory : " + base.getAbsolutePath)
            total = 0
            files = FileIO.getAllFiles(base)
            pendingActors = 0
            for(i <- 1 to actorPool if(i < files.length)) {
                pendingActors += 1
                context.child(s"counter$i").get ! CountFile(files.head)
                files = files.tail
            }

        case FileCount(count) =>
            total += count
            pendingActors -= 1
            if(files.length > 0) {
                sender() ! CountFile(files.head)
                files = files.tail
                pendingActors += 1
            } else if(pendingActors == 0) {
                context.parent ! WordCountTotal(total)
            }
    }
}

然后是监督上司的演员。

class WordCountForker(counterActors: Int) extends Actor with ActorLogging {

    var busyActors: List[(ActorRef, ActorRef)] = Nil
    var idleActors: List[ActorRef] = _

    override def preStart = {
        val first = context.actorOf(CounterSupervisor.props(counterActors))
        idleActors = List(first)
        log.info(s"Initialized first supervisor with $counterActors file counters.")
    }

    def receive = {
        case msg @ CountDirectory(dir) =>
            log.info("Count directory received")
            val counter = idleActors match {
                case Nil =>
                    context.actorOf(CounterSupervisor.props(counterActors))
                case head :: rest =>
                    idleActors = rest
                    head
            }
            counter ! msg
            busyActors = (counter, sender()) :: busyActors

        case msg @ WordCountTotal(n) =>
            val path = sender().path.toString()
            val index = busyActors.indexWhere { _._1.path.toString == path }
            val (counter, replyTo) = busyActors(index)
            replyTo ! msg
            idleActors = counter :: idleActors
            busyActors = busyActors.patch(index, Nil, 1)
    }
}

如果您想查看代码的其余部分I posted a Gist,我会在答案中留下一些部分,以尽可能简洁。

此外,关于您对效率的担忧,这里的解决方案将阻止每个目录有一个子系统,但如果有需要,您仍会产生多个子系统。

答案 2 :(得分:2)

您应该在工作中使用become/unbecome功能。如果您的工作人员开始扫描大文件夹,请使用become更改忽略另一条消息(或不处理该消息的响应)的actor行为,在目录扫描后发送带有字数和unbecome的消息标准行为。

答案 3 :(得分:1)

首先。命名问题:只需动态和唯一地命名你的演员,如下所示:
WorkerActor +“ - ”+ filename ...或... MasterActor +“ - ”+ directoryName
或者我错过了什么?

其次,为什么安排?第一个目录完成后,开始处理下一个目录是不是更合乎逻辑?如果需要安排,那么我会看到许多不同的解决方案,我会尝试解决其中的一些问题:

1。
三级层次结构:
MasterActor - &gt; DirectoryActor - &gt; WorkerActor
为每个新目录创建一个新的目录actor,为每个文件创建一个新的worker。

2。
两级层次结构:
MasterActor - &gt; WorkerActor
您为每个文件创建一个新工作人员 确定收到结果的两个选项:
a)通过期货询问和汇总结果,向工人分发工作 b)在作业中包含消息ID(例如目录名称)

3。
具有负载平衡的两级层次结构:
与选项2相同,但您没有为每个文件创建新工作程序,您有固定数量的工作程序,其中包含平衡调度程序或最小邮箱路由器。

4。
具有期货的一级层次结构:
主演员没有孩子,他确实工作,只用期货汇总结果。

我还建议阅读GregorRaýman在他的回答中提到的Akka聚合模式。