创建Akka中的actors的成本是多少?

29
考虑这样一种情况,我正在使用 Akka 实现一个处理传入任务的系统。我有一个主要的 actor 接收任务并将它们分派给一些 worker actor 处理任务。
我的第一反应是通过调度程序为每个传入任务创建一个 actor。在 worker actor 处理任务后,actor 会被停止。
对我来说,这似乎是最干净的解决方案,因为它遵循 "一个任务,一个 actor" 的原则。另一个解决方案是重用 actors,但这涉及额外的清理复杂性和一些池管理。
我知道 Akka 中的 actors 很便宜。但我想知道反复创建和删除 actors 是否存在固有成本。Akka 用于 actors 簿记的数据结构是否存在任何隐藏成本?
负载应该是每秒数十或数百个任务 - 将其视为创建一个 actor 的生产 Web 服务器请求。
当然,正确的答案在于根据传入负载类型对系统进行分析和微调。
后附说明:
关于手头的任务,我应该提供更多细节:
  • 仅有 N 个活动任务可以同时运行。如 drexin 所指出的那样 - 这将很容易地通过路由程序来解决。然而,任务的执行不是一种简单的运行和完成的事情。
  • 任务可能需要来自其他 actors 或服务的信息,因此可能必须等待并变为休眠状态。这样做会释放一个执行槽。该槽可以被另一个等待的 actor 占用,现在它有运行的机会。你可以将其类比于在一个 CPU 上安排进程的方式。
  • 每个 worker actor 都需要保留一些关于任务执行的状态。
注意:我欣赏针对我的问题的替代解决方案,并且我一定会考虑它们。然而,我也希望回答关于在 Akka 中密集创建和删除 actors 的主要问题。

你使用了建议的解决方案吗?我们遇到了完全相同的问题... - FrEaKmAn
嗨!你找到答案了吗?你是怎么解决这个问题的? - naaka
1
我的理解是创建角色的成本相对较低。您应该首先尝试设计一个角色系统,让您编写最简单和易于理解的代码。如果这意味着将某些临时任务与其自己的本地状态封装在短暂的演员中,请这样做。然后进行测量,如果性能不够好,请尝试调整系统并减少演员创建的数量,可能会导致演员逻辑变得更加复杂。 - Marius Danila
4个回答

22
你不应该为每个请求创建一个actor,而是应该使用路由器将消息分派给动态数量的actor。这就是路由器的作用。阅读文档中的这部分内容以获取更多信息:http://doc.akka.io/docs/akka/2.0.4/scala/routing.html 编辑:
创建顶级actors(system.actorOf)很昂贵,因为每个顶级actor都会初始化一个错误内核,而这些内核很昂贵。创建子actor(在actor context.actorOf内部)则便宜得多。
但我仍建议你重新考虑这一点,因为根据actor的创建和删除频率,你也会对垃圾回收造成额外的压力。
编辑2:
最重要的是,actors不是线程!因此,即使你创建1M个actors,它们也只会在池中有多少个线程上运行。因此,根据配置中的吞吐量设置,每个actor将处理n条消息,然后将线程释放到池中再次使用。
请注意,阻塞线程(包括休眠)不会将其返回到池中!

是的,在许多情况下路由器是一个不错的选择,但我认为它们在这种情况下不适用。我详细阐述了我的问题以解释原因。 - Marius Danila
1
@drexin,您能详细解释一下为什么顶级actor的错误内核比子actor更昂贵,以及子actor如何(更)便宜吗?据我所知,错误内核只是一种将容易出错的任务推送到上下文actor的模式。 - snw

8
一个在创建后接收一条消息并在发送结果后立即死亡的Actor可以被Future替代。Futures比Actors更轻量级。
您可以使用pipeTo在完成后接收Future的结果。例如,在启动计算的Actor中:
def receive = {
  case t: Task => future { executeTask( t ) }.pipeTo(self)
  case r: Result => processTheResult(r)
}

其中executeTask是您的函数,它需要一个Task并返回一个Result

然而,我建议通过路由器从池中重用演员,如@drexin答案中所解释的那样。


工作人员与系统中的其他角色进行通信。他们不仅运行一些孤立的代码。 - Marius Danila
我稍微详细地描述了我的问题。仅使用期货对我来说不是一个解决方案。 - Marius Danila
我已经阅读了您的编辑,我没有看到任何关于 futures 的问题。您可以通过定义自定义 ExecutionContext 来定义同时运行的最大 future 数量。使用单子操作符,您可以将多个 futures 组合在一起(例如将 futures 与其他服务结果组合在一起),并传递一个状态。 - paradigmatic
可能是这样。我不想进一步详细说明,但工人们的安排基于涉及其当前状态的某些逻辑。虽然您的解决方案是可行的,但将正在运行的任务抽象为独立的工作程序更加清晰且易于维护。 - Marius Danila
1
如果任务处理非常复杂,其成本可能会在很大程度上支配演员创建的成本...回答这个问题的唯一方法是使用您想要投入生产的处理工作流进行基准测试。 - paradigmatic
1
小心使用 futures!它不是万能药!可用于执行它们的资源数量是固定的,无限制地创建它们可能会造成严重后果! - Alex

1

我已经测试了从一些main上下文创建的10000个远程actor,由一个root actor创建,与生产模块中相同的方案创建了一个单个actor。 MBP 2.5GHz x2:

  • 在main中:main?root // main请求root创建一个actor
  • 在main中:actorOf(child) //创建一个子级
  • 在root中:watch(child) //监视生命周期消息
  • 在root中:root?child //等待响应(连接检查)
  • 在child中:child!root //响应(连接正常)
  • 在root中:root!main //通知已创建

代码:

def start(userName: String) = {
  logger.error("HELLOOOOOOOO ")
  val n: Int = 10000
  var t0, t1: Long = 0
  t0 = System.nanoTime
  for (i <- 0 to n) {
    val msg = StartClient(userName + i)
    Await.result(rootActor ? msg, timeout.duration).asInstanceOf[ClientStarted] match {
    case succ @ ClientStarted(userName) => 
      // logger.info("[C][SUCC] Client started: " + succ)
    case _ => 
      logger.error("Terminated on waiting for response from " + i + "-th actor")
      throw new RuntimeException("[C][FAIL] Could not start client: " + msg)
    }
  }
  t1 = System.nanoTime
  logger.error("Starting of a single actor of " + n + ": " + ((t1 - t0) / 1000000.0 / n.toDouble) + " ms")
}

结果:

Starting of a single actor of 10000: 0.3642917 ms

在“HELOOOOOOOO”和“Starting of a single”之间出现了一条消息,指出“Slf4jEventHandler已启动”,因此实验似乎更加真实(?)

调度程序是默认的(PinnedDispatcher每次都会启动一个新线程),看起来所有这些东西与Thread.start()自Java 1以来已经很长时间了-大约500K-1M个周期^)

这就是为什么我已经将循环内部的所有代码更改为new java.lang.Thread().start()

结果:

Starting of a single actor of 10000: 0.1355219 ms

0

演员非常适合作为有限状态机,因此在设计时可以考虑这一点。如果每个请求都有一个演员来处理,则可以极大地简化请求处理状态。我发现,作为经验法则,演员在管理两个以上的状态时特别擅长。

通常情况下,一个处理请求的演员会引用一个集合中维护的请求状态作为其自身状态的一部分。请注意,这也可以通过使用Akka反应流和扫描阶段来实现。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接