我正在开发一个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容器中运行。
我理解群集上的单身人员可能会成为瓶颈,但由于它只是将消息转发给其他参与者,所以我不知道它是如何阻止的。
我尝试了什么
任何人在本地计算机和生产服务器之间都遇到过这样的差异吗?
编辑 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)下,滴答功能不再每秒执行
答案 0 :(得分:1)
事实证明问题出在Rancher身上。 我们做了几次测试,应用程序在机器上直接运行,并在docker上正常运行,但在使用Rancher作为协调器时则没有。我不确定为什么,但因为它与Akka没有关系我关闭了这个问题。 谢谢大家的帮助。
答案 1 :(得分:-1)
可能瓶颈在于网络延迟?在您的机器中,所有组件并行运行,通信应该没有延迟,但在群集中,如果从一台计算机到另一台计算机进行大量数据库调用,网络延迟可能会很明显。