1. 背景
Akka框架作为Akka是一个以Actor模型为基础构建的基于事件的并发编程框架,底层使用Scala语言实现,提供Java和Scala两种API。通过Actor能够简化锁以及线程管理,Actor具有以下的特性:
- 提供了一种高级的抽象,能够封装状态和操作。简化并发应用的开发。
- 提供异步非阻塞/高性能的事件驱动模型,避免锁的滥用。
- 超级轻量级的线程事件处理能力。
由于Akka框架底层使用Scala语言实现,另外存在一个“月经”问题(每次RPC请求内部一个Int类型值+1,当服务长时间不重启导致Int越界),因此使用JAVA基于Actor模型去现实了一个RPC。从大致上来说,可以将该RPC认为是一个JAVA版的Akka,不过,在实现的过程中一些处理方式又不尽相同。
1.1 Actor模型与Go语言协程的比较
协程是Go语言中一种轻量级的线程,协程的设计目标是在多个协程之间进行并发执行,而不需要显式的线程管理和同步机制,使用 go 关键字就可以启动一个新的协程。Actor模型和Go协程都是并发编程的范式,它们在某些方面有一些相似之处,同样也存在一些不同点。
1.1.1 相似之处
- 轻量级: Actor模型和Go协程都设计为轻量级的并发单位。它们的创建和销毁成本相对较低,可以在应用程序中创建大量的并发执行单元。
- 并发性: Actor模型和Go协程都支持并发执行,允许程序中的多个任务同时进行。它们通过在不同的执行单元之间进行切换来提高程序的效率。
- 消息传递: Actor模型和Go协程都使用消息传递来实现并发组件之间的通信。在Actor模型中,Actor之间通过发送消息进行通信,而Go协程通过通道(channel)来进行消息传递。
- 无锁设计: 两者都鼓励无锁的设计。在Go中,通道的使用可以避免显式的锁,而Actor模型通过消息传递的方式也避免了直接的共享状态和锁的使用。
- 松耦合: Actor模型和Go协程都鼓励松耦合的设计。通过消息传递,两者都可以实现组件之间的松耦合,从而提高系统的可维护性和灵活性。
1.1.2 不同之处
-
通信方式:
Actor模型: 通信是通过消息传递实现的,Actor之间通过发送和接收消息进行通信。每个Actor都有自己的邮箱(消息队列),并且消息的发送和接收是异步的。
Go协程: 通信是通过通道(channel)实现的,协程通过发送和接收数据进行通信。通道可以是同步的,也可以是异步的,取决于具体的使用方式。 -
共享状态:
Actor模型: Actors之间没有共享状态,它们通过消息传递来进行通信,每个Actor有自己的状态。
Go协程: 协程之间可以共享内存,并通过共享内存进行直接的数据共享。这可能需要使用互斥锁等机制来保护共享状态。 -
监督和容错:
Actor模型: Actor模型中有监督机制,允许一个Actor监督另一个Actor,并对其进行容错处理。如果一个Actor失败,它的父Actor可以采取适当的措施。
Go协程: Go语言中的协程通常依赖于编程者显式地处理错误和异常,而不提供内置的监督和容错机制。 -
错误处理:
Actor模型: Actor模型中通常通过消息传递来处理错误,父Actor可以接收子Actor的错误消息并采取相应的措施。
Go协程: 在Go中,通常使用返回错误值的方式来处理错误,或者使用panic和recover来进行异常处理。 -
生命周期管理:
Actor模型: Actor模型中有明确定义的生命周期,包括创建、运行、停止等阶段。Actors通常由ActorSystem进行创建和管理。
Go协程: Go协程的生命周期由程序的运行时间决定,不需要显式的创建和销毁,它们在程序运行期间动态地执行。
总体而言,Actor模型更加强调消息传递和松耦合,适用于构建具有分布式、容错需求的系统。Go协程则更注重轻量级的并发,适用于构建本地的、需要共享内存的并发系统,选择哪种模型取决于具体的应用需求和设计偏好,非要个撕逼下谁优谁劣,俗好俗坏,个人感觉没啥意义。
1.2 Java版Akka-Rpc设计预期
- 使用protostuff序列化(.proto文件编写恶心,与Protocol Buffer性能几乎接近)
- 使用Netty进行通讯(同节点RPC不走网络,直接入收件箱队列);
- 路由策略:随机路由、指定Key路由、资源Id路由、强制路由
- 使用ZK进行集群状态管理
- 使用自定义注解进行服务注册及辅助控制(线程数量、方法名称设置等)
源码地址:
https://github.com/bossfriday/actor-rpc原型项目(目前已不维护)
https://github.com/bossfriday/bossfriday-nubybear/tree/master/cn.bossfriday.common/src/main/java/cn/bossfriday/common
当前版本已经经过了很多商用项目的检验(支持私有部署的IM系统),并且表现良好,Akka自增Int溢出的月经问题再也没有出现过,并且基于此还写了一个示例及验证项目:《一个用Java开发的分布式高性能文件服务》https://blog.csdn.net/camelials/article/details/124613041
以下是一个Actor示例:
@Slf4j
@ActorRoute(methods = ACTOR_FS_UPLOAD, poolName = ACTOR_FS_UPLOAD + "_Pool")
public class FileUploadActor extends BaseTypedActor<WriteTmpFileResult> {
@Override
public void onMessageReceived(WriteTmpFileResult msg) {
String fileTransactionId = "";
FileUploadResult result = null;
try {
fileTransactionId = msg.getFileTransactionId();
MetaDataIndex metaDataIndex = StorageEngine.getInstance().upload(msg);
if (metaDataIndex == null) {
throw new ServiceRuntimeException("MetaDataIndex is null: " + fileTransactionId);
}
result = new FileUploadResult(msg.getFileTransactionId(), OperationResult.OK, metaDataIndex);
} catch (Exception ex) {
log.error("UploadActor process error: " + fileTransactionId, ex);
result = new FileUploadResult(msg.getFileTransactionId(), OperationResult.SYSTEM_ERROR);
} finally {
this.getSender().tell(result, ActorRef.noSender());
msg = null;
}
}
}
- 性能及开销备注说明
1、不管是Go的协程还是Java19的虚线程其实有一个共同点就是:在用户态下,将协程或者虚线程(m)映射到少量的操作系统线程(n)下,从而减少线程切换带来的开销(这里先不谈他们的轻量性和系统线程执行选择策略)。同时可以将协程或者虚线程(m)理解为需要多线程处理的任务。那么类比下:如果一个系统中有很多Actor,并且这些Actor共享一个线程池,那么可以实现类似于将大量的需要并发处理的任务映射到少量的系统线程的效果。在这里实现中如果不指定ActorRoute注解中的poolName那么就是上面说的这个效果(不指定poolName则走一个公用的线程池,详见:ActorDispatcher.DEFAULT_THREAD_POOL定义)。当然把这种使用方式下的Actor说成是一个猴版的协程或者虚线程虽然有些不严谨,但是其效果大致相当。之所以说是猴版的原因是:协程和虚线程对于系统线程的选择策略是一个综合了多方面的结果,例如:负载均衡、本地性、工作窃取算法、线程池策略、动态调整等(线程池策略不是赫然其中之一吗?)
2、为了实现同节点RPC不走网络(原生Akka并不支持),实现了一个AbstractServiceBootstrap,这个Bootstrap可以理解为一个容器,启动时将所有PluginElements中的BaseUntypedActor加载到容器中,同时完成服务注册。大家可以把每一个PluginElement认为是一个微服务(每个微服务可以含有N个Actor,一个系统由N个微服务构成),把这个容器类比为tomcat,由于所有的Actor均运行于该容器内(同一进程下),再配合邮箱机制(A给B发RPC可以类比发邮件的过程:先进A的发现箱,B收到了A发来的邮件进B的收件箱)就可以做到同节点的ActorRPC不走网络。当然你也可以把每个微服务进行独立部署(在service-config.xml去配置即可,有时确实有这种强需求,例如:接入服务需要部署在DMZ区,其他服务部署在内网区。不过这种问题完全可以通过在DMZ部署一个代理去解决),不过我建议的方式还是在每台服务均部署全量服务,这样可以去尽量的吃同节点RPC不走网络的红利,另外这种方式也能最大化的利用服务器的硬件资源,因为很多时候,业务上就导致了各类的请求比率差异很大,这样横向扩容的时候,单独增加机器即可,而不用考虑,业务A扩几台,然后依赖的业务B需要对等扩充几台。
- AbstractServiceBootstrap
package cn.bossfriday.common;
import cn.bossfriday.common.conf.ServiceConfig;
import cn.bossfriday.common.exception.ServiceRuntimeException;
import cn.bossfriday.common.plugin.IPlugin;
import cn.bossfriday.common.plugin.PluginElement;
import cn.bossfriday.common.register.ActorRegister;
import cn.bossfriday.common.register.ActorRoute;
import cn.bossfriday.common.router.ClusterRouterFactory;
import cn.bossfriday.common.rpc.actor.BaseUntypedActor;
import cn.bossfriday.common.utils.ClassLoaderUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang.ArrayUtils;
import org.reflections.Reflections;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
/**
* AbstractServiceBootstrap
*
* @author chenx
*/
@Slf4j
public abstract class AbstractServiceBootstrap implements IPlugin {
/**
* start
*/
protected abstract void start();
/**
* stop
*/
protected abstract void stop();
@Override
public void startup(ServiceConfig config) {
try {
if (config == null) {
throw new ServiceRuntimeException("ServiceConfig is null");
}
ClusterRouterFactory.build(config);
this.registerActor(config);
ClusterRouterFactory.getClusterRouter().registryService();
ClusterRouterFactory.getClusterRouter().startActorSystem();
this.start();
log.info(config.getSystemName() + " startup() done.");
} catch (InterruptedException interEx) {
log.error("Bootstrap.startup() InterruptedException!", interEx);
Thread.currentThread().interrupt();
} catch (Exception ex) {
log.error("Bootstrap.startup() error!", ex);
}
}
@Override
public void shutdown() {
try {
this.stop();
} catch (Exception e) {
log.error("service shutdown error!", e);
}
}
/**
* registerActor
*
* @param config
* @throws IOException
* @throws ClassNotFoundException
*/
private void registerActor(ServiceConfig config) throws IOException, ClassNotFoundException {
List<Class<? extends BaseUntypedActor>> classList = new ArrayList<>();
this.loadActor(classList, config);
if (CollectionUtils.isEmpty(classList)) {
log.warn("no actor need to register!");
return;
}
classList.forEach(cls -> {
if (cls.isAnnotationPresent(ActorRoute.class)) {
ActorRoute route = cls.getAnnotation(ActorRoute.class);
this.registerActorRoute(cls, route);
}
});
}
/**
* loadActor(有配置走配置,无配置反射获取当前jar包内所有UntypedActor类)
*
* @param classList
* @param config
*/
private void loadActor(List<Class<? extends BaseUntypedActor>> classList, ServiceConfig config) throws IOException, ClassNotFoundException {
List<PluginElement> pluginElements = config.getPluginElements();
if (!CollectionUtils.isEmpty(pluginElements)) {
for (PluginElement pluginConfig : pluginElements) {
File file = new File(pluginConfig.getPath());
if (!file.exists()) {
log.warn("service build not existed!(" + pluginConfig.getPath() + ")");
continue;
}
List<Class<? extends BaseUntypedActor>> list = ClassLoaderUtil.getAllClass(pluginConfig.getPath(), BaseUntypedActor.class);
classList.addAll(list);
}
} else {
Set<Class<? extends BaseUntypedActor>> set = new Reflections().getSubTypesOf(BaseUntypedActor.class);
classList.addAll(set);
}
}
/**
* registerActorRoute
*
* @param cls
* @param route
*/
private void registerActorRoute(Class<? extends BaseUntypedActor> cls, ActorRoute route) {
if (ArrayUtils.isEmpty(route.methods())) {
return;
}
boolean isRegisterByPool = !"".equals(route.poolName()) && route.poolSize() > 0;
for (String method : route.methods()) {
try {
if (isRegisterByPool) {
ActorRegister.registerActor(method, cls, getActorExecutorMin(route), getActorExecutorMax(route), route.poolName(), route.poolSize());
} else {
ActorRegister.registerActor(method, cls, getActorExecutorMin(route), getActorExecutorMax(route));
}
log.info("registerActor done: " + cls.getSimpleName());
} catch (Exception ex) {
log.error("registerActor error!", ex);
}
}
}
private static final int DEFAULT_MIN;
private static final int DEFAULT_MAX;
static {
DEFAULT_MIN = (Const.CPU_PROCESSORS / 2) <= 0 ? 1 : (Const.CPU_PROCESSORS / 2);
DEFAULT_MAX = Const.CPU_PROCESSORS;
}
/**
* getActorExecutorMin
*
* @param route
* @return
*/
private static int getActorExecutorMin(ActorRoute route) {
if (route.min() > DEFAULT_MIN) {
return route.min();
}
return DEFAULT_MIN;
}
/**
* getActorExecutorMax
*
* @param route
* @return
*/
private static int getActorExecutorMax(ActorRoute route) {
if (route.max() > DEFAULT_MAX) {
return route.max();
}
return DEFAULT_MAX;
}
}
- service-config.xml
<?xml version="1.0" encoding="UTF-8"?>
<config>
<systemName>nubybear</systemName>
<zkAddress>localhost:2181</zkAddress>
<clusterNode>
<name>fs-node1</name>
<host>127.0.0.1</host>
<port>18080</port>
<virtualNodesNum>100</virtualNodesNum>
</clusterNode>
</config>
2. Actor 模型简介
Actor 的基础就是消息传递,一个 Actor 可以认为是一个基本的计算单元,它能接收消息并基于其执行运算,它也可以发送消息给其他 Actor。Actors 之间相互隔离,它们之间并不共享内存。
Actor 本身封装了状态和行为,在进行并发编程时,Actor 只需要关注消息和它本身。而消息是一个不可变对象,所以 Actor 不需要去关注锁和内存原子性等一系列多线程常见的问题。
所以 Actor 是由状态(State)、行为(Behavior)和邮箱(MailBox,可以认为是一个消息队列)三部分组成:
- 状态:Actor 中的状态指 Actor 对象的变量信息,状态由 Actor 自己管理,避免了并发环境下的锁和内存原子性等问题。
- 行为:Actor 中的计算逻辑,通过 Actor 接收到的消息来改变 Actor 的状态。
- 邮箱:邮箱是 Actor 和 Actor 之间的通信桥梁,邮箱内部通过 FIFO(先入先出)消息队列来存储发送方 Actor 消息,接受方 Actor 从邮箱队列中获取消息。
2.1 模型概念
可以看出按消息的流向,可以将 Actor 分为发送方和接收方,一个 Actor 既可以是发送方也可以是接受方。
另外我们可以了解到 Actor 是串行处理消息的,另外 Actor 中消息不可变。
Actor 模型特点
- 对并发模型进行了更高的抽象。
- 使用了异步、非阻塞、高性能的事件驱动编程模型。
- 轻量级事件处理(1 GB 内存可容纳百万级别 Actor)。
- 简单了解了 Actor 模型,我们来看一个基于其实现的框架。
2.2 Actor
Akka 是一个构建在 JVM 上,基于 Actor 模型的的并发框架,为构建伸缩性强,有弹性的响应式并发应用提高更好的平台。在Actor模型中,计算被抽象为独立的、互不干扰的实体,称为"Actor"。每个Actor都有自己的状态、行为和消息队列。Actor之间通过消息进行通信,而不是共享内存。当一个Actor接收到消息时,它可以执行一些操作,包括改变自己的状态、发送消息给其他Actor,或者创建新的Actor。
package cn.bossfriday.common.rpc.actor;
import cn.bossfriday.common.rpc.ActorSystem;
import cn.bossfriday.common.rpc.transport.RpcMessage;
import cn.bossfriday.common.utils.UUIDUtil;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
/**
* BaseUntypedActor
*
* @author chenx
*/
@Slf4j
public abstract class BaseUntypedActor {
@Setter
@Getter
private ActorRef sender;
@Setter
@Getter
private ActorRef self;
/**
* onReceive
*
* @param msg
*/
public abstract void onReceive(Object msg);
/**
* onReceive
*
* @param message
* @param actorSystem
*/
public void onReceive(RpcMessage message, ActorSystem actorSystem) {
if (message == null || actorSystem == null) {
log.warn("UntypedActor.onReceive(msg, actorSystem) returned by msg or actorSystem is null!");
return;
}
this.self = new ActorRef(actorSystem.getSelfAddress().getHostName(), actorSystem.getSelfAddress().getPort(), UUIDUtil.getUuidBytes(), message.getTargetMethod(), actorSystem);
if (message.hasSource()) {
if (message.getSourceMethod() == null) {
// source is callback actor
this.sender = new ActorRef(message.getSourceHost(), message.getSourcePort(), message.getSession(), actorSystem, null, 0);
} else {
this.sender = new ActorRef(message.getSourceHost(), message.getSourcePort(), message.getSession(), message.getSourceMethod(), actorSystem);
}
} else {
this.sender = ActorRef.noSender();
}
this.setSender(this.sender);
this.setSelf(this.self);
Object msgObj = null;
try {
msgObj = actorSystem.getMsgDecoder().decode(message.getPayloadData());
this.onReceive(msgObj);
} catch (Throwable throwable) {
this.onFailed(throwable);
} finally {
if (msgObj != null) {
msgObj = null;
}
message = null;
}
}
/**
* onFailed
*
* @param throwable
*/
public void onFailed(Throwable throwable) {
if (throwable != null) {
log.error("UntypedActor.onFailed()", throwable);
}
}
/**
* onTimeout
*
* @param actorKey
*/
public void onTimeout(String actorKey) {
log.warn("actor timeout, actorKey:" + actorKey);
}
/**
* clean
*/
public void clean() {
this.sender = null;
this.self = null;
}
}
2.3 ActorSystem
ActorSystem 是Actor模型中的一个关键概念,它是用于创建和管理Actor的系统级别的组件。在Akka框架中,ActorSystem 是整个Actor系统的入口点,提供了许多功能:
- Actor的创建和管理: ActorSystem 负责创建和管理所有的Actor。通过 ActorSystem,你可以创建新的Actor,并监视它们的生命周期。
- 调度和执行: ActorSystem 负责调度和执行Actor的任务。它使用调度器来管理Actor的执行,确保它们在适当的时间运行,并能够处理并发和异步操作。
- 配置和部署: ActorSystem 允许你配置Actor系统的各种参数,包括线程池大小、超时设置等。它还支持Actor的部署,可以在单个节点或多个节点上分布Actor。
- 监督和容错: ActorSystem 支持Actor之间的监督关系。如果一个Actor失败,它的父Actor可以采取适当的措施,例如重启、继续执行,或者停止。这有助于构建健壮和容错的系统。
- 配置和资源管理: ActorSystem 管理着整个Actor系统的配置信息,并负责释放系统级资源。这包括线程、网络连接等。
在使用Akka框架时,通常首先创建一个 ActorSystem 实例。创建 ActorSystem 的同时也会启动整个Actor系统。一旦有了 ActorSystem,你可以使用它创建顶层的Actor,并开始构建你的并发、分布式系统。
package cn.bossfriday.common.rpc;
import cn.bossfriday.common.Const;
import cn.bossfriday.common.exception.ServiceRuntimeException;
import cn.bossfriday.common.rpc.actor.ActorRef;
import cn.bossfriday.common.rpc.actor.BaseUntypedActor;
import cn.bossfriday.common.rpc.dispatch.ActorDispatcher;
import cn.bossfriday.common.rpc.interfaces.IActorMsgDecoder;
import cn.bossfriday.common.rpc.interfaces.IActorMsgEncoder;
import cn.bossfriday.common.rpc.mailbox.MessageInBox;
import cn.bossfriday.common.rpc.mailbox.MessageSendBox;
import cn.bossfriday.common.utils.UUIDUtil;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import java.lang.reflect.Constructor;
import java.net.InetSocketAddress;
import java.util.concurrent.ExecutorService;
/**
* ActorSystem
*
* @author chenx
*/
@Slf4j
public class ActorSystem {
@Getter
private String workerNodeName;
@Getter
private InetSocketAddress selfAddress;
@Getter
private MessageInBox inBox;
@Getter
private MessageSendBox sendBox;
@Getter
private ActorDispatcher dispatcher;
@Getter
@Setter
private IActorMsgEncoder msgEncoder;
@Getter
@Setter
private IActorMsgDecoder msgDecoder;
@Getter
private boolean isStarted = false;
private ActorSystem(String workerNodeName, InetSocketAddress selfAddress) {
this.workerNodeName = workerNodeName;
this.selfAddress = selfAddress;
this.dispatcher = new ActorDispatcher(this);
this.inBox = new MessageInBox(Const.EACH_RECEIVE_QUEUE_SIZE, selfAddress.getPort(), this.dispatcher);
this.sendBox = new MessageSendBox(this.inBox, selfAddress);
}
/**
* create
*
* @param workerNodeName
* @param selfAddress
* @return
*/
public static ActorSystem create(String workerNodeName, InetSocketAddress selfAddress) {
return new ActorSystem(workerNodeName, selfAddress);
}
/**
* start
*/
public void start() {
this.inBox.start();
this.sendBox.start();
this.isStarted = true;
}
/**
* stop
*/
public void stop() {
this.dispatcher.stop();
this.sendBox.stop();
this.inBox.stop();
this.isStarted = false;
}
/**
* registerActor
*
* @param method
* @param min
* @param max
* @param pool
* @param cls
* @param args
*/
public void registerActor(String method, int min, int max, ExecutorService pool, Class<? extends BaseUntypedActor> cls, Object... args) {
if (StringUtils.isEmpty(method)) {
throw new ServiceRuntimeException("method is null");
}
this.dispatcher.registerActor(method, min, max, pool, cls, args);
}
/**
* registerActor
*
* @param method
* @param min
* @param max
* @param cls
* @param args
*/
public void registerActor(String method, int min, int max, Class<? extends BaseUntypedActor> cls, Object... args) {
if (StringUtils.isEmpty(method)) {
throw new ServiceRuntimeException("method is null");
}
this.dispatcher.registerActor(method, min, max, cls, args);
}
/**
* actorOf(UntypedActor)
*
* @param ttl
* @param cls
* @param args
* @return
*/
public ActorRef actorOf(long ttl, Class<? extends BaseUntypedActor> cls, Object... args) {
try {
if (args == null || args.length == 0) {
return this.actorOf(ttl, cls.newInstance());
}
Class<?>[] clsArray = new Class<?>[args.length];
for (int i = 0; i < args.length; i++) {
clsArray[i] = args[i].getClass();
}
Constructor<? extends BaseUntypedActor> constructor = cls.getConstructor(clsArray);
BaseUntypedActor actor = constructor.newInstance(args);
return this.actorOf(ttl, actor);
} catch (Exception e) {
log.error("ActorSystem.actorOf() error!", e);
}
return null;
}
/**
* actorOf(UntypedActor)
*
* @param ttl
* @param actor
* @return
*/
public ActorRef actorOf(final long ttl, final BaseUntypedActor actor) {
return new ActorRef(this.selfAddress.getHostName(), this.selfAddress.getPort(), UUIDUtil.getUuidBytes(), this, actor, ttl);
}
/**
* actorOf(UntypedActor)
*
* @param cls
* @param args
* @return
*/
public ActorRef actorOf(Class<? extends BaseUntypedActor> cls, Object... args) {
return this.actorOf(Const.DEFAULT_CALLBACK_ACTOR_TTL, cls, args);
}
/**
* actorOf(UntypedActor)
*
* @param actor
* @return
*/
public ActorRef actorOf(BaseUntypedActor actor) {
return this.actorOf(Const.DEFAULT_CALLBACK_ACTOR_TTL, actor);
}
/**
* actorOf(select ActorRef prepare to tell)
*
* @param ip
* @param port
* @param targetMethod
* @return
*/
public ActorRef actorOf(String ip, int port, String targetMethod) {
byte[] session = UUIDUtil.toBytes(UUIDUtil.getUuid());
return new ActorRef(ip, port, session, targetMethod, this);
}
/**
* actorOf(select ActorRef prepare to tell)
*
* @param ip
* @param port
* @param session
* @param targetMethod
* @return
*/
public ActorRef actorOf(String ip, int port, byte[] session, String targetMethod) {
return new ActorRef(ip, port, session, targetMethod, this);
}
/**
* actorOf
*
* @param session
* @param targetMethod
* @return
*/
public ActorRef actorOf(byte[] session, String targetMethod) {
return this.actorOf(this.selfAddress.getHostName(), this.selfAddress.getPort(), session, targetMethod);
}
/**
* actorOf
*
* @param method
* @return
*/
public ActorRef actorOf(String method) {
return this.actorOf(this.selfAddress.getHostName(), this.selfAddress.getPort(), method);
}
}
2.4 ActorRef
ActorRef 可以看做是 Actor 的引用,是一个 Actor 的不可变,可序列化的句柄(handle),它可能不在本地或同一个 ActorSystem 中,它是实现网络空间位置透明性的关键设计,ActorRef 最重要功能是支持向它所代表的 Actor 发送消息。
package cn.bossfriday.common.rpc.actor;
import cn.bossfriday.common.exception.ServiceRuntimeException;
import cn.bossfriday.common.rpc.ActorSystem;
import cn.bossfriday.common.rpc.interfaces.IActorMsgEncoder;
import cn.bossfriday.common.rpc.mailbox.MessageSendBox;
import cn.bossfriday.common.rpc.transport.RpcMessage;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
/**
* ActorRef
*
* @author chenx
*/
@Slf4j
public class ActorRef {
private String host;
private int port;
@Getter
private String method;
@Getter
private byte[] session;
private MessageSendBox sendBox;
private IActorMsgEncoder tellEncoder;
private ActorSystem actorSystem;
private BaseUntypedActor callbackActor;
private long ttl;
public ActorRef() {
}
public ActorRef(String host, int port, byte[] session, ActorSystem actorSystem, BaseUntypedActor callbackActor, long ttl) {
this.host = host;
this.port = port;
this.session = session;
this.actorSystem = actorSystem;
this.callbackActor = callbackActor;
this.ttl = ttl;
if (this.actorSystem != null) {
this.sendBox = this.actorSystem.getSendBox();
this.tellEncoder = this.actorSystem.getMsgEncoder();
}
}
public ActorRef(String host, int port, byte[] session, String method, ActorSystem actorSystem) {
this.host = host;
this.port = port;
this.method = method;
this.session = session;
this.actorSystem = actorSystem;
if (this.actorSystem != null) {
this.sendBox = this.actorSystem.getSendBox();
this.tellEncoder = this.actorSystem.getMsgEncoder();
}
}
/**
* tell
*
* @param message
* @param sender
*/
public void tell(Object message, ActorRef sender) {
if (sender == null) {
throw new ServiceRuntimeException("sender is null!");
}
if (this.sendBox != null) {
RpcMessage msg = new RpcMessage();
msg.setSession(this.session);
msg.setTargetHost(this.host);
msg.setTargetPort(this.port);
msg.setTargetMethod(this.method);
msg.setSourceHost(sender.host);
msg.setSourcePort(sender.port);
msg.setSourceMethod(sender.method);
msg.setPayloadData(this.tellEncoder.encode(message));
this.registerCallBackActor(this.session);
sender.registerCallBackActor(this.session);
this.sendBox.put(msg);
}
}
/**
* registerCallBackActor
*
* @param session
*/
public void registerCallBackActor(byte[] session) {
if (this.callbackActor != null) {
this.actorSystem.getDispatcher().registerCallBackActor(session, this.callbackActor, this.ttl);
}
}
/**
* noSender
*
* @return
*/
public static ActorRef noSender() {
return DeadLetterActorRef.DEAD_LETTER_ACTOR_REF_INSTANCE;
}
}
2.5 Dispatcher 和 MailBox
ActorRef 将消息处理能力委派给 Dispatcher,实际上,当我们创建 ActorSystem 和 ActorRef 时,Dispatcher 和 MailBox 就已经被创建了。
Dispatcher 从 ActorRef 中获取消息并传递给 MailBox,Dispatcher 封装了一个线程池,之后在线程池中执行 MailBox。
- BaseMailBox
package cn.bossfriday.common.rpc.mailbox;
import cn.bossfriday.common.rpc.transport.RpcMessage;
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.LinkedBlockingQueue;
/**
* BaseMailBox
*
* @author chenx
*/
@Slf4j
public abstract class BaseMailBox {
protected final LinkedBlockingQueue<RpcMessage> queue;
protected boolean isStart = true;
protected BaseMailBox(LinkedBlockingQueue<RpcMessage> queue) {
this.queue = queue;
}
/**
* start
*/
public void start() {
new Thread(() -> {
while (BaseMailBox.this.isStart) {
try {
RpcMessage msg = BaseMailBox.this.queue.take();
BaseMailBox.this.process(msg);
} catch (InterruptedException ex) {
Thread.currentThread().interrupt();
log.error("MailBox.process() error!", ex);
} catch (Exception e) {
log.error("MailBox.process() error!", e);
}
}
}).start();
}
/**
* process
*
* @param msg
* @throws Exception
*/
public abstract void process(RpcMessage msg);
/**
* stop
*/
public abstract void stop();
/**
* put
*
* @param msg
*/
public void put(RpcMessage msg) {
try {
this.queue.put(msg);
} catch (InterruptedException ex) {
Thread.currentThread().interrupt();
log.error("MailBox.put() error!", ex);
} catch (Exception ex) {
log.error("MailBox.put() error!", ex);
}
}
}
- MessageInBox
package cn.bossfriday.common.rpc.mailbox;
import cn.bossfriday.common.rpc.dispatch.ActorDispatcher;
import cn.bossfriday.common.rpc.interfaces.IMsgHandler;
import cn.bossfriday.common.rpc.transport.NettyServer;
import cn.bossfriday.common.rpc.transport.RpcMessage;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.LinkedBlockingQueue;
import static cn.bossfriday.common.Const.SLOW_QUEUE_THRESHOLD;
/**
* MessageInBox
*
* @author chenx
*/
@Slf4j
public class MessageInBox extends BaseMailBox {
private final NettyServer server;
private ActorDispatcher dispatcher;
@SuppressWarnings("squid:S1604")
public MessageInBox(int size, int port, ActorDispatcher actorDispatcher) {
super(new LinkedBlockingQueue<>(size));
this.dispatcher = actorDispatcher;
this.server = new NettyServer(port, new IMsgHandler() {
@Override
public void msgHandle(RpcMessage msg) {
MessageInBox.super.put(msg);
}
});
}
@Override
public void start() {
try {
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workerGroup = new NioEventLoopGroup();
this.server.start(bossGroup, workerGroup);
super.start();
} catch (Exception e) {
log.error("MessageInBox start() error!", e);
}
}
@Override
public void process(RpcMessage msg) {
if (msg.getTimestamp() > 0) {
long currentTimestamp = System.currentTimeMillis();
if (currentTimestamp - msg.getTimestamp() > SLOW_QUEUE_THRESHOLD) {
log.warn("slow rpc, " + currentTimestamp + " - " + msg.getTimestamp() + " > " + SLOW_QUEUE_THRESHOLD);
}
}
this.dispatcher.dispatch(msg);
}
@Override
public void stop() {
try {
super.isStart = false;
super.queue.clear();
if (this.server != null) {
this.server.stop();
}
} catch (Exception e) {
log.error("MessageInBox stop() error!", e);
}
}
}
- MessageSendBox
package cn.bossfriday.common.rpc.mailbox;
import cn.bossfriday.common.rpc.transport.NettyClient;
import cn.bossfriday.common.rpc.transport.RpcMessage;
import lombok.extern.slf4j.Slf4j;
import java.net.InetSocketAddress;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.LinkedBlockingQueue;
import static cn.bossfriday.common.Const.EACH_SEND_QUEUE_SIZE;
/**
* MessageSendBox
*
* @author chenx
*/
@Slf4j
public class MessageSendBox extends BaseMailBox {
private MessageInBox inBox;
private InetSocketAddress selfAddress;
private ConcurrentHashMap<InetSocketAddress, NettyClient> clientMap = new ConcurrentHashMap<>();
public MessageSendBox(MessageInBox inBox, InetSocketAddress selfAddress) {
super(new LinkedBlockingQueue<>(EACH_SEND_QUEUE_SIZE));
this.inBox = inBox;
this.selfAddress = selfAddress;
}
@Override
public void process(RpcMessage msg) {
if (msg != null) {
InetSocketAddress targetAddress = new InetSocketAddress(msg.getTargetHost(), msg.getTargetPort());
// 本机通讯:不走网络(直接入接收队列)
if (this.selfAddress.equals(targetAddress)) {
this.inBox.put(msg);
return;
}
// 跨机通讯
if (!this.clientMap.containsKey(targetAddress)) {
NettyClient client = new NettyClient(msg.getTargetHost(), msg.getTargetPort());
this.clientMap.putIfAbsent(targetAddress, client);
}
this.clientMap.get(targetAddress).send(msg);
}
}
@Override
public void stop() {
try {
super.isStart = false;
super.queue.clear();
for (Map.Entry<InetSocketAddress, NettyClient> entry : this.clientMap.entrySet()) {
entry.getValue().close();
}
this.clientMap = new ConcurrentHashMap<>(16);
} catch (Exception e) {
log.error("MessageSendBox.stop() error!", e);
}
}
}
通过了解上面的一些概念,我们可以 Akka Actor 的处理流程归纳如下:
- 创建 ActorSystem
- 通过 ActorSystem 创建 ActorRef,并将消息发送到 ActorRef
- ActorRef 将消息传递到 Dispatcher中
- Dispatcher 依次的将消息发送到 Actor 邮箱中
- Dispatcher 将邮箱推送至一个线程中
- 邮箱取出一条消息并委派给 Actor 的 receive 方法
3. 总结
在Actor模型中,一切都可以抽象为Actor。
而Actor是封装了状态和行为的对象,他们的唯一通讯方式就是交换消息,交换的消息放在接收方的邮箱(Inbox)里。也就是说Actor之间并不直接通信,而是通过消息来相互沟通,每一个Actor都把它要做的事情都封装在了它的内部。
每一个Actor是可以有状态也可以是无状态的,理论上来讲,每一个Actor都拥有属于自己的轻量级线程,保护它不会被系统中的其他部分影响。因此,我们在编写Actor时,就不用担心并发的问题。