集群单例
对于某些使用场景,有时候需要强制确保集群中运行的某种类型的actor只有一个。
一些例子:
· 负责某些集群范围的一致性决定或者跨集群系统的动作协作的单点
· 外部系统的单个进入点
· 单个master,多个worker
· 集中化的命名服务,或者路由逻辑
使用单例不应该是首选的设计。它有一些缺点,例如单点瓶颈。单点失败也需要适当的考虑,但是对于一些场景,这个特性关心的是确保另一个单例最终会被启动。
集群单例模式由akka.cluster.singleton.ClusterSingletonManager实现。它管理所有集群节点或者特定角色节点上的单例actor。ClusterSingletonManager是在集群所有节点上或者特定角色节点上都应该启动的一个actor。实际的单例actor是由最老的节点上的ClusterSingletonManager启动的,从提供的Props创建一个子actor。ClusterSingletonManager确保某个时间点上任何点至多有一个单例运行。
单例actor总是运行在特定角色的最老的成员上。最老的成员由akka.cluster.Member#isOlderThan确定。当把这个成员从集群中移除时,最老的成员就会发生变化。我们要意识到在短暂的交接处理时间内没有活动的单例。
集群故障检测器会通知,当最老的节点由于某些事情不可达,比如JVM崩溃、硬关闭、网络故障灯。然后新的最老节点就会接管,新的单例actor被创建。对于这些失败场景,不存在优雅的交接,但是以合理的方式阻止超过一个的活动单例。一些极端情况最终用配置超时解决。
你可以使用提供的akka.cluster.singleton.ClusterSingletonProxy 访问单例actor,这个代理将会把所有的消息路由到当前的单例。这个代理会保持跟踪最老的节点,通过明确向单例的ActorSelection发送一个akka.actor.Identify消息来解析单例的ActorRef。如果单例在配置的特定时间内没有应答,那么句周期执行这个消息发送。考虑到实现,可能有一段时间ActorRef不可用,例如节点离开集群。在这些场景中,代理会将发送给单例的消息缓存起来,当单例最终可用时,再将消息传递给它们。当新的消息同代理发送时,如果缓存满了,那么ClusterSingletonProxy会丢弃老的消息。缓存的大小是可配置的,缓存大小设置为0可禁止缓存。
值得注意的是由于这些actor的分布式性质,消息总是可能会丢失的。一如既往的,在单例中需要实现额外的逻辑,在客户端actor要确保递送at-least-once 消息。
单例不会运行在WeaklyUp状态的成员上,如果这个特性是使能的话。
要意识到潜在的问题
这个模式可能一开始看起来很诱人,但是它有些缺点。下面列出一些:
· 集群单例会很快成为性能瓶颈
· 你不能依赖集群单例一直可用—例如,当单例运行的节点死了,它需要花费一些时间进行通知,单例会迁移到另一个节点上
· 在使用自动Downing导致的网络分离场景中,可能会发生这样的事情:分离的集群决定场景自己的单例,这意味着在系统中可能运行着多个单例。而集群无法找到它们(因为网络分离)
尤其只最后一个点是你应该关心的—通常,当使用集群单例模式时,你应该自己关闭downing节点,不要依赖于基于时间的auto-down特性。
警告:不要同时使用集群单例和自动Downing, 由于它允许集群分离成两个单独的集群,这就反过来导致多个单例被启动,每一个分离的集群中都有一个。
例子
假设我们需要一个外部系统的单个进入点。有一个actor接收来自JMX队列的消息,严格要求只有一个JMS消费者必须存在,确保消息按顺序处理。这可能不是你想要设计的东西,但是在通常的真实世界的场景中需要与外部系统交互。
在集群中的每一个节点上,你都需要启动一个ClusterSingletonManager,并提供单例actor的Props,在这个场景中就是JMS队列消费者consumer。
final ClusterSingletonManagerSettings settings =
ClusterSingletonManagerSettings.create(system).withRole("worker");
system.actorOf(ClusterSingletonManager.props(
Props.create(Consumer.class, queue, testActor),
new End(), settings), "consumer");
这里,我们限制单例节点的角色为"worker",但是所有的节点,角色独立的,可以不使用withRole指定。
这里在实际停止单例actor之前,我们使用应用特定的终止消息关闭资源。注意如果你只需要关闭actor,PoisonPill是极好的终止消息。
用上面给定的名字,访问单例可以从任意的集群节点用配置的代理获取:
ClusterSingletonProxySettings proxySettings =
ClusterSingletonProxySettings.create(system).withRole("worker");
system.actorOf(ClusterSingletonProxy.props("/user/consumer", proxySettings),
"consumerProxy");
更加复杂的例子可以在Lightbend Activator教程Akka和Java实现的分布式worker找到。
依赖
要使用集群单例,你必须在你的工程中添加如下的依赖:
sbt:
"com.typesafe.akka" %% "akka-cluster-tools" % "2.4.16"
maven:
<dependency>
<groupId>com.typesafe.akka</groupId>
<artifactId>akka-cluster-tools_2.11</artifactId>
<version>2.4.16</version>
</dependency>
配置
下面的配置属性由ClusterSingletonManagerSettings读取,当使用ActorSystem参数创建时。可以修改ClusterSingletonManagerSettings或者从相同层次的另一个配置区创建它。ClusterSingletonManagerSettings是ClusterSingletonManager.props工厂方法的一个参数,即每一个单例可以用不同的设置按需配置。
akka.cluster.singleton {
# The actor name of the child singleton actor.
singleton-name = "singleton"
# Singleton among the nodes tagged with specified role.
# If the role is not specified it's a singleton among all nodes in the cluster.
role = ""
# When a node is becoming oldest it sends hand-over request to previous oldest,
# that might be leaving the cluster. This is retried with this interval until
# the previous oldest confirms that the hand over has started or the previous
# oldest member is removed from the cluster (+ akka.cluster.down-removal-margin).
hand-over-retry-interval = 1s
# The number of retries are derived from hand-over-retry-interval and
# akka.cluster.down-removal-margin (or ClusterSingletonManagerSettings.removalMargin),
# but it will never be less than this property.
min-number-of-hand-over-retries = 10
}
下面的配置属性由ClusterSingletonProxySettings读取,当用ActorSystem参数创建它时。可以修改ClusterSingletonProxySettings或者从相同层次的另一个配置区创建它。ClusterSingletonProxySettings是ClusterSingletonProxy.props工厂方法的一个参数,即每一个单例代理可以用你不同的设置按需配置。
akka.cluster.singleton-proxy {
# The actor name of the singleton actor that is started by the ClusterSingletonManager
singleton-name = ${akka.cluster.singleton.singleton-name}
# The role of the cluster nodes where the singleton can be deployed.
# If the role is not specified then any node will do.
role = ""
# Interval at which the proxy will try to resolve the singleton instance.
singleton-identification-interval = 1s
# If the location of the singleton is unknown the proxy will buffer this
# number of messages and deliver them when the singleton is identified.
# When the buffer is full old messages will be dropped when new messages are
# sent via the proxy.
# Use 0 to disable buffering, i.e. messages will be dropped immediately if
# the location of the singleton is unknown.
# Maximum allowed buffer size is 10000.
buffer-size = 1000
}