胡喜明,胡 淼
(杭州电子科技大学 通信工程学院,浙江 杭州 310018)
当前互联网开发中,网站业务流量的增加以及业务复杂度的提升使得传统单体架构不足以满足当前的需求,大部分互联网开发者采用分布式架构来构建当前复杂的业务[1]。该架构中采用业务拆分的理念,根据功能拆分为多个服务,通过RPC系统实现业务间通信,该系统肩负着服务间协调以及数据交换的责任[2]。
响应式编程作为一种新生的编程范式,提供了基于事件驱动的形式对异步化数据进行处理。该范式下允许开发人员去实现拥有时间驱动、异步化、可扩展的响应式系统。该范式下认为同步阻塞的开发形式是对资源的一种浪费,而原生异步化虽然能解决单一的事务异步问题,但是对多个事务组合性回调问题无法处理,因此引入响应式编程范式来解决该问题[3]。当前几乎所有的开发语言和框架都在向着响应式编程跟进,Java在1.8后也引入了响应式编程的开发模式,然而当前主流的RPC系统均采用Netty作为底层通讯框架,该框架无法支持响应式方法的远程调用,使得响应式编程在分布式项目中实现难度加大[4]。
为解决该问题,本文提出了一种基于响应式的RPC系统,系统中采用基于响应式编程的Reactor-netty组件作为跨进程通信手段,实现服务间非阻塞调用。项目中还设计了一种动态负载均衡策略,实现根据服务调用情况的动态化选择。该系统采用优化后的SPI机制实现良好的功能扩展。最后,通过与Netty作为底层通信框架的系统对比,该系统具有明显的性能优势。
响应式编程是一种关注传输中的数据流以及数据变化传递的异步编程模式,这也意味着可以应用编程语言进行静态和动态数据流的表示。Reactor是响应式编程的第四代函数库,同时也是一种为了实现响应式形式的具体编程规范,其主要为了解决jvm中异步方法的缺点,具有协调多个异步任务、较好的可读性、丰富的数据流运算符、懒加载订阅模式、背压等优点。该框架主要用于在JVM平台上基于响应式流规范构建非阻塞异步应用。java语言在1.8版本后引入了Reactor框架,其中可组合形式的反应类型Flux和Mono是其核心对象。Flux对象表示0到N项的反应序列,而一个Mono对象表示单值或空(0或1)的结果[3]。
Reactor库函数的下属分支Reactor-netty作为响应式编程家族的一员支持上述两种反应类型的封装,其底层基于Netty框架,并对netty进行响应式编程封装,将其转换为异步事件驱动的网络应用程序框架。Reactor-netty内部仍然保留了Netty的主从多线程模型,拥有Netty框架的全部优势[5]。同时Reactor-netty内部直接继承了Java函数式的API接口,支持HTTP、TCP等协议的调用。Reactor-netty 通过Reactor Streams中的背压理论进行数据流量控制,发布者和订阅者可以进行数据流量协商,保证服务高质量传输。其中背压分为4种策略:
(1)OnBackpressureBuffer策略:对下游的请求数据采用缓存的形式,保证系统不会压力过大。
(2)OnBackpressureDrop策略:元素就绪时,根据下游是否有未满足的request来发出当前数据。
(3)OnBackpressureLatest策略:一直发送当前最新的数据。
(4)OnBackpressureError策略:当前数据已经满了,再次添加请求直接报错[3]。
图1为OnBackpressureError策略的背压原理,当订阅者的消费能力远小于发布者,订阅者可以通知发布者进行服务的取消和终止功能,保证传输数据流量合理。
图1 OnBackpressureError策略背压原理
RPC是分布式系统的核心通信组件,肩负着实现高效服务间通信的职责[6]。如图2所示,一个完整的RPC流程包括3类角色:服务提供者、服务消费者以及服务注册中心[7-9]。分布式系统的服务调用流程如下:
图2 RPC架构
(1)服务提供方实现相应的发布接口,并将服务信息注册到注册中心。
(2)注册中心接收到注册信息后,对信息进行内部存储并开启服务监控功能,保证服务下线后及时通知服务消费方。
(3)服务消费方向注册中心发送服务订阅请求,通过注册中心所提供的信息在本地实现负载均衡选取指定地址。
(4)获取到指定地址后,服务消费者向该地址发送服务调用请求,服务提供方对接收到的请求进行解析并向消费方返回请求结果[10]。
图3为响应式RPC功能层次,根据RPC各个角色的功能将架构分为:数据传输层、服务治理层、服务调用层以及Ext层4部分。
图3 响应式RPC功能层次
数据传输层主要用于对服务提供者以及服务消费者提供数据通信,需要保证双方在数据传输过程中高效、正确。为保证该需求,RPC系统在服务双方需要定义统一的数据管理标准。其中主要功能包括:通讯协议、通讯框架、编码以及对象序列化方式。
服务治理层主要用于对服务进行管控,在进行服务远程调用时,可以方便获取到服务注册信息。项目中采用注册中心的形式实现服务治理,其主要功能包括:服务持久化注册表、服务发现、服务注册、服务下线、服务监控。
服务调用层是对服务消费者所发起的请求进行调用,与本地服务调用不同,远程调用需要根据服务的注册信息进行具体地址的选择。同时RPC框架需要对接收到的服务信息进行解析。因此该层主要提供的功能为:代理调用以及服务负载均衡管理。
Ext层是对当前服务的扩展层,从图中可以看到Ext层是贯穿于其它层之间,对上述3层中的组件进行扩展,根据企业需求实现RPC系统定制化。
数据传输层是RPC系统的核心基础功能,一个RPC传输系统的性能好坏取决于当前传输的方式以及稳定性。该层的设计主要围绕如何提高性能以及减少无效字段传输的方面考虑。此时需要对传输协议、通信框架、数据编码以及序列化方式进行优化选取。
3.1.1 数据传输协议
对于一个完善的RPC框架,数据的传输不仅要考虑粘包拆包的问题,而且还需要考虑到一些扩展性的需求,因此需要自定义一套私有化协议,在协议中需要保证基本业务的实现,同时要考虑到对数据的扩展性支持,保证开发人员可以基于当前的协议进行业务上的扩展。
图4为协议整体设计,其中字段解释如下:
图4 私有化协议
(1)Protocal:该字段是标识当前传输的协议,可以通过该字段方便地转换所传输的协议。
(2)Type:该字段是标识当前协议可以传输的类型,分为request、response、oneway这3种。其中request为请求是发送的报文类型,response为接收到请求并处理结束进行返回时的报文类型,而oneway为客户端单方面请求无需服务端响应的报文。
(3)Command:该字段标识当前请求所对应的命令,分为request、heart、response。其中heart为心跳检测请求,定时检测服务是否存活。
(4)Id:该id字段是对当前请求的唯一标识,每个请求都会初始化一个requestId保证请求的唯一性。
(5)ClassNameLen:该字段标识当前请求class名称的长度,可提前解析出要调用的class。
(6)Timeout:该字段标识当前请求的超时时间。
(7)BodyLen:该字段标识在序列化后传入的信息长度。
(8)Body:该字段标识在序列化后的二进制信息。其中主要包括请求的方法名、方法参数类型、方法具体参数。
上述请求协议字段中,ClassNameLen字标识了当前存入的Class的长度,在高性能的网络框架中系统都是采用Reactor多线程模式,即一个IO线程去挂载多个连接,如果将数据处理都放在当前这个线程将会影响到其它挂载的数据,因此在当前的服务框架中BodyLen以及Body字段的解析可以不放在当前的IO线程中,而是在后续开启的业务线程中进行,保证IO线程的流畅。
3.1.2 Reactor-netty通讯框架设计
Reactor-netty是在原生Netty框架上的响应式封装,其底层仍然沿用Netty的handler机制。handler是针对不同事件的处理模式,根据传入的事件进行处理并通过Pipeline实现handler之间的连接,当前handler在处理完事件后传递给下一个handler。handler分为input以及output两种,其中input为接收时所需要处理的事件,output为向外传输时所需要处理的事件。Reactor-netty在隐藏了大部分netty内部细节的同时增加响应式背压,只向外暴露简洁的API方法。项目中主要在TCP协议基础上进行私有化设计,Reactor-netty对外提供了易于使用的TcpServer模块以及TcpClient模块。
项目中TcpServer模块的核心代码如下所示,首先需要进行服务开启以及端口配置,其内置了create以及port方法,在创建后需要绑定netty底层的bootStrap进行事件的轮训监听。在设置了启动端口后需要对netty的channel管道进行配置,通过ChannelOption类进行参数选取,之后需要对生命周期中的连接初始化进行回调绑定,其中doOnConnection方法是在连接了远程服务器的时候会被回调,其内部需要对读写超时、序列化、粘包解析、心跳、连接处理进行配置,该方法是框架提供的生命周期事件管理中的一种,其它的还包括doOnBind、doOnBound、doOnUnbound、doOnLifecycle。上述方法都是在产生了对应事件时自动调用。最后需要为Reactor-netty注入事件处理器,通过handle方法对NettyInbound和NettyOutbound进行初始化,实现业务模块。
TcpServer
//创建服务
.create()
//绑定事务组
.doOnBind(bootstrap-> bootstrap.option(Chan-nelOption.SO_BACKLOG, serverProperties.getBacklog()))
//绑定端口
.port(serverProperties.getPort())
.runOn(loopResources)
//绑定channel配置值
.option(ChannelOption.SO_KEEPALIVE, ser-verProperties.isKeepAlive())
.option(ChannelOption.SO_REUSEADDR, true)
//其余的option值设定,省略
……
//初始化设置
.doOnConnection(this::initConnection)
//一系列handler绑定
.handle(this::handler);
//异步回调组合
Mono.just(pkg)
.map(RpcDataPackage::getData)
.handle(data, synchronousSink)-> {}
.publish(rpcHandlerFunction::handle)
.onErrorMap()
.
.onErrorResume()
.publish(nettyOutbound::send)
.doOnError(e-> log.error("conn catch error.", e));
项目中的客户端采用TcpClient封装,其内部配置流程与TcpServer类似,差别在于handle处理事件不同,其主要是作为请求封装以及相应的接收,因此在handler中需要对动态代理进行封装,将数据封装为传输所需要的代码,并通过doOnSubscribe方法实现事件的注册监听,对请求响应的数据包装,最后将包装后的消息响应给客户端。
数据整体传输过程中,由于有了Reactor-netty的封装可以在请求的参数以及返回值中使用Mono以及Flux类型的对象,实现响应式支持,如上代码所示,在异步回调组合中,通过流式编程对方法进行组合式编写,Mono的组合形式是对Future异步回调机制的优化,实现基于事件的异步通知。
3.1.3 编码以及序列化功能
由3.1.1节的数据传输层协议设计可知,在序列化之前,需要对前置自定义协议字段进行编码操作。首先需要创建一个ByteBuf字节缓冲区,用于存储整体编码后的数据。从当前传入的对象中解析出Protocal协议字段插入到缓冲区中并获取对应的协议对象,然后将协议中的Type、Command、Id、ClassName、Timeout按照顺序进行解析存储在ByteBuf中。此时对数据的编码阶段已经结束,后续请求中的详细数据需要对其进行序列化处理。在Reactor-netty中通过实现MessageToMessageDecoder接口中的decode方法,在方法内部对数据进行预处理以及调用反序列化方法。通过实现MessageToByteEncoder接口中的encode方法对传入的对象进行获取以及序列化方法调用,将二进制数据存入Reactor-Netty内置的Buffer缓冲区中。
在实现数据编码的过程中离不开序列化的支持,序列化是将java对象向着二进制对象转化,反序列化是序列化的逆过程。在当前RPC框架中,数据均以二进制进行传输,因此详细请求信息需要进行序列化操作。
系统中采用接口+实现类形式进行序列化机制的扩展。
@SPI
public interface SerializerInterface {
//序列化
//反序列化
}
在序列化的选取中,考虑当前主流的4种序列化方式java、hession、Kryo、ProtoBuf。原生java序列化性能方面存在不足,Hession框架在速度上相对java原生有所提高但是数据序列化后长度不理想、ProtoBuf虽然性能方面很出众但是需要编写特定的IDL文件,在代码接入方面不理想。
本项目最终选取Kryo作为当前框架的序列化方式,Kryo性能好,兼容性强,可提前对数据长度进行设置,提高资源利用率。
(1)Kryo序列化
在类KryoSerializer中,通过对SerializerInterface接口的serialize方法重写,实现数据的序列化功能。核心代码如下:
ByteArrayOutputStream oss=null;
Output out1=null;
private final ThreadLocal
@Override
protected Kryo initialValue(){
Kryo kryo = new Kryo();
//循环引用
kryo.setReferences(true);
kryo.setRegistrationRequired(false);
return kryo;
}
};
//序列化操作
public
//生成对应Stream流
oss = new ByteArrayOutputStream();
//生成Out输出流
out1 = new Output(oss);
try {
kryoLocal.get().writeObject(output,seri);
out1.flush();
byte[]result = oss.toByteArray();
return result;
} catch(Exception e){
//处理报错信息
……
}
}
Kryo是线程不安全的,在序列化过程中首先需要对Kryo进行线程私有化管理,通过ThreadLocal实现线程隔离,保证每个线程拿到自己的Kryo实例。初始化后创建ByteArrayOutputStream字节流缓冲区以及OutPut输出流,Kryo通过writeObject方法将数据写入到OutPut输出流中的字节流缓冲区中,最后从字符流缓冲区获取当前的二进制字节序列结束整个序列化流程,并关闭释放所有资源。
(2)Kryo反序列化
在类KryoSerializer中的deserialize方法实现了数据的反序列化功能,通过该方法实现了二进制到类对象的转化。核心代码如下:
ByteArrayInputStream in1=null;
Input in1=null;
//反序列化实现
public
{
//初始化输入流缓冲区
is1= new ByteArrayInputStream(bytes);
//初始化输入流
in1 = new Input(is);
try {
Object result = kryoLocal.get().readObject(input, clazz);
return result;
} catch(Exception e){
//处理报错信息
……
}
}
反序列化方法中传入了二进制字节序列,首先创建ByteArrayInputStream字节输入流缓冲区以及Input输入流对象,并对传入的二进制字节序列进行装载。通过Kryo的readObject方法实现二进制字节流到对象的转换,然后释放所有资源。
以上是基于Kryo实现的编解码以及序列化过程,该框架可以有效解决原生java序列化所带来的性能问题,提高了RPC服务的相应速度以及效率。
服务治理在RPC中主要以注册中心的形式存在,用于对服务的发布、查询、监控以及下线处理。通过注册中心将服务提供方以及服务消费方两者连接在一起,服务提供方注册服务信息到注册中心,服务消费方从注册中心获取信息,实现两者通信。同时注册中心还兼具了服务的心跳管理功能,保证了服务与注册中心之间保持连接。
本系统中采用Zookeeper作为注册中心,其内部采用树形结构的目录,与传统的文件系统不同,Zookeeper的节点可以设置为临时以及持久化两种,同时在其内部拥有Watcher机制,Zookeeper允许开发者对某一些节点添加Watcher监控,根据节点的变化来进行操作触发,并且在Watcher机制对时间仅做一次响应,做到了轻量级的数据通知[11]。系统在整合中主要分为服务注册、服务发现、服务移除。
(1)服务注册
在服务注册阶段,Zookeeper获取到当前注册的信息,检测当前父节点是否存在,不存在则创建父节点。在父节点下将当前注册的服务作为子节点。将当前的注册信息缓存在本地内存中并对节点注册Watcher监听,如果服务服务下线,直接将当前内存中的数据清除,保证本地内存和Zookeeper的一致性。
(2)服务发现
在服务注册阶段已经将注册好的服务缓存在本地内存中,调用服务发现请求时可以直接从本地拉取所需要的服务,并响应给服务消费方。
(3)服务移除
根据传入的信息进行地址拼接,系统对拼接后的地址进行检测,查询传入的URL地址信息是否在Zookeeper中,若信息存在则进行节点删除操作。服务已经注册了Watcher监听事件,对服务进行删除后会触发本地内存移除操作,保证服务信息在本地与远程的一致性。
服务调用层主要是建立在数据传输层之上,在数据传输之前以及之后需要对数据的调用形式进行处理。其中分为Provider提供方以及Consumer消费方,Consumer对请求数据进行封装,借助服务治理层获取地址进行路由选择,然后通过数据传输层将当前请求传送给Provider。Provider通过数据传输层对传输的请求进行解析,采用动态代理形式对请求方法进行调用,将调用结果封装后传输给Consumer完成整体调用。整体流程如图5所示。
图5 调用流程
图5流程中可以看到,在调用阶段需要对服务进行代理调用以及服务地址的负载均衡选择,接下来将对这两部分进行分析。
3.3.1 服务对象代理
系统在对请求进行反序列化后可以获取到当前的类信息,服务提供方无法直接对服务接口进行调用,需要采取服务代理的模式,根据获取到的方法名称,参数类型、参数实体调用真实方法。系统中基于java动态代理技术,该技术是java内置的方法,调用方法的内部需要传入最终方法的名称以及参数类型,以此来保证唯一方法的获取。在获取到当前的Method方法类后,调用其invoke方法并传入真实参数得到返回结果。具体调用核心代码如下:
try {
//参数赋值service的类信息、className、方法类型、方法参数
Class> serviceClass
String methodName
Class>[]parameterTypes
Object[]parameters
//jdk动态代理
Method method = serviceClass.getMethod(methodName, parameterTypes);
method.setAccessible(true);
//得到返回结果
Object result = method.invoke(serviceBean, parameters);
RpcResponse.setResult(result);
} catch(Throwable t){
//错误处理
RpcResponse.setErrorMsg(t);
}
//返回结果
return RpcResponse;
3.3.2 服务负载均衡
负载均衡的目的是将当前的服务请求均衡地分布给所属的网络系统[12]。单节点项目中由于大批量的请求全部都在一台机器上处理会导致性能上的瓶颈,因此系统必然需要集群化配置。而在集群环境下如何选择每次请求的地址是需要根据负载均衡的配置策略决定的。当前主流的选取策略有轮询、随机权重选择、最少连接数选择等[13]。上述负载均衡策略均为静态化选择,是通过管理员手动配置参数来实现服务器的负载均衡,静态化的优势在于可以方便的配置,并且算法实现相对容易,在小型系统中开销比较小,可以满足小系统的开发。然而静态化的节点选取是无法考虑到后续在实际运行过程中系统真实的负载情况[14]。为解决此问题,项目中提出了动态负载均衡的思想,该思想的核心是根据服务器在处理数据时的请求成功以及超时情况进行地址选取。如果当前服务处理时间在阈值范围内,可以认为当前服务器正常运行。如果出现异常或者超时,说明当前服务器可能负载压力过大,需要做出调整。该方法基于最大权值策略,每次选取当前权值最大的服务器进行路由。
该方法的执行流程如图6所示。
图6 负载均衡流程
首先对每个服务的权值设置初始值为100,开启一个循环的线程,等待客户端请求。当接收到RPC的请求后根据权值计算出被选择的概率,计算规则为当前服务每次请求成功一次,其对应的权值加一,而如果当前服务出现了异常或者超时,权值减少5。一般出现异常或者超时的情况相对较少,除非是服务器直接宕机无法接收请求。权值需要设置上下界限,以100作为权值上界,0作为权值下界,如果当前权值降为0,等待1 min,给服务器缓冲的时间,如果1 min后仍然出现调用超时,问题直接进行服务器的下线操作,如果成功则将权值回复到60。修改的权值信息都会自动更新到注册中心,可以从注册中心获取到当前权值信息。
Java SPI是一种允许开发人员在接入端实现外部扩展的模式,该模式对定义好的接口进行扩展,开发者可以采用代码无侵入的形式对当前框架增添功能,实现了整体服务的插拔式配置。
使用Java SPI模式需要遵循如下流程:
(1)服务提供者需要定义所需要扩展的接口,并且在当前项目的resources资源目录中新建META-INF/services文件夹,该文件夹主要作为记录扩展点文件的地址,在其内部创建一个以接口完整路径命名的文件,内容是当前接口的实现类路径。
(2)主程序内部通过ServiceLoader类进行动态装载,其内部扫描上一步文件中配置的所有类名,一次性全部加载到JVM中。
(3)将加载到的类文件信息全部进行初始化操作并生成对应的类。
(4)根据传入的参数进行轮询比较,找到匹配的类进行获取。
按照上述配置,Java开发者可以对服务进行扩展。然而Java SPI仍然存在如下的缺点:
(1)Java内置的SPI机制对所有的扩展类进行了实例化操作,在开发中可能只是需要对部分类进行实例化操作,且无法实现单例模式。
(2)无法对扩展点进行排序以及分组。
(3)扩展点文件只被限制在META-INF/services目录中无法额外扩展。
基于原生Java SPI的问题,对其进行了优化处理,使得当前系统中的SPI机制支持对自定义扩展来设置单例/多例、支持实现类排序功能、支持只创建所需要的类,解决原生全量加载问题、支持根据特征属性值区分不同类别、支持基于注解支持自动扫描实现类。
如图7所示为优化后的SPI类关系,在启动加载阶段,RPC内部会根据配置通过ExtensionLoader扩展类加载器进行数据加载,读取服务配置的目录,根据目录信息查询扩展接口对应配置文件,在该文件内部循环加载这个文件下的描述文件,并且按照行进行读取,其中相同接口只会被加载进内存一次,通过对注解的验证以及解析生成对应的Class文件,最后在使用扩展类时才会对其进行加载实例化操作。
图7 优化后SPI类关系
图7中可以看到优化后的SPI加入了两个注解,分为@SPI以及@Extension,其中@SPI标志着当前扩展的接口,而@Extension标志当前扩展的实现类。系统通过对注解解析实现类加载功能,实现注解化配置。
public @interface SPI{
//扩展类是否使用单例,默认使用
boolean singleton()default true;
}
public @interface Extension {
//扩展点名字
String value();
//优先级排序,大的优先级高
int order()default 0;
//扩展类是分类名称
boolean category()default false;
}
在上述注解类代码中可以看到,其内部可配置多个属性,通过SPI机制的优化功能就是通过对属性值的解析实现的。基于优化后的SPI扩展机制,开发人员可实现自定义扩展功能,并且无需对RPC底层代码进行修改,实现第三方自由化扩展。
系统功能测试主要是检测RPC的基本调用功能以及响应式编程方法是否可以正常调用。
在本地电脑搭建RPC环境,应用虚拟机单独开启Zookeeper服务,项目启动后查看Zookeeper注册中心可以看到当前服务已经注册成功,图8为当前服务的树形结构展示,这里在服务端开启集群形式,因此在Zookeeper的服务信息中,记录了两个地址端口号不同的地址。
图8 Zookeeper树形结构
为验证系统中响应式编程的支持,客户端编写测试代码发起远程服务调用,如下代码所示有两个方法,分为响应式方法以及普通方法的调用,响应式模式的编程方法中需要接收Mono对象作为返回值,而Mono对象在传统RPC框架中并不支持,项目中通过编写入参以及返回值都为MONO对象的方法,进行测定,经测定系统可以正确的调用两个方法并获取返回值。该系统在功能方面可正常运行,符合预期要求。
//引入响应式编程特有类MONO
import reactor.core.publisher.Mono;
@Service("testService")
public interface TestService {
//响应式方法
Mono
//普通方法
Request testMethod2(Request request);
}
这一部分主要针对基于Reactor-netty的RPC系统和基于Netty的RPC系统的响应性能进行测试。通过maven插件对接入了RPC项目的客户端代码以及服务端代码进行打包,将打包好的JAR文件分别上传到服务器中。测试中选取5台服务器,其中两台服务器分别部署基于Netty的RPC服务提供方和服务消费方,再选取两台作为基于Rea-ctor-netty的RPC系统的服务方以及消费方,最后一台单独部署Zookeeper注册中心。5台机器配置见表1。
表1 服务器配置
实验中采用并发测试工具Jmeter对两个注册中心系统进行性能压力测试。Jmeter模拟高并发下服务调用,其内部通过多线程机制设置并发访问数量,系统中以1k大小的对象进行数据传递,并在控制台打印传输的字符串,根据并发数的不同对比两个rpc数据响应速度。
表2为客户端并发模拟服务调用请求的响应时间。由表中可知,在并发数量较小的情况下两者的服务响应时间相近。随着并发访问量的提升,基于Netty的RPC响应时间远大于基于Reactor-netty的RPC系统,可以看出在服务调用方面响应式Reactor-netty的RPC系统有明显的性能优势。
表2 并发性能测试结果
本文设计了一款基于响应式的RPC系统,解决了当前java语言编写的RPC系统无法支持响应式流编程的问题。该系统采用Reactor-netty框架在Nett的基础上实现性能优化;通过响应式编程提升代码可读性,通过Mono类实现基于事件的异步回调组合形式;系统集成Kryo序列化方法,提升整体数据传输性能;系统应用Zookeeper实现注册中心的功能,为服务的治理提供保障;系统在传输地址选择方面采用动态负载均衡,提升系统在集群环境下的处理能力;通过优化后的SPI机制实现服务扩展;最后,通过和Netty作为通讯框架的RPC系统对比可知,该系统在请求速度方面的性能更加优越。