java akka 远程
假设我们的测试应用程序取得了巨大的成功,并且一台服务器逐渐无法处理不断增长的流量。 我们面临两种选择:用更好的服务器替换服务器(向上扩展)或购买第二台服务器并构建集群(向外扩展)。 我们选择构建集群是因为将来更容易扩展。 但是,我们很快发现我们的应用程序不再满足第一个要求:
客户端应用程序最多应从一个线程调用URL […] –禁止使用多个HTTP连接同时获取随机数。
显然,集群中的每个节点都是独立的,具有自己的独立Akka实例,因此是RandomOrgClient
actor的独立副本。 为了解决此问题,我们有几种选择:
- 具有全局(群集范围!)锁(分布式监视器,信号灯等),以保护多线程访问。 普通
synchronized
是不够的。 - …或在集群中创建一个专用节点以与random.org通信,所有其他节点通过某个API使用该节点
- …或仅在一个节点上仅创建
RandomOrgClient
一个实例,并通过某些API(RMI,JMS…)将其公开给远程客户端
您还记得花了多少时间来描述Actor
和ActorRef
之间的区别吗? 现在,这种区别将变得显而易见。 事实证明,我们的解决方案将基于最后的建议,但是我们不必担心API,序列化,通信或传输层。 更好的是,Akka中没有此类API可以处理远程角色。 在配置文件中足以说明这一点:假定仅在此节点上创建此特定参与者。 所有其他节点(而不是在本地创建相同的actor)将返回一个特殊的代理,该代理从外部看起来像是正常的actor,而实际上它将所有消息转发到其他节点上的远程actor。
再说一遍:我们无需更改代码中的任何内容,这足以在配置文件中进行一些调整:
akka {
actor {
provider = "akka.remote.RemoteActorRefProvider"
deployment {
/buffer/client {
remote = "akka://RandomOrgSystem@127.0.0.1:2552"
}
}
}
remote {
transport = "akka.remote.netty.NettyRemoteTransport"
log-sent-messages = on
netty {
hostname = "127.0.0.1"
}
}
}
而已! 集群中的每个节点都由服务器地址和端口标识。 配置的关键部分是声明/buffer/client
仅创建127.0.0.1:2552
。 每隔一个实例(在不同的服务器或端口上工作),而不是创建actor的新副本,将构建一个特殊的透明代理,以调用远程服务器。
如果您不记得我们解决方案的体系结构,则下图演示了消息流。 如您所见,每个节点都有自己的RandomOrgBuffer
副本(否则,对缓冲区的每次访问都将导致远程调用,这完全破坏了缓冲区的目的)。 但是,每个节点(中间的节点除外)都有对RandomOrgClient
actor的远程引用(中间的节点在本地访问此actor):
中间的机器(JVM 1)在端口2552上执行,它是唯一与random.org
通信的random.org
。 其他所有服务器(分别在2553和2554上运行的JVM 2和3)都通过JVM 1与该服务器间接通信。顺便说一句,我们可以使用配置文件或-akka.remote.netty.port=2553
更改每个节点使用的TCP / IP端口。 -akka.remote.netty.port=2553
Java属性。
在再次宣布过早的成功之前,我们面临另一个问题。 或者实际上,我们还没有真正克服最初的障碍。 由于RandomOrgClient
现在由多个访问RandomBuffer
参与者(跨集群分布),但它仍然可以启动到random.org多个并发的HTTP连接,代表在集群中的每个节点的。 容易想到这样的情况,其中多个RandomOrgBuffer
实例同时发送FetchFromRandomOrg
消息,开始多个并发HTTP连接。 为了避免这种情况,如果一个请求尚未完成,我们在actor中实现了一种对请求进行排队的技术 :
case class FetchFromRandomOrg(batchSize: Int)
case class RandomOrgServerResponse(randomNumbers: List[Int])
class RandomOrgClient extends Actor {
val client = new AsyncHttpClient()
val waitingForReply = new mutable.Queue[(ActorRef, Int)]
override def postStop() {
client.close()
}
def receive = LoggingReceive {
case FetchFromRandomOrg(batchSize) =>
waitingForReply += (sender -> batchSize)
if (waitingForReply.tail.isEmpty) {
sendHttpRequest(batchSize)
}
case response: RandomOrgServerResponse =>
waitingForReply.dequeue()._1 ! response
if (!waitingForReply.isEmpty) {
sendHttpRequest(waitingForReply.front._2)
}
}
private def sendHttpRequest(batchSize: Int) {
val url = "https://www.random.org/integers/?num=" + batchSize + "&min=0&max=65535&col=1&base=10&format=plain&rnd=new"
client.prepareGet(url).execute(new RandomOrgResponseHandler(self))
}
}
private class RandomOrgResponseHandler(notifyActor: ActorRef) extends AsyncCompletionHandler[Unit]() {
def onCompleted(response: Response) {
val numbers = response.getResponseBody.lines.map(_.toInt).toList
notifyActor ! RandomOrgServerResponse(numbers)
}
}
这次要注意waitingForReply
队列。 当请求从远程Web服务获取随机数时,要么我们发起新的连接(如果队列中除了我们之外不包含任何人)。 如果有其他的演员等候,我们必须有礼貌地把自己放在队列,记住谁要求多少个号码( waitingForReply
+ =( sender
->
batchSize
))。 收到答复后,我们将从队列中的第一个演员(等待时间最长的演员)中取出,并代表他发起另一个请求。
毫不奇怪,没有multithreading
或同步代码。 但是,重要的是不要通过在receive方法之外访问其状态来破坏封装。 我通过读取onCompleted()
方法内的waitingForReply
队列犯了这个错误。 因为此方法是由HTTP客户端工作线程异步调用的,所以潜在地我们可以同时从两个线程访问actor状态(如果它正在同时处理接收中的某些消息)。 这就是为什么我决定将HTTP回复回调提取到一个单独的类中,而不具有对actor的隐式访问的原因。 这更加安全,因为通过编译器本身可以保护对参与者状态的访问。
这是“发现Akka”系列的最后一部分。 请记住,完整的源代码可在GitHub上找到 。
参考: 远程演员–在Java和邻里博客中,从我们的JCG合作伙伴 Tomasz Nurkiewicz 发现Akka 。
翻译自: https://www.javacodegeeks.com/2012/11/remote-actors-discovering-akka.html
java akka 远程