集群用法
关于Akka集群概念的介绍请参阅集群规范。
为你的工程准备集群
Akka cluster是一个独立的jar文件。确保你的工程具有如下的依赖:
<dependency>
<groupId>com.typesafe.akka</groupId>
<artifactId>akka-cluster_2.11</artifactId>
<version>2.4.16</version>
</dependency>
简单的集群示例
下面的配置使能了集群扩展。它加入集群,actor订阅集群membership事件并记录到日志。
application.conf配置看起来是这样的:
akka {
actor {
provider = "cluster"
}
remote {
log-remote-lifecycle-events = off
netty.tcp {
hostname = "127.0.0.1"
port = 0
}
}
cluster {
seed-nodes = [
"akka.tcp://ClusterSystem@127.0.0.1:2551",
"akka.tcp://ClusterSystem@127.0.0.1:2552"]
# auto downing is NOT safe for production deployments.
# you may want to use it during development, read more about it in the docs.
#
# auto-down-unreachable-after = 10s
}
}
# Disable legacy metrics in akka-cluster.
akka.cluster.metrics.enabled=off
# Enable metrics extension in akka-cluster-metrics.
akka.extensions=["akka.cluster.metrics.ClusterMetricsExtension"]
# Sigar native library extract location during tests.
# Note、: use per-jvm-instance folder when running multiple jvm on one host.
akka.cluster.metrics.native-library-extract-folder=${user.dir}/target/native
为了在你的Akka工程中使能集群能力,你应该至少添加Remoting设置,除了cluster。akka.cluster.seed-nodes通常也应该加入到application.conf文件中。
注意
如果你在Docker容器里运行Akka,或者因为某种原因节点有独立的内部和外部IP,你就必须配置远程地址,根据Akka behind NAT 或者in a Docker container
将种子节点配置为初始化自动加入集群的联络点。
注意如果你想要在不同的机器上启动节点,你必须在application.conf中指定IP地址或者主机名,而不是127.0.0.1。
使用集群扩展的actor看起来是这样的:
package sample.cluster.simple;
import akka.actor.UntypedActor;
import akka.cluster.Cluster;
import akka.cluster.ClusterEvent;
import akka.cluster.ClusterEvent.MemberEvent;
import akka.cluster.ClusterEvent.MemberRemoved;
import akka.cluster.ClusterEvent.MemberUp;
import akka.cluster.ClusterEvent.UnreachableMember;
import akka.event.Logging;
import akka.event.LoggingAdapter;
public class SimpleClusterListener extends UntypedActor {
LoggingAdapter log = Logging.getLogger(getContext().system(), this);
Cluster cluster = Cluster.get(getContext().system());
// subscribe to cluster changes
@Override
publicvoid preStart() {
// #subscribe
cluster.subscribe(getSelf(), ClusterEvent.initialStateAsEvents(), MemberEvent.class, UnreachableMember.class);
// #subscribe
}
// re-subscribe when restart
@Override
publicvoid postStop() {
cluster.unsubscribe(getSelf());
}
@Override
publicvoid onReceive(Object message) {
if (messageinstanceof MemberUp) {
MemberUp mUp = (MemberUp) message;
log.info("Member is Up: {}", mUp.member());
} elseif (messageinstanceof UnreachableMember) {
UnreachableMember mUnreachable = (UnreachableMember) message;
log.info("Member detected as unreachable: {}", mUnreachable.member());
} elseif (messageinstanceof MemberRemoved) {
MemberRemoved mRemoved = (MemberRemoved) message;
log.info("Member is Removed: {}", mRemoved.member());
} elseif (messageinstanceof MemberEvent) {
// ignore
} else {
unhandled(message);
}
}
}
这个
actor
将自己注册为某些集群事件的订阅者。当订阅启动时,它会收到对应集群状态的事件,然后它会收到表示集群中发生变化的事件。
运行这个示例的最容易的方式是下载Lightbend Activator,打开教程Akka Cluster Samples with Java。它包含一些如何运行SimpleClusterApp的指导。
加入种子节点
是否加入集群应该手动或者自动完成,配置的初始联络点,即所谓的种子节点。当一个新节点启动时,它会向所有的种子节点发送一个消息,然后向第一个应答的种子节点发送join命令。如果没有任何种子节点应答(可能还没有启动),它会重试这个过程,直到成功或者关闭。
在配置文件中定义种子节点(application.conf):
akka.cluster.seed-nodes = [
"akka.tcp://ClusterSystem@host1:2552",
"akka.tcp://ClusterSystem@host2:2552"]
也可以使用Java系统属性定义种子节点,当启动JVM时,使用下面的语法:
-Dakka.cluster.seed-nodes.0=akka.tcp://ClusterSystem@host1:2552
-Dakka.cluster.seed-nodes.1=akka.tcp://ClusterSystem@host2:2552
种子节点可能以让任意顺序启动,也没有让所有的种子节点都运行起来,但是配作为种子节点配置列表中第一个元素的节点必须在初始化启动一个集群时启动,否则其它的种子节点将不会初始化,其它节点也无法加入集群。第一个节点之所以特殊是为了避免当从一个空集群启动时形成分离岛。同时启动所有配置的种子节点是最快的(顺序没有关系),否则就会以配置的seed-node-timeout时间超时,知道节点可以加入。
一旦两个以上的种子节点启动起来了,name关闭第一个种子节点就没有问题了。如果第一个种子节点重启了,它会首先尝试加入集群中的其它种子节点。
如果你没有配置种子节点,你需要手动或者以编程方式加入集群。
手动加入集群可以通过ref:cluster_jmx_java或者 Command Line Management。以编程方式加入可以通过Cluster.get(system).join。没有成功的加入会在配置的超时时间(retry-unsuccessful-join-after)之后进行自动重试。重试可以通过将这个属性设置为off来禁止。
你可以加入集群中的任何节点。它不一定必须为种子节点。注意你只可以加入已经存在的集群成员,这意味着引导节点必须将自己加入,然后后面的节点可以加入它们形成集群。
你也可以使用Cluster.get(system).joinSeedNodes以编程方式加入,当在启动时使用外部工具或者API动态发现其它的节点时,这是很有用的。当使用joinSeedNodes时,你不应该包含节点自己,除了第一个种子节点,而且应该作为joinSeedNodes的第一个参数。
联络种子节点的不成功尝试将会在超时(由属性seed-node-timeout配置)之后自动重试。加入特定种子节点的不成功尝试将会在超时(由属性retry-unsuccessful-join-after配置)之后自动重试。重试意味着它将尝试与所有的种子节点联络,并加入第一个应答的节点。种子节点列表中的第一个节点会将自己加入,如果它无法在配置的seed-node-timeout超时时间内联系到其它的任何种子节点。
一个ActorSystem只能加入集群一次。过多的尝试将会被忽略。当它成功加入时,它要想加入另一个集群或者想要再次加入这个集群,那么它必须重启。重启后,它可以使用相同的主机名和端口,此时它会变为集群中已有成员的化身,尝试加入,然后已有的化身从集群中移除,它又会被允许加入。
注意
ActorSystem的名字对于集群中的所有成员必须是相同的。这个名字是在启动ActorSystem时给定的。
Downing
当一个成员被故障检测器认为不可达,那么leader就不被允许执行它的任务,例如将新加入成员的状态改为Up。这个阶段必须首先再次变得不可达,或者不可达的成员的状态被修改为Down。将状态修改为Down可以自动执行也可以手动执行。默认情况下,它必须手动执行,使用JMX或者命令行管理。
也可以以编程方式执行:Cluster.get(system).down(address)。
一个预先打包好的Downing问题解决方案A是由Split Brain Resolver提供的,这属于Lightbend Reactive Platform的一部分。如果你不使用RP,你应该认真阅读一下Split Brain Resolver的文档,确保你使用的解决方案能够处理上面关系的问题。
Auto-downing (不要使用)
在生产环境中,你不应该使用自动downing特性。若是为了测试,你可以通过配置使能它:
akka.cluster.auto-down-unreachable-after = 120s
这意味着集群的leader成员将会在配置的古可达超时时间之后,将不可达的节点的状态修改为down。
有一个很幼稚的方法将不可达节点从集群成员关系中移除。这种方法对于崩溃和短期过度的网络分离工作的很好,但对于长期的网络分离工作的不好。
网络分离的两边都认为对方为不可达,一段时间后就将它们从集群成员关系中移除。由于这会在两边发生,结果是创建了两个没有连接的分离的集群。这也可能发生在长期的GC暂停或者系统过载。
警告
我们推荐在生产环境中使用Akka集群的auto-down特性。如果你使用集群单例(Singleton)或者集群分片(Sharding),这对于正确的行为是很关键的,尤其是和Akka持久化一起使用时。Akka持久化和集群分片一起使用如果发生网络分离可能会导致数据损坏。
Leaving
有两种方式可以将成员从集群中移除:
你可以停止actor system (或者JVM进程)。在自动或手动downing后,它会被检测为不可达并被移除。
可以实现更加优雅的退出,你需要告诉集群某个节点要离开了。这可以通过JMX或者命令行实现。也可以以编程方式实现:Cluster.get(system).leave(address)。
注意这个命令可以向集群中的任何成员发出,不必非得是要离开的成员。即将离开成员所在的集群扩展,不是ActorSystem或者JVM,将会在leader将该成员状态修改为之后被关闭。因此该成员将会从集群中移除。通常这是自动完成的,但是如果在这个处理过程中发生网络失败,依然有必要为了完成移除将节点的状态设置为Down。
订阅集群事件
你可以订阅集群成员关系变化通知:
cluster.subscribe(getSelf(), MemberEvent.class, UnreachableMember.class);
完整状态akka.cluster.ClusterEvent.CurrentClusterState的快照会被作为第一个消息发送给订阅者,后面的事件都是增量更新。
注意,如果你在初始加入程序完成前启动了订阅,你可能会收到一个空的CurrentClusterState,不包含任何成员。这是期望的行为。当成员被集群接受时,你会收到这个节点或其它节点的MemberUp消息。
如果你不方便处理CurrentClusterState,你可以使用ClusterEvent.initialStateAsEvents()作为订阅参数。这意味着,你不会收到第一个消息CurrentClusterState,而是收到对应当前状态的事件,模仿你监听事件时发生的行为。注意,那些初始化事件对应于当前状态,它不是集群中实际发生的变化的完整历史。
cluster.subscribe(getSelf(), ClusterEvent.initialStateAsEvents(),
MemberEvent.class, UnreachableMember.class);
追踪成员生命周期的事件是:
· ClusterEvent.MemberUp:新成员加入集群,且它的状态被改为Up。
· ClusterEvent.MemberExited:成员只在离开集群,它的状态已经被修改为Exiting。注意这个节点可能已经被关闭了,当这个事件发布给另一个节点时。
· ClusterEvent.MemberRemoved:成员完全离开了集群。
· ClusterEvent.UnreachableMember:被至少一个其它的节点的故障检测器检测到该成员不可达。
· ClusterEvent.ReachableMember:成员在变得不可达后,被认为再次可达。所有之前检测到它为不可达的节点都再次检测到它可达。
还有更多变化事件,查阅继承akka.cluster.ClusterEvent.ClusterDomainEvent类的类的API文档,以了解更多事件的细节。
有时候只获取完整的成员关系状态(用Cluster.get(system).state())更方便,而不是订阅集群事件。注意,这个状态不必与发布给集群订阅的事件同步。
Worker Dial-in Example
让我们看一个例子,这个例子说明worker(这里称为后端)如何检测和注册新的master节点(这里称为前端)。
这个示例应用提供了一个服务用于转换文本。当一些文本被发送给一个前端服务时,它会被代理给一个后端的worker,这个workder执行转换任务,并将结果发送给原始客户端。新的后端节点,和新的前端节点一样,可以动态地从集群中添加和移除。
package sample.cluster.transformation;
import java.io.Serializable;
public interface TransformationMessages {
public static class TransformationJob implements Serializable {
private final Stringtext;
public TransformationJob(Stringtext) {
this.text =text;
}
public String getText() {
return text;
}
}
public static class TransformationResult implements Serializable {
private final Stringtext;
public TransformationResult(Stringtext) {
this.text =text;
}
public String getText() {
return text;
}
@Override
public String toString() {
return "TransformationResult(" +text + ")";
}
}
public static class JobFailed implements Serializable {
private final Stringreason;
private final TransformationJobjob;
public JobFailed(Stringreason, TransformationJobjob) {
this.reason =reason;
this.job =job;
}
public String getReason() {
return reason;
}
public TransformationJob getJob() {
return job;
}
@Override
public String toString() {
return "JobFailed(" +reason + ")";
}
}
public static final StringBACKEND_REGISTRATION ="BackendRegistration";
}
执行转换任务的后端worker
代码:
package sample.cluster.transformation;
import static sample.cluster.transformation.TransformationMessages.BACKEND_REGISTRATION;
import sample.cluster.transformation.TransformationMessages.TransformationJob;
import sample.cluster.transformation.TransformationMessages.TransformationResult;
import akka.actor.UntypedActor;
import akka.cluster.Cluster;
import akka.cluster.ClusterEvent.CurrentClusterState;
import akka.cluster.ClusterEvent.MemberUp;
import akka.cluster.Member;
import akka.cluster.MemberStatus;
public class TransformationBackendextends UntypedActor {
Cluster cluster = Cluster.get(getContext().system());
//subscribe to cluster changes, MemberUp
@Override
public void preStart() {
cluster.subscribe(getSelf(), MemberUp.class);
}
//re-subscribe when restart
@Override
public void postStop() {
cluster.unsubscribe(getSelf());
}
@Override
public void onReceive(Objectmessage) {
if (messageinstanceof TransformationJob) {
TransformationJob job = (TransformationJob) message;
getSender().tell(new TransformationResult(job.getText().toUpperCase()), getSelf());
} else if (messageinstanceof CurrentClusterState) {
CurrentClusterState state = (CurrentClusterState) message;
for (Member member : state.getMembers()) {
if (member.status().equals(MemberStatus.up())) {
register(member);
}
}
} else if (messageinstanceof MemberUp) {
MemberUp mUp = (MemberUp) message;
register(mUp.member());
} else {
unhandled(message);
}
}
void register(Membermember) {
if (member.hasRole("frontend"))
getContext().actorSelection(member.address() +"/user/frontend").tell(
BACKEND_REGISTRATION, getSelf());
}
}
注意,TransformationBackend actor
订阅集群事件,来检测新的、潜在的前端节点,并向它们发送注册消息,这样它们就知道它们可以使用后端的worker
。
接收用户任务并代理给注册的后端worker的前端代码:
package sample.cluster.transformation;
import static sample.cluster.transformation.TransformationMessages.BACKEND_REGISTRATION;
import java.util.ArrayList;
import java.util.List;
import sample.cluster.transformation.TransformationMessages.JobFailed;
import sample.cluster.transformation.TransformationMessages.TransformationJob;
import akka.actor.ActorRef;
import akka.actor.Terminated;
import akka.actor.UntypedActor;
public class TransformationFrontendextends UntypedActor {
List<ActorRef> backends =new ArrayList<ActorRef>();
int jobCounter = 0;
@Override
public void onReceive(Objectmessage) {
if ((messageinstanceof TransformationJob) && backends.isEmpty()) {
TransformationJob job = (TransformationJob) message;
getSender().tell(
new JobFailed("Service unavailable, try again later", job),
getSender());
} else if (messageinstanceof TransformationJob) {
TransformationJob job = (TransformationJob) message;
jobCounter++;
backends.get(jobCounter % backends.size()).forward(job, getContext());
} else if (message.equals(BACKEND_REGISTRATION)) {
getContext().watch(getSender());
backends.add(getSender());
} else if (messageinstanceof Terminated) {
Terminated terminated = (Terminated) message;
backends.remove(terminated.getActor());
} else {
unhandled(message);
}
}
}
注意,TransformationFrontend actor
监视注册的后端,能够将它从可用的后端worker
列表中移除。死亡监视(Death watch
)使用集群的故障检测器检测集群中的节点,即它检测网络故障和JVM
崩溃,除了优雅终止被监视的
actor
。当不可达的集群节点
down
掉或被移除时,死亡监视会产生Terminated
消息给正在监视的actor
。
Typesafe Activator教程Akka Cluster Samples with Java,包含完整的源代码,以及如何运行Worker Dial-in示例的指导。
节点角色
不是所有的节点都需要执行相同的功能。可能有一个子集是执行web前端的,有一个子集是执行数据访问层的,还有一个子集是执行数字运算。actor的部署—例如由cluster-aware router部署—可以考虑节点角色,来实现职责分工。
节点的角色可以由配置属性akka.cluster.roles定义。通常在启动脚本中定义为系统属性或者环境变量。
节点的角色是你订阅的MemberEvent的成员关系信息的一部分。
每当达到集群大小时如何启动
一个常用的使用场景是在集群初始化、成员加入、集群已经达到一定大小之后,启动actor。
在leader将成员状态由Joining修改为Up之前,你可以使用配置选项定义需要的成员数量:
akka.cluster.min-nr-of-members = 3
用类似的方式,在leader将成员状态由Joining修改为Up之前,你可以定义需要的某种角色的成员数量:
akka.cluster.role {
frontend.min-nr-of-members = 1
backend.min-nr-of-members = 2}
你可以在注册的registerOnMemberUp回调中启动actor,当当前的成员状态修改为Up时,就会触发这个回调,即集群至少定义了the defined number of members.
Cluster.get(system).registerOnMemberUp(newRunnable() {
@Override
public voidrun() {
system.actorOf(Props.create(FactorialFrontend.class, upToN,true),
"factorialFrontend");
}});
这个回调还可以用于其它的事情,而不仅是启动actor。
集群单例
有一些场景,确保某种类型的actor只有一个在集群中运行是很方便的,有时候也是强制要求的。
这可以通过订阅成员事件实现,但是需要考虑几个极端情况。因此,这个特殊的使用场景使用contrib模块中的集群单例就变得简单了。
集群分片
通过集群中的几个节点分发actor,支持使用actor的逻辑标识符与actor的交互,而无需关心它们在集群中的物理位置。
参加contrib模块中的Cluster Sharding。
分布式发布订阅
在集群actor之间,使用actor的逻辑路径发布订阅消息(点到点消息),即发送者不必知道目的actor运行在那个节点上。
参见contrib模块中的Distributed Publish Subscribe in Cluster。
Cluster客户端
不是集群部分的actor system与集群中运行的actor通信。客户端不必知道目的actor运行在哪个节点上。
参加contrib模块的Cluster Client。
故障检测器
在集群中,每一个节点都由一些(默认最大是5个)其它节点监视着,当任何节点检测到该节点不可达时,那么这个信息就会通过gossip被传播到集群中的其它剩余部分。换句话说,只需要一个节点将某个节点标记为不可达,就可以让集群中的剩余部分将其标记为不可达。
故障检测器也会检测节点再次变得可达。当所有监视不可达节点的节点检测到它再次可达时,在gossip传播之后,将会认为它是可达的。
如果系统消息不能传递给节点,那么这个节点就会被隔离,那么它就不能从不可达恢复了。如果有太多的未确认的系统消息(例如监视、终止、远程actor部署、远程parent监督的actor失败),这是有可能发生的。那么节点需要变为down或者removed状态,actor system必须在它再次加入集群前重启。
集群中的节点通过发送心跳消息互相监视,以检测是否有节点对于集群中其它节点是不可达到的。心跳到达时间是由The Phi Accrual Failure Detector解释的。
失败的怀疑程度是由称为phi的值确定的。Phi故障检测的基本思想是用phi值来表达动态反映当前网络条件的数值范围。
Phi值由phi = -log10(1 - F(timeSinceLastHeartbeat))计算,这里F是标准正态分布的累积分布函数,它的均值和方差都是从历史的心跳到达时间估计出来的。
在配置中,你可以调整akka.cluster.failure-detector.threshold,来定以何时phi值被认为是一个失败。
低门限容易产生许多误报,但是确保了真正崩溃时的快速检测。相反,高门限产生更少的错误,但是需要更多的时间来检测实际的崩溃。默认的门限是8,对于大多数场景都是合适的。然而,在云环境中,例如Amazon EC2,这个门限值可以增加到12,以解释在这样的平台上发生的网络问题。
下面的图表说明了phi值如何随着增加的心跳时间而增长。
Phi是从历史达到事件的均值和标准方差计算而来的。上一个图表示标准方差为200ms的例子。如果心跳以更小的方差达到,那么曲线会变得很陡峭,即它检测到失败更快。标准方差为100ms的曲线看起来是这样的:
为了应对突发的异常情况,例如垃圾收集暂停,短暂网络故障,故障检测器配置了边界,akka.cluster.failure-detector.acceptable-heartbeat-pause。你可能想要按照你的环境调整这个配置。下面是acceptable-heartbeat-pause配置为3s的曲线:
死亡监视使用集群的故障检测器检测集群中的节点,即它检测网络故障和JVM崩溃,除了优雅终止actor。当不可达的集群节点down掉或者被移除时,死亡监视产生的Terminated消息给正在监视的actor。
如果你遇到了怀疑误报,当系统过载时,你应该为集群actor定义一个单独的分发器,正如Cluster Dispatcher描述的那样。
感知集群的路由器
所有路由器都可以感知到集群中的成员,即部署新的routee或者查看集群节点中的routee。当一个节点变得不可达,或者离开集群时,这个节点的routee也自动从路由器中取消注册。当新的节点加入集群后,额外的routee被添加到路由器中。节点不可达后,如果又再次可达,那么routee也会被添加到router中。
感知集群的router会使用WeaklyUp状态的成员,如果这个特性使能的话。
有两种不同类型的路由器:
· Group:这种类型的router使用ActorSelection指定发送消息的路径。运行在不同集群节点上的router可以共享routee。 这种类型router的一种使用场景是运行在集群后端节点上的服务或者运行集群前端节点上router。
· Pool:这种类型的router创建routee作为子actor,并部署到远程节点上。 每一个router都有自己的routee实例。例如,如果你在10节点集群中的3个节点上启动了router,且每一个节点配置一个router实例,那么你总共得到30个routee。由不同的router创建的routee在router之间不能共享。这种类型router的一个使用场景是单个master协调任务,并将实际的工作代理给集群中其它节点上的routee。
使用Routee分组的Router
当使用分组功能时,你必须在集群成员节点上启动router actor。这不是由router完成的。分组配置看起来是这样子的:
akka.actor.deployment {
/statsService/workerRouter {
router = consistent-hashing-group
routees.paths = ["/user/statsWorker"]
cluster {
enabled = on
allow-local-routees = on
use-role = compute
}
}
}
注意:routee actor尽可能比启动ActorSystem早。因为只要成员状态被修改为Up,router就会尝试使用它们。
在routees.paths中定义的没有地址信息的actor路径用于router选择转发消息的目的actor。消息使用ActorSelection转发给routee,所以期望具有相同得到传递语义。可能限制只查询特定角色的成员节点的routee,可由use-role指定。
max-total-nr-of-instances集群中routee的数量。默认情况下,max-total-nr-of-instances被设置为较大的值(10000),这样新的routee会被添加到router中,当节点集群时。如果你想要限制routee的数量,可以将其设置为较小的值。
同种类型的router也可以在代码中定义:
int totalInstances = 100;
Iterable<String> routeesPaths = Collections.singletonList("/user/statsWorker");
boolean allowLocalRoutees = true;
String useRole = "compute";
ActorRef workerRouter = getContext().actorOf(
new ClusterRouterGroup(new ConsistentHashingGroup(routeesPaths),
new ClusterRouterGroupSettings(totalInstances, routeesPaths,
allowLocalRoutees, useRole)).props(), "workerRouter2");
参见
Configuration
章节进一步了解设置。
分组routee的Router示例
让我们看看如何使用具有分组routee功能的感知集群的router分组,即router发送给routee的路径。
这个示例应用提供了一个计算文本统计信息的服务。当文本发送给这个服务室,它会将文本拆分为单词,并将该任务代理给一个独立的worker(router的routee),来计算每一个单词中的字符数量。每一个单词的字符数量会返回给一个聚合器。当所有的结果都收集完时,这个聚合器计算每一个单词的平均字符个数。
消息:
import java.io.Serializable;
public interface StatsMessages {
public static class StatsJob implements Serializable {
private final String text;
public StatsJob(String text) {
this.text = text;
}
public String getText() {
returntext;
}
}
public static class StatsResult implements Serializable {
private final double meanWordLength;
public StatsResult(doublemeanWordLength) {
this.meanWordLength = meanWordLength;
}
public double getMeanWordLength() {
return meanWordLength;
}
@Override
public String toString() {
return"meanWordLength: " + meanWordLength;
}
}
public static class JobFailed implements Serializable {
private final String reason;
public JobFailed(String reason) {
this.reason = reason;
}
public String getReason() {
return reason;
}
@Override
public String toString() {
return"JobFailed(" + reason + ")";
}
}
}
计算每个单词中字符个数的
worker
:
import akka.actor.UntypedActor;
import java.util.HashMap;
import java.util.Map;
public class StatsWorker extends UntypedActor {
Map<String, Integer> cache = new HashMap<String, Integer>();
@Override
publicvoid onReceive(Object message) {
if (messageinstanceof String) {
String word = (String) message;
Integer length = cache.get(word);
if (length == null) {
length = word.length();
cache.put(word, length);
}
getSender().tell(length, getSelf());
} else {
unhandled(message);
}
}
}
接收来自用户的文本,并将其分割成单词,将任务代理给
worker
,聚合结果的服务:
import akka.actor.ActorRef;
import akka.actor.Props;
import akka.actor.UntypedActor;
import akka.routing.ConsistentHashingRouter.ConsistentHashableEnvelope;
import akka.routing.FromConfig;
import cluster.StatsMessages.StatsJob;
public class StatsService extends UntypedActor {
// This router is used both with lookup and deploy of routees. If you
// have a router with only lookup of routees you can use Props.empty()
// instead of Props.create(StatsWorker.class).
ActorRef workerRouter = getContext().actorOf(
FromConfig.getInstance().props(Props.create(StatsWorker.class)), "workerRouter");
@Override
public void onReceive(Object message) {
if (messageinstanceof StatsJob) {
StatsJob job = (StatsJob) message;
if (job.getText().equals("")) {
unhandled(message);
} else {
final String[] words = job.getText().split(" ");
final ActorRef replyTo = getSender();
// create actor that collects replies from workers
ActorRef aggregator =
getContext().actorOf(
Props.create(StatsAggregator.class, words.length,replyTo))
// send each word to a worker
for (String word : words) {
workerRouter.tell(new ConsistentHashableEnvelope(word, word),aggregator);
}
}
} else {
unhandled(message);
}
}
}
import akka.actor.ActorRef;
import akka.actor.ReceiveTimeout;
import akka.actor.UntypedActor;
import cluster.StatsMessages.JobFailed;
import cluster.StatsMessages.StatsResult;
import scala.concurrent.duration.Duration;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
public class StatsAggregator extends UntypedActor {
final int expectedResults;
final ActorRef replyTo;
final List<Integer> results = new ArrayList<Integer>();
public StatsAggregator(intexpectedResults, ActorRef replyTo) {
this.expectedResults = expectedResults;
this.replyTo = replyTo;
}
@Override
publicvoid preStart() {
getContext().setReceiveTimeout(Duration.create(3, TimeUnit.SECONDS));
}
@Override
publicvoid onReceive(Object message) {
if (messageinstanceof Integer) {
Integer wordCount = (Integer) message;
results.add(wordCount);
if (results.size() == expectedResults) {
intsum = 0;
for (intc : results)
sum += c;
doublemeanWordLength = ((double) sum) / results.size();
replyTo.tell(new StatsResult(meanWordLength), getSelf());
getContext().stop(getSelf());
}
} else if (message == ReceiveTimeout.getInstance()) {
replyTo.tell(new JobFailed("Service unavailable, try again later"), getSelf());
getContext().stop(getSelf());
} else {
unhandled(message);
}
}
}
注意,目前为止没有什么是集群特有的,都只是普通的
actor
。
所有的几点都启动了StatsService和StatsWorker actor。记住routee是这个场景的worker。Router由routees.paths配置:
akka.actor.deployment {
/statsService/workerRouter {
router = consistent-hashing-group
routees.paths = ["/user/statsWorker"]
cluster {
enabled = on
allow-local-routees = on
use-role = compute
}
}
}
这意味着用户请求可以发送到任意节点上的StatsService,它使用所有节点上的StatsWorker。
Lightbend Activator教程Akka Cluster Samples with Java,包含了完整的源代码和运行使用分组routee的Router示例的指导。
使用远程部署routee池子的router
当使用池子,在成员节点上创建和部署routee,router的配置看起来是这样的:
akka.actor.deployment {
/statsService/singleton/workerRouter {
router = consistent-hashing-pool
cluster {
enabled = on
max-nr-of-instances-per-node = 3
allow-local-routees = on
use-role = compute
}
}
}
可以限制特定角色(use-role)的成员节点上部署的routee数量。
max-total-nr-of-instances定义了集群中的routee总数,但是每一个节点的routee数量不会超过max-nr-of-instances-per-node。默认情况下,max-total-nr-of-instances设置为较大的值(10000)将导致新的routee添加到router中,当节点加入集群时;如果你想要限制routee的总数,将其设置为较小的值。
同种类型的router也可以在代码中定义:
int totalInstances = 100;
int maxInstancesPerNode = 3;
boolean allowLocalRoutees = false;
String useRole = "compute";
ActorRef workerRouter = getContext().actorOf(
new ClusterRouterPool(new ConsistentHashingPool(0),
new ClusterRouterPoolSettings(totalInstances, maxInstancesPerNode,
allowLocalRoutees, useRole)).props(Props.create(StatsWorker.class)), "workerRouter3");
参考配置章节,进一步了解设置的描述。
使用远程部署routee池子的router示例
让我们看看如何在单个master节点上使用感知集群的router,来创建和部署worker。为了跟踪单个master,我们使用contrib模块中的集群单例。ClusterSingletonManager在每一个节点上启动:
ClusterSingletonManagerSettings settings = ClusterSingletonManagerSettings.create(system)
.withRole("compute");
system.actorOf(ClusterSingletonManager.props(
Props.create(StatsService.class), PoisonPill.getInstance(), settings),
"statsService");
每一个节点上需要一个actor,来跟踪当前单个master在哪儿,并代理任务给StatsService。这是由ClusterSingletonProxy提供的:
ClusterSingletonProxySettings proxySettings =
ClusterSingletonProxySettings.create(system).withRole("compute");
system.actorOf(ClusterSingletonProxy.props("/user/statsService",
proxySettings), "statsServiceProxy");
ClusterSingletonProxy接收来自用户的文本,并将任务代理给当前的StatsService,它是单个master。它监听集群事件以查找最老的节点上的StatsService。
所有的节点启动了ClusterSingletonProxy和ClusterSingletonManager。Router现在配置成这样:
akka.actor.deployment {
/statsService/singleton/workerRouter {
router = consistent-hashing-pool
cluster {
enabled = on
max-nr-of-instances-per-node = 3
allow-local-routees = on
use-role = compute
}
}
}
Lightbend Activator教程Akka Cluster Samples with Java,包含了完整的源代码以及如何运行使用远程部署routee池子的router示例。
集群度量
集群中的成员节点可以在集群度量扩展的帮助下,可以收集系统的健康指标,并将这些指标发送给其它的集群节点,发给系统事件总线上注册的订阅者。
JMX
集群的信息和管理可以使用根名字为akka.Cluster的JMX MBean完成。JMX信息可以使用普通的JMX控制台显示,例如JConsole或JVisualVM。
从JMX中,你可以:
· 查看集群中的成员
· 查看节点的状态
· 查看每一个成员的角色
· 加入节点到集群中的另一个节点
· 标记集群中的任意节点为down
· 告诉集群中的任意节点离开
成员节点是由它们的地址标识的,格式为akka.<protocol>://<actor-system-name>@<hostname>:<port>.
命令行管理
集群可以使用akka发布包提供的bin/akka-cluster脚本管理。
不带参数运行它,看看如何使用这个脚本:
Usage: bin/akka-cluster <node-hostname> <jmx-port> <command> ...
Supported commands are:
join <node-url> - Sends request a JOIN node with the specified URL
leave <node-url> - Sends a request for node with URL to LEAVE the cluster
down <node-url> - Sends a request for marking node with URL as DOWN
member-status - Asks the member node for its current status
members - Asks the cluster for addresses of current members
unreachable - Asks the cluster for addresses of unreachable members
cluster-status - Asks the cluster for its current status (member ring,
unavailable nodes, meta data etc.)
leader - Asks the cluster who the current leader is
is-singleton - Checks if the cluster is a singleton cluster (single
node cluster)
is-available - Checks if the member node is available
Where the <node-url> should be on the format of
'akka.<protocol>://<actor-system-name>@<hostname>:<port>'
Examples: bin/akka-cluster localhost 9999 is-available
bin/akka-cluster localhost 9999 join akka.tcp://MySystem@darkstar:2552
bin/akka-cluster localhost 9999 cluster-status
要使用这个脚本,当启动集群节点的JVM时,你必须使能远程监控和管理,请参考使用JMX技术监控和管理。
用系统属性使能远程监视和管理的示例:
java -Dcom.sun.management.jmxremote.port=9999 \
-Dcom.sun.management.jmxremote.authenticate=false \
-Dcom.sun.management.jmxremote.ssl=false
配置
集群有几个配置属性。我们参考reference configuration了解更多信息。
集群信息日志
你可以关闭集群事件的日志,配置info级别:
akka.cluster.log-info = off
集群分发器
集群扩展的底层是用actor实现的,有必要为这些actor创建一些隔离板,以避免其它actor的干扰。尤其是用于故障检测的心跳,如果它们没有机会在正常的间隔内运行,那么就会误报。因此,你可以为集群actor定义一个单独的分发器:
akka.cluster.use-dispatcher = cluster-dispatcher
cluster-dispatcher {
type = "Dispatcher"
executor = "fork-join-executor"
fork-join-executor {
parallelism-min = 2
parallelism-max = 4
}
}
注意:通常情况下,没必要为集群配置单独的分发器。默认的分发器应该足以执行集群任务,即akka.cluster.use-dispatcher 不应该被修改。如果当使用默认的分发器时,有集群相关的问题,典型的就是运行阻塞了或者运行CPU集中的actor或者任务。这些任务和actor使用专用的分发器,不要使用默认的分发器,因为它们会让内部的任务饥饿。相关的配置属性:akka.cluster.use-dispatcher = akka.cluster.cluster-dispatcher。对应的默认值:akka.cluster.use-dispatcher =.