在Akka管理拥挤的演员

Hovedøya

Hovedøya

在Akka应用程序中,有时actor可以更长的时间来处理不断增加的负载。

由于每个参与者一次只能处理一条消息,并且将待处理消息的待办事项积压在称为邮箱的队列中,因此如果同时向一个参与者发送太多消息,或者参与者无法执行任务,则存在使一个参与者过载的风险。处理消息的速度足够快–队列将不断增长。

这将负面影响系统的响应能力,甚至可能导致应用程序崩溃。

实际上,通过简单地尽可能快地将连续的消息流发送给actor来模拟这种负载非常容易:

case object Ping
 
class PingActor extends Actor {
 
    def receive = {
        case Ping =>
            //don't do this at home!
            Thread sleep 1
    }
}
 
object Main extends App {
    val system = ActorSystem("Heavy")
    val client = system.actorOf(Props[PingActor], "Ping")
    while(true) {
        client ! Ping
    }
}

当然,您永远都不应该在演员中睡觉,这只是为了强调邮箱。 如果您很(不幸)并且花了很多时间在睡眠中,那么您的应用程序将很快花费大部分时间进行(无结果的)GC,并且您可能会看到可怕的OOM错误:

Exception: java.lang.OutOfMemoryError thrown from the UncaughtExceptionHandler in thread "Heavy-akka.actor.default-dispatcher-6"
Exception: java.lang.OutOfMemoryError thrown from the UncaughtExceptionHandler in thread "Heavy-akka.actor.default-dispatcher-10"

……最后死:

Uncaught error from thread [Heavy-akka.actor.default-dispatcher-7] shutting down JVM
since 'akka.jvm-exit-on-fatal-error' is enabled for ActorSystem[Heavy]
java.lang.OutOfMemoryError: GC overhead limit exceeded

今天,我们将学习如何处理此类拥挤的参与者,以使流量的突然爆发不会使整个应用程序崩溃。

路由和负载平衡

减轻一个演员负担的一种简单解决方案是将工作分散在该演员的多个副本上。 Akka提供内置
路由和负载平衡角色,位于前端并管理我们角色的多个实例。 路由器选择(使用可配置策略)基础实例之一,因此分散了负载:

val props = Props[PingActor].
       withRouter(RoundRobinRouter(nrOfInstances = 10))
val client = system.actorOf(props, "Ping")

我们要做的是,要求Akka在10个独立的PingActor实例(而不是一个)的前面放置Round Robin路由器。 从理论上讲,这可以将等待时间减少一个数量级。 因此,如果路由如此有效,那么为什么不像Enterprise Java Beans池一样默认且透明地使用它呢?

为了回答这个问题,我们需要一个更复杂的例子。 PingActor是无状态的,因此可以在路由器后安全地复制它。 但是接下来的演员呢?

class StoreActor extends Actor {
 
    private var lastUsedId = 0
 
    def receive = {
        case Store(s) =>
            val id = nextUniqueId()
            slowStore(s, id)
            sender ! Done(id)
    }
 
    private def nextUniqueId() = {
        lastUsedId += 1
        lastUsedId
    }
 
    private def slowStore(s: String, id: Int) {
        //...
    }
}

显然, StoreActor假定只有lastUsedId一个实例,并且由于从不并发调用receive ,因此可以保证ID的唯一性。 我们生成唯一的ID,存储一些消息,并将生成的ID发送回客户端actor。

不幸的是,当我们将任何路由器放置在StoreActor前面时,该actor的每个副本都有其自己的lastUsedId变量,并且不可避免地要重复。 让我们重新考虑我们的设计。 为了生成唯一的ID,我们必须仅拥有一个计数器副本并限制对其的访问。 但是存储很可能是线程安全的,因此可以并行化。 最简单的解决方案是使用StoreActorStoreActor对象和AtomicInteger

//DIRTY! Close your eyes!
object StoreActor {
 
    val lastUsedId = new AtomicInteger
 
}
 
class StoreActor extends Actor with ActorLogging {
 
    private def nextUniqueId() = StoreActor.lastUsedId.incrementAndGet()
 
    //...
 
}

好吧……老实说,共享可变状态几乎不是我们所追求的。 我们应该更仔细地研究参与者模型,提取ID生成逻辑来分离参与者,从而将单一责任原则作为奖励:

case object GiveMeUniqueId
 
class UniqueIdActor extends Actor {
 
