Akka中的mailboxes介绍及应用实例

391 阅读3分钟

信箱是行为体模型的基本部分之一。通过邮箱机制,行为体可以将信息的接收与信息的阐述解耦。

所以,让我们看看Akka Typed,这个最著名的行为体系统的化身,是如何实现邮箱的概念的。

记录用户的浏览情况

首先,行为体是一个对象,它通过响应它所收到的通信来执行其行动。因此,在Akka Typed中,这种通信是行为体行为_者_ 定义的消息_。_

在这一点上,我们需要一个具体的例子来继续讨论。想象一下,我们刚刚开始了一个新的电子商务业务,通过一个网站销售鞋子。为了更好地理解我们用户的行为,我们想收集他们在电子商务网站内的导航信息。

例如,让我们收集鼠标移动、点击和用户在键盘上的文字:

sealed trait UserEvent {
  val usedId: String
}
case class MouseClick(override val usedId: String, x: Int, y: Int) extends Event
case class TextSniffing(override val usedId: String, text: String) extends Event
case class MouseMove(override val usedId: String, x: Int, y: Int) extends Event

因此,为了释放反应式系统的力量,我们定义了一个收集此类信息的行为者:

val eventCollector: Behavior[UserEvent] = Behaviors.receive { (ctx, msg) =>
  msg match {
    case MouseClick(user, x, y) => 
      ctx.log.info(s"The user $user just clicked point ($x, $y)")
    case MouseMove(user, x, y) => 
      ctx.log.info(s"The user $user just move the mouse to point ($x, $y)")
    case TextSniffing(user, text) =>
      ctx.log.info(s"The user $user just entered the text '$text'")
  }
  Behaviors.same
}

扩展我们的业务

正如我们所说,actor_eventCollector_从其邮箱中读取要处理的信息。信箱不过是存放消息的数据结构。 _ActorSystem_负责为actor的邮箱填充消息。

有一天,我们电子商务网站的广告出现在美国《时尚》杂志上,第二天,浏览网站的用户数量增加了十倍。我们的_事件收集器(eventCollector_)角色会发生什么?它是否有关于负载的意外增加的弹性?

不幸的是,长话短说,这个角色将被大量的信息所淹没,系统将以_OutOfMemoryError_结束。事实上,行为体的默认邮箱是_SingleConsumerOnlyUnboundedMailbox_。

顾名思义,这个邮箱是无界的,这意味着它不会拒绝任何已交付的信息。此外,actor系统中没有实现背压机制。因此,如果传入的消息数量远远大于角色的执行速度,系统将很快耗尽内存。

此外,使用_SingleConsumerOnlyUnboundedMailbox_邮箱的角色不能与其他角色共享,实现了多生产者和只有一个消费者的通信模型。

我们怎样才能避免可怜的_事件收集器(eventCollector_ )角色毁了我们的业务呢?让我们一起看看如何配置我们的角色。

信箱的类型

幸运的是,Akka Typed库中有相当多的邮箱类型可供我们选择。我们可以在actor的创建过程中为其设置邮箱类型,而不是接受默认的类型。因此,我们使用_Props_对象来选择合适的邮箱:

ctx.spawn(eventCollector, id, MailboxSelector.bounded(1000))

正如我们所看到的,MailboxSelector_工厂让我们创建一个_Props 对象进行适当的配置。在上面的例子中,我们将actor配置为提供一个有界的邮箱,它只存储固定数量的消息。我们将在文章的后面看到更多关于有界邮箱的内容。

此外,通过_MailboxSelector.fromConfig_工厂方法,可以从配置属性中读取邮箱类型:

val props = MailboxSelector.fromConfig("mailboxes.event-collector-mailbox")
ctx.spawn(eventCollector, s"{$id}_1", props)

因此,配置文件将是类似的东西:

mailboxes {
  event-collector-mailbox {
    mailbox-type = "akka.dispatch.SingleConsumerOnlyUnboundedMailbox"
  }
}

一般来说,最好不要对配置进行硬编码。所以,这种方法是比较好的,因为我们永远不知道生活中会有什么。

