阿卡接口
希望到目前为止您玩得开心,但我们的应用程序存在严重的性能缺陷。 在测量了我们在上一部分中开发的RandomOrgRandom
类的响应时间之后,我们会注意到一些确实令人不安的东西(该图表表示后续调用的响应时间(以毫秒为单位)):
事实证明,定期响应时间(返回一个随机数的时间)要大几个数量级(对数刻度!)。记住参与者是如何实现的,原因很明显:我们的参与者急切地获取50个随机数并填充缓冲区,返回一个数字。 如果缓冲区为空,则actor将执行对random.org
阻塞I / O调用,该过程大约需要半秒。 这在图表上清晰可见-每第50次调用要慢得多。 在某些情况下,这种行为是可以接受的(就像不可预测的垃圾收集会偶尔增加响应的延迟)。 但是,仍然让我们尝试改善我们的实现。
您是否知道我们如何使响应时间更可预测和更平坦 ? 我建议监视缓冲区大小,当它危险地接近于空时,我们开始在后台获取随机数。 我们希望,由于这种体系结构上的改变将永远不会完全是空的,因为在我们触底之前,背景响应就会到来。 但是,我们并没有被迫急于获取太多的随机数。 您还记得使用Java同步的原始天真的实现吗? 它与我们当前只有一个参与者的系统存在相同的问题。 但是现在,我们终于可以看到基于演员的Akka系统的真正威力。 为了实现上面建议的改进,我们将需要后台线程来获取数字以及在缓冲区上进行某种同步的方法(因为它将被同时访问。
在Akka中,每个参与者在逻辑上都是单线程的,因此我们不会被同步所困扰。 首先,我们将划分职责 :一个RandomOrgBuffer
( RandomOrgBuffer
)将保存随机数的缓冲区,并在请求时返回它们。 第二演员( RandomOrgClient
)几乎不负责获取新的随机数批次。 当RandomOrgBuffer
发现较低的缓冲区级别时,它要求RandomOrgClient
(通过发送消息)开始获取新的随机数。 最终,当RandomOrgClient
从random.org
服务器收到响应时,它将新一批的随机数发送到RandomOrgBuffer
(当然,再次使用回复消息)。 RandomOrgBuffer
用新数字填充缓冲区。
让我们从第二个角色开始,在后台与random.org
Web服务进行通信。 当该FetchFromRandomOrg
收到FetchFromRandomOrg
消息时,它将发起HTTP请求-消息包含要提取的所需批处理大小。 当响应到达时,我们解析它并将整个批处理发送回发送方(在这种情况下为RandomOrgBuffer
)。 这段代码非常类似于我们在fetchRandomNumbers():
看到的fetchRandomNumbers():
case class FetchFromRandomOrg(batchSize: Int)
case class RandomOrgServerResponse(randomNumbers: List[Int])
class RandomOrgClient extends Actor {
protected def receive = LoggingReceive {
case FetchFromRandomOrg(batchSize) =>
val url = new URL("https://www.random.org/integers/?num=" + batchSize + "&min=0&max=65535&col=1&base=10&format=plain&rnd=new")
val connection = url.openConnection()
val stream = Source.fromInputStream(connection.getInputStream)
sender ! RandomOrgServerResponse(stream.getLines().map(_.toInt).toList)
stream.close()
}
}
现在是时候让实际的actor处理来自潜在多个客户端的请求单个随机数的请求了。 不幸的是,逻辑变得非常复杂(但至少我们不必担心同步和线程安全性)。 首先, RandomOrgBuffer
现在必须处理两种不同的消息:之前来自客户端代码的RandomRequest
和包含从RandomOrgClient
发送的一批新随机数的新RandomOrgServerResponse
(请参见上面的代码)。 其次, RandomOrgBuffer
必须记住,它通过发送FetchFromRandomOrg
来启动获取新随机码的FetchFromRandomOrg
。 否则,我们将冒着启动与random.org
多个并发连接或不必要地堆积它们的风险。
class RandomOrgBuffer extends Actor with ActorLogging {
val BatchSize = 50
val buffer = new Queue[Int]
var waitingForResponse = false
val randomOrgClient = context.actorOf(Props[RandomOrgClient], name="client")
preFetchIfAlmostEmpty()
def receive = {
case RandomRequest =>
preFetchIfAlmostEmpty()
sender ! buffer.dequeue()
case RandomOrgServerResponse(randomNumbers) =>
buffer ++= randomNumbers
waitingForResponse = false
}
private def preFetchIfAlmostEmpty() {
if(buffer.size <= BatchSize / 4 && !waitingForResponse) {
randomOrgClient ! FetchFromRandomOrg(BatchSize)
waitingForResponse = true
}
}
}
关键部分是preFetchIfAlmostEmpty()
方法,该方法启动获取随机数的过程(在后台,由其他preFetchIfAlmostEmpty()
执行)。 如果缓冲区级别太低(我假设为批处理大小的1/4),并且我们尚未等待来自random.org
的响应, random.org
适当的消息发送给RandomOrgClient
。 我们还将在启动时(当缓冲区完全为空时)立即调用此方法,以在第一个请求到达时预热缓冲区。 注意,一个actor如何通过调用context.actorOf
(实际上是ActorRef
)来创建另一个actor的实例。
此代码包含巨大的错误,您可以发现它吗? 想象一下,如果RandomOrgBuffer
突然RandomOrgBuffer
接收数百条RandomRequest
消息会发生什么? 是否超过缓冲区大小,甚至在填满后立即使用? 或者,如果请求在actor实例化之后直接发出,而缓冲区仍然为空? 不幸的是,当RandomRequest
到达并且我们的缓冲区完全为空,而random.org
响应尚未返回时,我必须处理这种情况。 但是,我们不能简单地阻止,等到带有新随机数批的响应到达。 我们不能从一个简单的原因中做到这一点:如果在处理RandomRequest
时我们正在睡觉或以其他方式等待,我们将无法处理任何其他消息,包括RandomOrgServerResponse
–请记住,一个RandomOrgServerResponse
一次只能处理一条消息! 不仅我们会在应用程序中引入死锁,而且还会打破Akka中最重要的规则之一:永远不要在演员体内阻塞或睡觉-在下一篇文章中会详细介绍。
处理这种情况的正确方法是让等待的参与者排队-向我们发送了RandomRequest
但由于缓冲区为空,我们无法立即发送回复。 一旦我们获得RandomOrgServerResponse
,我们的优先任务就是处理这些等待的参与者,然后实时处理后续请求。 有一个有趣的FetchFromRandomOrg
情况–如果等待的actor数量如此之大,以致于在满足他们的请求之后,缓冲区几乎又变空了,我们将立即再次发送FetchFromRandomOrg
。 它可能会变得更加有趣–想象一下缓冲区是空的,而我们仍然没有满足所有未决的参与者。 以下代码处理了所有这些扭曲:
class RandomOrgBuffer extends Actor with ActorLogging {
val BatchSize = 50
val buffer = new Queue[Int]
val backlog = new Queue[ActorRef]
var waitingForResponse = false
//...
def receive = LoggingReceive {
case RandomRequest =>
preFetchIfAlmostEmpty()
if(buffer.isEmpty) {
backlog += sender
} else {
sender ! buffer.dequeue()
}
case RandomOrgServerResponse(randomNumbers) =>
buffer ++= randomNumbers
waitingForResponse = false
while(!backlog.isEmpty && !buffer.isEmpty) {
backlog.dequeue() ! buffer.dequeue()
}
preFetchIfAlmostEmpty()
}
}
在我们的最终版本中,积压队列代表未决的参与者。 请注意RandomOrgServerResponse
的处理方式:如果首先尝试满足尽可能多的未决参与者。 显然,我们的目标是消除非常大的响应时间,因此我们应努力减少待办事项队列的使用,因为放置在此的每个参与者都更有可能等待更长的时间。 在理想情况下,积压队列始终为空,并且RandomOrgBuffer
根据当前负载每次调整请求的批处理大小(获取较小或较大的批处理以及调整阈值,在该阈值以下,缓冲区几乎为空)。 但是,我将这些改进留给您。 最后,我们可以测量RandomOrgBuffer
响应时间(线性标度, latencies
不再如此不均匀地分布):
在RandomOrgBuffer
处理传入消息变得相当复杂。 我特别担心waitingForResponse
标志(我讨厌标志!)。在下一篇文章中,我们将学习如何以非常清晰,惯用和面向对象的方式处理此问题。
这是我最初在scala.net.pl上发表的文章“ Poznajemy Akka:dwoch aktorow ”的译文 。
参考: 两位演员–在Java和社区博客上从我们的JCG合作伙伴 Tomasz Nurkiewicz 发现Akka 。
翻译自: https://www.javacodegeeks.com/2012/11/two-actors-discovering-akka.html
阿卡接口