    private var lastUsedId = 0
 
    def receive = {
        case GiveMeUniqueId =>
            lastUsedId += 1
            sender ! lastUsedId
    }
 
}

显然,路由器后面的所有StoreActor实例都应共享对一个UniqueIdActor实例的引用:

class StoreActor(uniqueIdActor: ActorRef) extends Actor {
 
    private implicit val timeout = Timeout(10 minutes)
 
    import context.dispatcher
 
    def receive = {
        case Store(s) =>
            uniqueIdActor ? GiveMeUniqueId map {
                case id: Int =>
                    slowStore(s, id)
                    Done(id)
            } pipeTo sender
    }
 
    private def slowStore(s: String, id: Int) {
        //...
    }
}

如您所见, uniqueIdActor被传递给actor构造函数。 显然,我们不应该在每个StoreActor创建新的UniqueIdActor ,因为那样会产生10个独立的子副本,而不是一个集中的actor。 这是胶水代码:

val uniqueIdActor = system.actorOf(Props[UniqueIdActor], "UniqueId")
val props = Props(classOf[StoreActor], uniqueIdActor).
       withRouter(RoundRobinRouter(nrOfInstances = 10))
val client = system.actorOf(props, "Heavy")

软件事务存储

您可能会觉得单独包装一个Int演员是一个过大的杀手。 另一方面,共享的可变AtomicInteger与Akka的“无共享”精神相去甚远。 我们可以在基于ScalaSTM的 Akka中试用软件事务存储 。 我们将使用事务Ref包装可变的Int ,并在所有StoreActor之间共享此引用:

class StoreActor(counter: Ref[Int]) extends Actor {
 
    def receive = {
        case Store(s) =>
            val id = nextUniqueId()
            slowStore(s, id)
            sender ! Done(id)
    }
 
    private def nextUniqueId() = atomic {
        implicit tx =>
            counter += 1
            counter()
    }
 
    //...
 
}

这次所有StoreActor实例共享事务Ref[Int] 。 调用nextUniqueId()在事务内增加counter ,因此该代码是线程安全的。 更简单的体系结构和同步的nextUniqueId()更易于阅读和维护。 但是,任何形式的共享数据结构都是有问题的,尤其是当我们尝试扩展时。 但是,就像练习一样,尝试将STM替换为
代理商 。 这是STM的起始胶水代码:

import scala.concurrent.stm.Ref
 
val globalUniqueId = Ref(0)
val props = Props(classOf[StoreActor], globalUniqueId).
       withRouter(RoundRobinRouter(nrOfInstances = 10))
val client = system.actorOf(props, "Heavy")

在一个完美的世界中,可以在多个角色之间分配工作。 但是,如果我们真的只需要一个实例,而又跟不上传入消息怎么办? 在这种情况下,我们至少应该快速失败

有界邮箱

默认情况下,邮箱仅受我们拥有的内存量限制。 这意味着一个流氓角色可以影响整个系统,因为每个角色都有一个单独的邮箱,但它们都共享相同的堆。 一个简单的解决方案是限制邮箱的大小,并简单地丢弃高于给定阈值的所有内容。 幸运的是,Akka支持(邮箱)开箱即用的有界邮箱 。 通常,如果我们不能应付不断增加的负载,那么我们至少应该快速失败而不是永远挂掉。

class StoreActor extends Actor with RequiresMessageQueue[BoundedMessageQueueSemantics] {
 
    private var lastUniqueId = 0
 
    //...
 
}

另外,您必须在代码或application.conf配置队列容量:

bounded-mailbox {
    mailbox-type = "akka.dispatch.BoundedMailbox"
    mailbox-capacity = 1000
    mailbox-push-timeout-time = 100ms
}

使用此配置,只有一个StoreActor实例可以排队多达1000条消息。 如果发送了更多消息,则将它们丢弃并转发到Dead Letter Queue ,除非StoreActor邮箱不会在100毫秒内收缩。

摘要

保持邮箱简短和参与者快速是影响Akka应用程序的响应能力和稳定性的关键因素。 通过监视系统,您应该发现瓶颈,然后向上扩展/向外扩展或快速失败。 否则,您的JVM将Swift开始阻塞并失去动力。

参考: Java和社区博客上的JCG合作伙伴 Tomasz Nurkiewicz 管理Akka中拥挤的演员

翻译自: https://www.javacodegeeks.com/2013/07/managing-congested-actors-in-akka.html

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值