Akka调度程序:生产中的奇怪行为(消息未触发)

时间:2017-12-13 15:57:54

标签: multithreading scala jdbc akka akka-cluster

我正在开发一个scala + akka应用程序作为更大的应用程序的一部分。该应用程序的目的是调用外部服务和SQL数据库(使用JDBC),进行一些处理,并以经常性方式返回解析结果。该应用程序使用akka群集,以便它可以水平扩展。

如何运作

我在群集上创建一个**单身演员*,负责将指令发送到指令处理程序演员池。我从Redis发布/子频道接收事件,说明应该刷新哪些数据源以及频率。此 SourceScheduler actor将指令与间隔存储在内部Array中。

然后我每秒都使用akka Scheduler执行 tick 功能。此函数过滤数组以确定需要执行哪些指令,并将消息发送到指令处理程序池。池中的路由执行指令并通过Redis Pub / Sub

发出结果

问题

在我的机器上(Ryzen 7 + 16GB RAM + ArchLinux)一切正常,我们可以轻松处理2500个数据库调用/秒。但是一旦投入生产,我就无法处理超过400个请求/秒。

SourceScheduler每秒都没有打勾,邮件卡在邮箱中。此外,该应用程序使用更多的CPU资源,以及更多的RAM(生产1.3GB,而我的机器约350MB)

生产应用程序在MS Azure服务器上Rancher的JRE-8基于alpine的Docker容器中运行。

我理解群集上的单身人员可能会成为瓶颈,但由于它只是将消息转发给其他参与者,所以我不知道它是如何阻止的。

我尝试了什么

  • 我使用Tomcat JDBC作为SQL查询的连接池管理器。我确定我没有泄漏任何连接,因为我记录了从池中借来的每个连接以及返回它的每个连接
  • 像JDBC查询这样的阻塞操作都是在一个单独的调度程序上执行的,一个具有500个线程的固定线程池执行程序,所以其他所有actor都应该正常运行
  • 我还给SourceScheduler演员一个专门的固定调度员,所以它应该在它自己的线程上运行
  • 我尝试在3个节点的集群中运行应用程序,但性能没有提高。由于SourceScheduler是单例,因此运行多个节点无法解决问题
  • 我已经在同事的机器上试过了这个应用程序。奇迹般有效。我只遇到生产服务器的问题
  • 我尝试将生产服务器升级到Azure上最强大的服务器(16核,2.3ghz),没有明显的变化

任何人在本地计算机和生产服务器之间都遇到过这样的差异吗?

编辑 SourceScheduler.scala

class SourceScheduler extends Actor with ActorLogging with Timers {
  case object Tick
  case object SchedulerReport
  import context.dispatcher

  val instructionHandlerPool = context.actorOf(
    ClusterRouterGroup(
      RoundRobinGroup(Nil),
      ClusterRouterGroupSettings(
        totalInstances = 10,
        routeesPaths = List("/user/instructionHandler"),
        allowLocalRoutees = true
      )
    ).props(),
    name = "instructionHandlerRouter")

  var ticks: Int = 0
  var refreshedSources: Int = 0
  val maxTicks: Int = Int.MaxValue - 1

  var scheduledSources = Array[(String, Int, String)]()

  override def preStart(): Unit = {
    log.info("Starting Scheduler")
  }

  def refreshSource(hash: String) = {
    instructionHandlerPool ! Instruction(hash)
    refreshedSources += 1
  }

  // Get sources that neeed to be refreshed
  def getEligibleSources(sources: Seq[(String, Int, String)], tick: Int) = {
    sources.groupBy(_._1).mapValues(_.toList.minBy(_._2)).values.filter(tick * 1000 % _._2 == 0).map(_._1)
  }

  def tick(): Unit = {
    ticks += 1
    log.debug("Scheduler TICK {}", ticks)
    val eligibleSources = getEligibleSources(scheduledSources, ticks)
    val chunks = eligibleSources.grouped(ConnectionPoolManager.connectionPoolSize).zipWithIndex.toList
    log.debug("Scheduling {} sources in {} chunks", eligibleSources.size, chunks.size)
    chunks.foreach({
      case(sources, index) =>
        after((index * 25 + 5) milliseconds, context.system.scheduler)(Future.successful {
          sources.foreach(refreshSource)
        })
    })
    if(ticks >= maxTicks) ticks = 0
  }
  timers.startPeriodicTimer("schedulerTickTimer", Tick, 990 milliseconds)
  timers.startPeriodicTimer("schedulerReportTimer", SchedulerReport, 10 seconds)

  def receive: Receive = {
    case AttachSource(hash, interval, socketId) =>
      scheduledSources.synchronized {
        scheduledSources = scheduledSources :+ ((hash, interval, socketId))
      }
    case DetachSource(socketId) =>
      scheduledSources.synchronized {
        scheduledSources = scheduledSources.filterNot(_._3 == socketId)
      }
    case SchedulerReport =>
      log.info("{} sources were scheduled since last report", refreshedSources)
      refreshedSources = 0
    case Tick => tick()
    case _ =>
  }
}

每个源都由一个哈希确定,该哈希包含执行所需的所有数据(例如数据库的主机),刷新间隔以及请求它的客户端的唯一ID,以便我们可以在客户端断开连接。 每一秒,我们通过应用带有 ticks 计数器当前值的模数来检查是否需要刷新源。 我们刷新较小块的源以避免连接池饥饿 问题是在小负载(~300 rq / s)下,滴答功能不再每秒执行

2 个答案:

答案 0 :(得分:1)

事实证明问题出在Rancher身上。 我们做了几次测试,应用程序在机器上直接运行,并在docker上正常运行,但在使用Rancher作为协调器时则没有。我不确定为什么,但因为它与Akka没有关系我关闭了这个问题。 谢谢大家的帮助。

答案 1 :(得分:-1)

可能瓶颈在于网络延迟?在您的机器中,所有组件并行运行,通信应该没有延迟,但在群集中,如果从一台计算机到另一台计算机进行大量数据库调用,网络延迟可能会很明显。