日前在调研使用 Akka Typed 构建分布式系统的时候,发现 Akka Typed 的 Remote 方案,特别是发现远端的 Actor 的部分依赖于 Akka Cluster Typed,其中 Reception 消息通过 Distributed Data 来同步[1]。这对于复用 Akka Cluster 构建业务系统描述业务逻辑来说没啥问题,但是对于基于 Akka 的分布式系统却有问题。这是因为 Akka 之上二次开发的系统,例如早期的 Flink 和 Spark,都有自己的集群管理机制,有 Master Actor 和 Worker Actor,如果采用 Akka Cluster 来搭建系统,就会面临管理上下层两个集群的问题,因此它们都采用了裸的 Akka Remote 来搭建系统。
对于 Typed 来说,为什么不能脱离 Akka Cluster 来做呢?这是因为如果没有集群信息,ActorSystem 就需要一个根据远端地址来发现 Actor 的机制,这就是经典 Untyped Akka 里面的 ActorSelection 机制。但是在 Typed 的世界里,一个 String 类型的地址会使得这样发现的类型很难保证,而整个 Typed 首要解决的问题即是通过类型系统来自动检测类型不匹配,从而提前发现编程中的错误。如果使用 String 类型的地址发现一个任意类型的 Actor 并在运行时判断它的类型,那么 Typed 的意义和概念上的一致性就会被破坏。
尽管如此,管理两个概念层次上的集群对于二次开发的系统来说还是不可接受的。这是因为它会导致排查问题的时候需要关注两个集群各自的管理策略,修改的时候也需要同时考虑这个改动在上层系统上导致了什么行为变更,对 Akka Cluster 又导致了什么行为变更,这样的开发过程将是非常痛苦的。
我在社区提出了相关的 issue,并形成了针对 Receptionist 做 ActorSelection 的一致意见。对一个确定类型的 Actor 我们可以保证类型的正确性同时基于地址来发现 Actor,只要能联系到远端的 Receptionist,我们就可以通过 Akka Typed 中的 Reception 协议来完成发现的功能。
不过,Akka 面向的毕竟是业务终端的用户,因此社区不打算提供这样的一个接口,以保持半强迫用户使用 Akka Cluster Typed 的倾向。对于在 Akka Typed 上搭建分布式系统的用户,可以手动进入 Untyped 的世界模拟上面一段描述的方法实现不基于 Cluster 的 Remote 方案。
下面贴出我尝试的两个不同进程上的 Typed ActorSystem 发现的例子,展示实现这个方案。
import akka.actor.typed.ActorRef;
import akka.actor.typed.ActorSystem;
import akka.actor.typed.Behavior;
import akka.actor.typed.RecipientRef;
import akka.actor.typed.javadsl.Behaviors;
import akka.actor.typed.receptionist.Receptionist;
import akka.actor.typed.receptionist.ServiceKey;
import com.typesafe.config.ConfigFactory;
import java.io.Serializable;
public class Master {
public static final class MasterMessage implements Serializable {
public final String content;
public MasterMessage(String content) {
this.content = content;
}
}
public static final ServiceKey masterKey = ServiceKey.create(MasterMessage.class, "master");
public static final Behavior behavior = Behaviors.receiveMessage(msg -> {
System.out.println("master receives: " + msg.content);
return Behaviors.same();
});
public static void main(String[] args) {
ActorSystem actorSystem = ActorSystem.create(Behaviors.setup(ctx -> {
ActorRef actorRef = ctx.spawnAnonymous(behavior);
ctx.getSystem().receptionist().tell(Receptionist.register(masterKey, actorRef));
return Behaviors.same();
}), "master", ConfigFactory.parseString(MASTER_CONFIG));
}
private final static String MASTER_CONFIG = "" +
"akka {\n" +
" actor {\n" +
" provider = \"remote\"\n" +
" allow-java-serialization = on\n" +
" }\n" +
"\n" +
" remote {\n" +
" artery {\n" +
" transport = tcp # See Selecting a transport below\n" +
" canonical.hostname = \"127.0.0.1\"\n" +
" canonical.port = 25520\n" +
" }\n" +
" }\n" +
"}\n";
}
(没有配置 Scala Plugin,别吐槽 Java 写 Akka 啦)
import akka.actor.ActorRef;
import akka.actor.Address;
import akka.actor.AddressFromURIString;
import akka.actor.typed.ActorSystem;
import akka.actor.typed.javadsl.Behaviors;
import akka.actor.typed.receptionist.Receptionist;
import com.typesafe.config.ConfigFactory;
public class Worker {
public static void main(String[] args) {
ActorSystem actorSystem = ActorSystem.create(Behaviors.setup(ctx -> {
Address address = AddressFromURIString.parse("akka://master@127.0.0.1:25520");
String receptionistPath = ctx.getSystem().receptionist().path().toStringWithAddress(address);
ctx.classicActorContext()
.actorSelection(receptionistPath)
.tell(Receptionist.find(
Master.masterKey,
ctx.spawnAnonymous(Behaviors.receiveMessage(msg -> {
msg.getServiceInstances(Master.masterKey)
.forEach(ref -> ref.tell(new Master.MasterMessage("Hey from worker.")));
return Behaviors.same();
}))
), ActorRef.noSender());
return Behaviors.same();
}), "worker", ConfigFactory.parseString(WORKER_CONFIG));
}
private final static String WORKER_CONFIG = "" +
"akka {\n" +
" actor {\n" +
" provider = \"remote\"\n" +
" allow-java-serialization = on\n" +
" }\n" +
"\n" +
" remote {\n" +
" artery {\n" +
" transport = tcp # See Selecting a transport below\n" +
" canonical.hostname = \"127.0.0.1\"\n" +
" canonical.port = 25521\n" +
" }\n" +
" }\n" +
"}\n";
}
后记。其实 Flink 和 Spark 使用 Akka 并没有把它当做一个集群,只是当做一个分布式的通信框架来使用,而 Akka 更多的是作为一个执行框架存在的。Spark 后来的 RPC 抽象包括 Flink FLIP-6 的 RPC 抽象都缺少了 Akka 重要的一个功能:监督机制,即子 Actor 失败之后的 failover 逻辑。我想如果采用 Akka Cluster Typed,并直接赋予 Actor 角色,采用 Akka 的集群管理抽象来实现系统的集群管理,即上下两层管理融合成功能相同的一层管理,或许就能解决原始问题本身。注意这里的差别在于之前的上下两层系统是不同的抽象,现在【上层系统】是 Akka Cluster 的一个实现。