现在,我们刚刚了解到,有许多类型的邮箱。粗略地说,我们可以用两种不同的特征对它们进行分类:

  1. 有界与无界
  2. 阻断式与非阻断式

我们可以在Akka Typed官方文档中找到可用邮箱类型的完整列表。

有界的与无界的

正如我们所说的,如果消息的生产者远比消费者快,那么无界的邮箱会无限增长,消耗所有可用的内存。因此,我们只在琐碎的用例中使用这种邮箱。

另一方面,有界邮箱只保留固定数量的消息。当邮箱满了的时候,行为者系统会丢弃所有到达行为者处的消息。这样一来,我们就可以避免内存耗尽。

正如我们刚才所做的,我们可以直接使用_Mailbox.bounded_工厂方法来配置邮箱的大小。或者,更好的是,我们可以通过配置属性文件来指定它:

mailboxes { 
  event-collector-mailbox { 
    mailbox-type = "akka.dispatch.BoundedMailbox"
    mailbox-capacity = 100
  } 
}

上面的例子是有界邮箱大放异彩的一个明显例子。如果对应的系统能保持正常运行,我们就不怕丢失一些邮件。

一个新的问题应该出现了。被丢弃的邮件去了哪里?它们就这样被扔掉了吗?幸运的是,行为体系统让我们通过死信的机制来检索被丢弃的消息的信息--我们很快就会了解更多关于这个机制的工作原理。

阻断与非阻断

对于行为体模型来说,这听起来很奇怪,但Akka也提供了一种阻断消息生产者的邮箱类型。事实上,使用阻塞式邮箱,发送者将被阻塞,直到actor系统通知消息被成功传递到邮箱中。

显然,让生产者永远等待并不是一个好主意。因此,Akka提供了带有超时设置的有界邮箱,这被称为_邮箱推送超时时间_:

mailboxes { 
  event-collector-mailbox { 
    mailbox-type = "akka.dispatch.BoundedMailbox" 
    mailbox-capacity = 100 
    mailbox-push-timeout-time = 1s
  }
}

发送者将在指定的时间内等待消息被插入邮箱中。之后,行为者系统将把消息发送到_死信_,而生产者将自由地进行处理。然而,如果我们将_邮箱推送时间_设置为零,我们又会得到一个无阻塞的邮箱。

所以,使用阻塞的、有边界的邮箱是Akka实现反压的方式

死信

每当一个消息未能被写入actor邮箱,actor系统就会将其重定向到一个名为_/deadLetters_的合成actor。死信消息的交付保证与系统中的任何其他消息相同。所以,最好不要太相信这样的消息。死信的主要目的是调试

我们可以使用actor系统对象中的专用方法检索到默认的监听死信的actor的引用:

val defaultDeadLettersActor: ActorRef[DeadLetter] = system.deadLetters[DeadLetter]

此外,一个角色可以订阅接收_akka.actor.DeadLetter_消息。行为体系统使用一个特殊的通信通道来传递死信,称为事件流

所以,为了将一个角色订阅到事件流,该角色必须在其行为中监听_DeadLetter_消息:

val deadLettersListener: Behavior[DeadLetter] = Behaviors.receive { (ctx, msg) =>
  msg match {
    case DeadLetter(message, sender, recipient) =>
      ctx.log.debug(s"Dead letter received: ($message, $sender, $recipient)")
      Behaviors.same
  }
}

然后,行为体系统必须被工具化,将死信重定向到该行为体。

val deadLettersActor: ActorRef[DeadLetter] = 
  system.systemActorOf(deadLettersListener, "deadLettersListener")
system.eventStream.tell(EventStream.Subscribe[DeadLetter](deadLettersActor))

然而,我们必须记住,被订阅的角色将只收到本地系统中发布的死信,因为死信不会在网络上传播

结论

总结一下,在这篇文章中,我们介绍了行为体邮箱的概念。我们通过一个例子探讨了无界邮箱的优点和缺点,以及何时使用有界邮箱来代替。