在 Akka 中创建演员的成本是多少?

Posted

技术标签:

【中文标题】在 Akka 中创建演员的成本是多少?【英文标题】:What is the cost of creating actors in Akka? 【发布时间】:2012-11-28 06:55:33 【问题描述】:

考虑一个场景,我正在实现一个使用 Akka 处理传入任务的系统。我有一个主要参与者,它接收任务并将它们分派给一些处理任务的工作参与者。

我的第一个直觉是通过让调度程序为每个传入任务创建一个演员来实现这一点。工作角色处理完任务后,它会停止。

这对我来说似乎是最干净的解决方案,因为它遵循“一个任务,一个演员”的原则。另一种解决方案是重用演员 - 但这涉及清理和一些池管理的额外复杂性。

我知道 Akka 的演员很便宜。但我想知道是否存在与重复创建和删除演员相关的固有成本。 Akka 用于记录参与者的数据结构是否存在任何隐藏成本?

负载应该是每秒数十或数百个任务的数量级 - 将其视为生产网络服务器,每个请求创建一个参与者。

当然,正确的答案在于根据传入负载的类型对系统进行分析和微调。 但我想知道是否有人可以根据自己的经验告诉我一些事情?

稍后编辑:

我应该提供有关手头任务的更多详细信息:

在某个时间点只能运行 N 个活动任务。正如@drexin 指出的那样 - 这可以使用路由器轻松解决。但是,任务的执行并不是简单的运行和完成类型的事情。 任务可能需要来自其他参与者或服务的信息,因此可能必须等待并进入睡眠状态。通过这样做,他们释放了一个执行槽。这个槽可以被另一个等待的actor占据,它现在有机会运行。您可以类比进程在一个 CPU 上的调度方式。 每个工作参与者都需要保持一些关于任务执行的状态。

注意:我很欣赏我的问题的替代解决方案,我一定会考虑它们。但是,我还想回答有关 Akka 中密集创建和删除演员的主要问题。

【问题讨论】:

您是否使用了任何建议的解决方案?我们有完全相同的问题... 嗨!你找到答案了吗?你是怎么解决这个问题的? 我的理解是演员的创作成本相对较低。您应该首先尝试设计一个允许您编写最简单和最易理解的代码的参与者系统。如果这意味着将一些临时任务与它自己的本地状态封装在一个短命的参与者中——那就去做吧。然后进行测量,如果性能不够好,请尝试调整系统并减少创建 Actor 的数量,这可能会导致 Actor 逻辑变得更加复杂。 【参考方案1】:

Actor 可以制作出色的有限状态机,因此请在此处帮助推动您的设计。如果您的请求处理状态通过每个请求有一个参与者而大大简化,那么就这样做。根据经验,我发现演员特别擅长管理两个以上的州。

不过,通常情况下,一个请求处理参与者从一个集合中引用请求状态,该集合作为其自身状态的一部分进行维护,这是一种常见的方法。请注意,这也可以通过 Akka 反应流和使用扫描阶段来实现。

【讨论】:

【参考方案2】:

您不应该为每个请求都创建一个参与者,而应该使用路由器将消息分派给动态数量的参与者。这就是路由器的用途。阅读这部分文档以获取更多信息:http://doc.akka.io/docs/akka/2.0.4/scala/routing.html

编辑:

创建*** Actor (system.actorOf) 的成本很高,因为每个*** Actor 也会初始化一个错误内核,而且成本很高。创建子演员(在演员context.actorOf 内)要便宜得多。

但我仍然建议你重新考虑这一点,因为根据创建和删除演员的频率,你也会对 GC 施加额外的压力。

编辑2:

最重要的是,演员不是线程!所以即使你创建了 1M 个actor,它们也只能在池中的线​​程数上运行。因此,根据配置中的吞吐量设置,每个参与者将在线程再次释放到池之前处理 n 条消息。

请注意,阻塞线程(包括休眠)不会将其返回到池中!

【讨论】:

是的,路由器在很多情况下都是不错的选择,但我认为它们在这种情况下不起作用。我详细说明了我的问题以解释原因。 @drexin,您能否详细说明为什么***actor错误内核比子actor更昂贵,以及子actor如何(方式)便宜?据我了解,错误内核只是一种将容易出错的任务推给上下文参与者的模式。【参考方案3】:

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

在主要:主要? root // main 请求 root 创建一个actor in main: actorOf(child) // 创建一个孩子 在根目录中:watch(child) // 观察生命周期消息 在根目录中:根目录? child // 等待响应(连接检查) 在孩子:孩子! 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 started”,所以实验看起来更加真实(?)

Dispatchers 是默认设置(PinnedDispatcher 每次都会启动一个新线程),并且似乎所有这些东西都与 Thread.start() 相同,自从 Java 1 - 500K-1M 周期或所以^)

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

结果:

Starting of a single actor of 10000: 0.1355219 ms

【讨论】:

【参考方案4】:

在创建后立即收到一条消息并在发送结果后立即死亡的演员可以被未来替换。 Future 比 Actor 更轻量级。

您可以使用pipeTo 接收完成后的未来结果。例如在你的演员启动计算:

def receive = 
  case t: Task => future  executeTask( t ) .pipeTo(self)
  case r: Result => processTheResult(r)

其中executeTask 是您的函数,它采用Task 来返回Result

但是,我会通过路由器重用池中的演员,如@drexin 回答中所述。

【讨论】:

worker 与系统中的其他参与者进行通信。他们不只是运行一些孤立的代码 我稍微详细说明了我的问题。仅使用期货对我来说不是解决方案。 我已阅读您的编辑,我认为期货没有任何问题。您可以通过定义自定义ExecutionContext 来定义同时运行的期货的最大数量。使用一元运算符,您可以将多个期货组合在一起(例如期货到其他服务结果)并传递一个状态。 如果任务处理非常复杂,它的成本可能在很大程度上决定了创建演员的成本......回答这个问题的唯一方法是使用您想要投入生产的处理工作流程对其进行基准测试. 小心期货!这不是灵丹妙药!有固定数量的资源可用于执行它们,无限制地创建它们可能会造成严重破坏!

以上是关于在 Akka 中创建演员的成本是多少?的主要内容,如果未能解决你的问题,请参考以下文章

在另一个actor中创建Akka actor

如何访问通过 guice 创建的 akka 系统?

Swift 的演员阵容的运行时成本是多少?

如何从演员本身中获取 Akka 演员的名字?

Akka - 你应该创建多少个actor实例?

akka http:Akka 流与演员建立休息服务