在reactmq的最后两篇文章中,我描述了如何编写反应式 , 持久性消息队列。 队列具有以下特征:
- 有一个存储消息的代理,可能有多个客户端发送或接收消息
- 提供至少一次交付; 接收消息会阻止该消息; 除非它在一段时间内被删除,否则将可以再次交付
- 由于akka-streams,发送和接收消息是被动的:如果任何组件无法跟上负载,就会施加反压
- 使用akka- persistence,消息是持久的:所有发送/接收/删除事件都保存在日志中,并且当系统重新启动时,将重新创建队列参与者的状态
可以使用复制的日记实现(例如Cassandra)复制事件存储。 但是,缺少一件:将代理本身集群化。
幸运的是, akka集群可以为您提供帮助! 让我们看看如何使用它,以确保集群中始终存在一个代理。
集群设置
首先,我们需要一个工作集群。 我们将在build.sbt
文件中需要两个新依赖build.sbt
, akka-cluster
和akka-contrib
,因为我们将使用一些akka-contrib
扩展。
其次,我们需要为ActorSystem
提供启用集群的配置。 最好弄清楚配置文件的用途,因此有cluster-broker-template.conf
文件 :
akka {
actor.provider = "akka.cluster.ClusterActorRefProvider"
remote.netty.tcp.port = 0 // should be overriden
remote.netty.tcp.hostname = "127.0.0.1"
cluster {
seed-nodes = [
"akka.tcp://broker@127.0.0.1:9171",
"akka.tcp://broker@127.0.0.1:9172",
"akka.tcp://broker@127.0.0.1:9173"
]
auto-down-unreachable-after = 10s
roles = [ "broker" ]
role.broker.min-nr-of-members = 2
}
extensions = [ "akka.contrib.pattern.ClusterReceptionistExtension" ]
}
进行设置:
- 我们需要使用
ClusterActorRefProvider
与其他集群节点上的ClusterActorRefProvider
进行通信 - 因为我们将对多个节点使用相同的配置文件(用于本地测试),所以该端口将被代码覆盖。 除了端口之外,我们还需要指定群集通信将绑定到的主机名。
- 该节点需要种子节点的初始列表,它将在启动时尝试与之通信以形成集群
- 我们正在使用自动
down
,这会导致在10秒后声明节点down
。 这可能会导致分区 ,但是我们还指定至少需要有2个活动的节点才能使群集片段正常运行(总共将有3个节点,因此对于2个分区,我们可以安全地进行分区) - 最后,我们声明使用配置文件启动的所有群集节点都将具有
broker
角色,并使用群集接收程序扩展名-但稍后会介绍更多。
现在实际上启动集群BrokerManager
系统非常简单,请参见BrokerManager
类 :
class BrokerManager(clusterPort: Int) {
// …
val conf = ConfigFactory
.parseString(s"akka.remote.netty.tcp.port=$clusterPort")
.withFallback(ConfigFactory.load("cluster-broker-template"))
val system = ActorSystem(s"broker", conf)
// ...
}
要指定端口,我们从仅指定端口部分的字符串中创建配置,并使用其他设置,我们从模板文件退回到配置。
启动集群节点只是启动三个简单应用程序的问题 :
object ClusteredBroker1 extends App {
new BrokerManager(9171).run()
}
集群单例
准备好集群之后,我们现在必须在一个节点上启动代理。 在任何时候,都应该只运行一个代理,否则队列将被破坏。 为此, Cluster Singleton contrib扩展是完美的。
要使用扩展,我们需要创建一个actor,该角色将由单例扩展管理,并且仅在一个群集节点上启动。 因此,我们创建了BrokerManagerActor
,现在可以启动单例了 :
def run() {
// …
system.actorOf(ClusterSingletonManager.props(
singletonProps = Props(classOf[BrokerManagerActor], clusterPort),
singletonName = "broker",
terminationMessage = PoisonPill,
role = Some("broker")),
name = "broker-manager")
}
class BrokerManagerActor(clusterPort: Int) extends Actor {
val sendServerAddress = new InetSocketAddress(
"localhost", clusterPort + 10)
val receiveServerAddress = new InetSocketAddress(
"localhost", clusterPort + 20)
override def preStart() = {
super.preStart()
new Broker(sendServerAddress, receiveServerAddress)(context.system)
.run()
}
override def receive = { case _ => }
}
在ClusterSingletonManager
属性中,我们指定要运行的actor,可用于终止actor的消息,以及可以运行actor的集群角色(我们集群的节点只有一个角色broker
)。
BrokerManagerActor
占用一个端口(如果我们要在本地主机上运行一对,则每个节点都应该是唯一的),并基于该端口创建一个地址,新队列消息发送客户端的套接字将在该地址上侦听,而另一个队列消息接收客户端侦听套接字。
群集客户和接待员:群集端
现在,我们在集群中运行了一个代理,但是客户如何知道单例的地址是什么? 好吧,我们只需要BrokerManagerActor
请求BrokerManagerActor
演员即可! 这可以通过简单的消息交换来完成:
case object GetBrokerAddresses
case class BrokerAddresses(sendServerAddress: InetSocketAddress,
receiveServerAddress: InetSocketAddress)
class BrokerManagerActor(clusterPort: Int) extends Actor {
// as above, plus:
override def receive = {
case GetBrokerAddresses => sender() ! BrokerAddresses(
sendServerAddress, receiveServerAddress)
}
}
但是,仍然存在一个问题。 想要使用我们的消息队列的客户端不必是集群的成员。 在这里, Cluster Client contrib扩展可以提供帮助。
在集群节点一侧,客户端扩展提供了一个接待员,希望在外部可见的actor可以向该接待员注册。 这就是我们的BrokerManagerActor
在启动时所做的 :
class BrokerManagerActor(clusterPort: Int) extends Actor {
// …
override def preStart() = {
// ...
ClusterReceptionistExtension(context.system)
.registerService(self)
}
}
集群客户端和接待员:客户端
至于客户端本身,它们还需要一些配置才能与集群通信:
akka {
actor.provider = "akka.remote.RemoteActorRefProvider"
remote.netty.tcp.port = 0
remote.netty.tcp.hostname = "127.0.0.1"
}
cluster.client.initial-contact-points = [
"akka.tcp://broker@127.0.0.1:9171",
"akka.tcp://broker@127.0.0.1:9172",
"akka.tcp://broker@127.0.0.1:9173"
]
再次进行设置:
- 与远程参与者(其中居住在集群中)进行交流,我们需要使用
RemoteActorRefProvider
(该ClusterARP
是更丰富的版本RemoteARP
) - 与种子节点类似,我们需要提供种子接触点,以便客户端可以通过某种方式启动与集群的通信
实际上,启动集群客户端非常简单 ,我们需要创建一个actor系统并创建一个与集群通信的actor:
val conf = ConfigFactory.load("cluster-client")
implicit val system = ActorSystem(name, conf)
val initialContacts = conf
.getStringList("cluster.client.initial-contact-points")
.asScala.map {
case AddressFromURIString(addr) => system.actorSelection(
RootActorPath(addr) / "user" / "receptionist")
}.toSet
val clusterClient = system.actorOf(
ClusterClient.props(initialContacts), "cluster-client")
要启动消息队列的客户端(我们有两种类型的客户端:一种将消息发送到队列,另一种从队列接收消息),我们需要找出代理的地址是什么。 为此,我们(使用Akka的询问模式)询问在接待员处注册的经纪人其地址:
clusterClient ? ClusterClient.Send(
"/user/broker-manager/broker",
GetBrokerAddresses,
localAffinity = false)
.mapTo[BrokerAddresses]
.flatMap { ba =>
logger.info(s"Connecting a $name using broker address $ba.")
runClient(ba, system)
}
最后,当客户端流完成时(例如,由于代理关闭),我们尝试在1秒后重新启动它 。 在这里,一些指数退避机制可能很有用。
用于运行队列消息发送方和接收方的可运行应用程序使用与单节点代码相同的代码,不同之处在于从集群获取代理地址:
object ClusterReceiver extends App with ClusterClientSupport {
start("receiver", (ba, system) =>
new Receiver(ba.receiveServerAddress)(system).run())
}
跑步
如果尝试运行ClusterReceiver
(任意数量), ClusterSender
(任意数量), ClusteredBroker1
, ClusteredBroker2
和ClusteredBroker3
,则会看到消息从发件人通过单个运行的代理流向收件人。 您可以杀死代理节点,然后在几秒钟后在另一个群集节点上启动另一个代理节点,发送者/接收者将重新连接。
对于我们编写的少量代码,我会说这很好!
加起来
我们的消息队列现在是:
- 反应式的,使用akka流
- 持久性,使用akka-persistence
- 使用akka群集进行群集
最好的方面是,在代码中,我们不必处理背压处理,将消息存储到磁盘或在集群中进行通信并达成共识的任何细节。 现在,我们有了一个真正的反应性应用程序。
感谢Akka团队的Endre帮助我们为流添加了错误处理 。 整个代码可在GitHub上找到 。 请享用!
翻译自: https://www.javacodegeeks.com/2014/11/clustering-reactmq-with-akka-cluster.html