Scala Akka调度模式

Scala Akka调度模式,scala,akka,Scala,Akka,考虑经典的“字数计算”程序。它统计某个目录中所有文件的字数。Master接收一些目录并在工作者参与者之间拆分作业(每个工作者使用一个文件)。这是伪代码: class WordCountWorker extends Actor { def receive = { case FileToCount(fileName:String) => val count = countWords(fileName) sender ! WordCount(fileName

考虑经典的“字数计算”程序。它统计某个目录中所有文件的字数。Master接收一些目录并在工作者参与者之间拆分作业(每个工作者使用一个文件)。这是伪代码:

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
      ...

  }
}
但是我想按计划运行这个单词计数程序(例如每1分钟一次),提供不同的目录进行扫描

Akka为安排消息传递提供了很好的方法:

system.scheduler.schedule(0.seconds, 1.minute, wordCountMaster , StartCounting(directoryName))
但当调度器按记号发送新消息时,上述调度器的问题开始出现,但之前的消息尚未处理(例如,我发送消息扫描某个大目录,1秒后发送另一条消息扫描另一个目录,因此处理第一个目录的操作尚未完成)。因此,我的
WordCountMaster
将从处理不同目录的工作人员那里接收
WordCount
消息

作为一种替代计划消息发送的解决方法,我可以计划一些代码块的执行,这些代码块将在每次新建
WordCountMaster
时创建。即一个目录=一个
WordCountMaster
。但我认为这是低效的,而且我需要注意为
WordCountMaster
提供唯一的名称,以避免
InvalidActorNameException

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


一些更新: 在为每个目录创建一个主参与者的情况下,我遇到了一些问题:

  • 命名演员的问题
  • InvalidActorNameException:参与者名称[WordCountMaster]不唯一

    InvalidActorNameException:参与者名称[WordCountWorker]不正确 独一无二

    我可以克服这个问题,只是不提供演员的名字。但是在这种情况下,我的演员会收到自动生成的名字,比如
    $a
    $b
    等等。这对我不好

  • 配置有问题:
  • 我想将路由器的配置排除在
    application.conf
    之外。也就是说,我想为每个
    WordCountWorker
    路由器提供相同的配置。但由于我不控制参与者名称,因此无法使用下面的配置,因为我不知道参与者名称:

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

    我不是Akka专家,但我认为每个聚合使用一个参与者的方法并不是低效的。您需要以某种方式分离并发聚合。您可以为每个聚合提供一个id,以便在一个且唯一的主参与者中用id分隔它们,或者您可以使用Akka参与者命名和活动周期逻辑,并将每个计数轮的每个聚合委托给一个只为该聚合逻辑而活的参与者

    对我来说,每个聚合使用一个参与者似乎更优雅


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

    您应该在worker中使用功能。如果您的工作人员开始扫描大文件夹,请使用
    been
    更改忽略另一条消息的参与者行为(或不处理该消息的响应),在目录扫描后,使用字数将消息发送回,并
    取消对
    的命名,使其成为标准行为。

    首先。对于命名问题:只需动态且唯一地命名您的参与者,类似这样:
    WorkerActor+“-”+文件名…或。。。MasterActor+“-”+目录名
    还是我遗漏了什么

    第二,为什么要安排?当第一个目录完成时,开始处理下一个目录不是更合乎逻辑吗?如果计划是一项要求,那么我看到了许多不同的解决方案,我将尝试解决其中一些问题:

    1.
    三级层次结构:
    MasterActor->DirectoryActor->WorkerActor
    为每个新目录创建一个新的目录参与者,为每个文件创建一个新的工作进程

    2.
    两级层次结构:
    主演员->工作演员
    您可以为每个文件创建一个新工作进程。
    两个用于识别接收结果的选项:
    a) 通过询问将工作分发给工人,并通过未来汇总结果
    b) 在作业中包括消息ID(例如目录名)

    3.
    具有负载平衡的两级层次结构:
    与选项2相同,但您不会为每个文件创建新的工作进程,您有固定数量的工作进程,可以使用平衡调度程序,也可以使用最小的邮箱路由器

    4.
    具有未来的一级层次结构:
    大师级演员没有孩子,他只会工作并用未来来聚合结果


    我还建议阅读Gregor Raýman在回答中提出的Akka聚合模式。

    就我个人而言,我根本不会使用参与者来解决聚合问题,但不管怎样,还是这样

    我不认为有一种合理的方法可以像你建议的那样同时处理多个目录的字数计算。相反,你应该有一个“大师级”演员来监督计数器。因此,您有三个演员类:

    • FileCounter:它接收要读取的文件并仅对其进行处理。完成后,它会将结果发送回发件人
    • CounterSupervisor:这个跟踪哪个FileCounter已经完成了他们的工作,并将结果发送回WordCountForker
    • WordCountForker:这个参与者将跟踪哪个子系统完成了他们的任务,如果他们都很忙,就创建一个新的副手来解决这个问题
    文件计数器必须是最容易写入的

    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) } }