Spring RSocket:基于服务注册发现的 RSocket 负载均衡

简介: RSocket 作为通信协定的后起之秀,核心是二进制异步化消息通信,是否也能和 Spring Cloud 技巧栈结合,实现办事注册发明、客户端负载均衡,从而更高效地实现面向办事的架构?这篇文章我们就评论辩论一下 Spring Cloud 和 RSocket 结合实现办事注册发明和负载均衡。

RSocket 分布式通信协定是 Spring Reactive 的核心内容,从 Spring Framework 5.2 开端,RSocket 已经是 Spring 的内置功能,Spring Boot 2.3 也添加了 spring-boot-starter-rsocket,简化了 RSocket 的办事编写和办事调用。RSocket 通信的核心架构中包含两种模式,分别是 Broker 代理模式和办事直连通信模式。

Broker 的通信模式更灵活,如 Alibaba RSocket Broker,采取的是事宜驱动模型架构。而今朝更多的架构则是面向办事化设计,也就是我们常说的办事注册发明和办事直连通信的模式,个中最有名的就是 Spring Cloud 技巧栈,涉及到设备推送、办事注册发明、办事网关、断流保护等等。在面向办事化的分布式收集通信中,如 REST API、gRPC 和 Alibaba Dubbo 等,都与 Spring Cloud 有很好地集成,用户根本不消关怀办事注册发明和客户端负载均衡这些底层细节,就可以完成异常稳定的分布式收集通信架构。

RSocket 作为通信协定的后起之秀,核心是二进制异步化消息通信,是否也能和 Spring Cloud 技巧栈结合,实现办事注册发明、客户端负载均衡,从而更高效地实现面向办事的架构?这篇文章我们就评论辩论一下 Spring Cloud 和 RSocket 结合实现办事注册发明和负载均衡。

办事注册发明

办事注册发明的道理异常简单,重要涉及三种角色:办事供给方、办事花费者和办事注册中间。典范的架构如下:

办事供给方,如 RSocket Server,在应用启动后,会向办事注册中间注册应用相干的信息,如应用名称,ip 地址,Web Server 监听端标语等,当然还会包含一些元信息,如办事的分组(group),办事的版本号(version),RSocket 的监听端标语,假如是 WebSocket 通信,还须要供给 ws 映射路径等,不少开辟者会将办事供给方的办事接口列表作为 tags 提交给办事注册中间,便利后续的办事查询和治理。

在本文中,我们采取 Consul 作为办事注册中间,主如果 Consul 比较简单,下载后履行 consul agent -dev 就可以启动对应的办事,当然你可以应用 Docker Compose,设备也异常简单,然后 docker-compose up -d 就可以启动 Consul 办事。

当我们向办事中间注册和查询办事时,都须要有一个应用名称,对应到 Spring Cloud 中,也就是 Spring Boot 对应的 spring.application.name 的值,这里我们称之为应用名称,也就是后续的办事查找都是基于该应用名称进行的。假如你调用 ReactiveDiscoveryClient.getInstances(String serviceId); 查找办事实例列表时,这个 serviceId 参数其实就是 Spring Boot 的应用名称。推敲到办事注册和后续的 RSocket 办事路由的合营以及便利大年夜家懂得,这里我们计算设计一个简单的定名规范。

假设你有一个办事应用,功能名称为 calculator,同时供给两个办事: 数学计算器办事(MathCalculatorService)和汇率计算器办事(ExchangeCalculatorService), 那么我们该若何来定名该应用及其对应的办事接口名?

这里我们采取类似 Java package 定名规范,采取域名倒排的方法,如 calculator 应用对应的则为 com-example-calculator 样式,为何是中划线,而不是点?. 在 DNS 解析中作为主机名是不法的,只能作为子域名存在,不克不及作为主机名,而今朝的办事注册中间设计都遵守 DNS 规约,所以我们采取中划线的方法来定名应用。如许采取域名倒排和应用名结合的方法,可以确保应用之间不会重名,别的也便利和 Java Package 名称进行转换,也就是 - 和 . 之间的互相转换。

那么应用包含的办事接口应当若何定名?办事接口全名是由应用名称和 interface 名称组合而成,规矩如下:

String serviceFullName = appName.WordStr("-", ".") + "." + serviceInterfaceName;

例如以下的办事定名都是合乎规范的:

  • com.example.calculator.MathCalculatorService
  • com.example.calculator.ExchangeCalculatorService

而 com.example.calculator.math.MathCalculatorService 则是缺点的, 因为在应用名称和接口名称之间多了 math。为何要采取这种定名规范?起首让我们看一下办事花费方是若何调用长途办事的。假设办事花费方拿到一个办事接口,如 com.example.calculator.MathCalculatorService,那么他该若何提议办事调用呢?

  • 起首根据 Service 周全提取处对应的应用名称(appName),如 com.example.calculator.MathCalculatorService 办事对应的 appName 则为 com-example-calculator。假如应用和办事接口之间不存在任何干系,那么想要获取办事接口对应的办事供给方信息,你可能还须要应用名称,这会相对来说比较麻烦。假如接口名称中包含对应的应用信息,则会简单很多,你可以懂得为应用是办事周全中的一部分。
  • 调用 ReactiveDiscoveryClient.getInstances(appName) 获取应用名对应的办事实例列表(ServiceInstance),ServiceInstance 对象会包含诸如 IP 地址,Web 端标语、RSocket 监听端标语等其他元信息。
  • 根据 RSocketRequester.Builder.transports(servers) 构建具有负载均衡才能的 RSocketRequester 对象。
  • 应用办事全称和具体功能名称作为路由进行 RSocketRequester 的 API 调用,样例代码如下:

rsocketRequester .route("com.example.calculator.MathCalculatorService.square") .data(number) .retrieveMono(Integer.class)

经由过程上述的定名规范,我们可以从办事接口全称中提掏出应用名,然后和办事注册中间交互查找对应的实例列表,然后建立和办事供给者的连接,最后基于办事名称进行办事调用。该定名规范,根本做到到了最小化的依附,开辟者美满是基于办事接口调用,异常简单。

RSocket 办事编写

有了办事的定名规范和办事注册,编写 RSocket 办事,这个照样异常简单,和编写一个 Spring Bean 没有任何差别。引入 spring-boot-starter-rsocket 依附,创建一个 Controller 类,添加对应的 MessagMapping annotation 作为基本路由,然后实现功能接口添加功能名称,样例代码如下:

@Controller @MessageMapping("com.example.calculator.MathCalculatorService") public class MathCalculatorController implements MathCalculatorService { @MessageMapping("square") public Mono<Integer> square(Integer input) { System.out.println("received: " + input); return Mono.just(input * input); } }

上述代码看起来似乎有点奇怪,既然是办事实现,添加 @Controller 和 @MessageMapping,看起来似乎有点不伦不类的。当然这些 annotation 都是一些技巧细节表现,你也能看出,RSocket 的办事实现是基于 Spring Message 的,是面向消息化的。这里我们其实只须要添加一个自定义的 @SpringRSocketService annotation 就可以解决这个问题,代码如下:

@Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Documented @Controller @MessageMapping() public @interface SpringRSocketService { @AliasFor(annotation = MessageMapping.class) String[] value() default {}; }

回到办事对应的实现代码,我们改为应用 @SpringRSocketService annotation,如许我们的代码就和标准的 RPC 办事接口完全一模一样啦,也便于懂得。此外 @SpringRSocketService 和 @RSocketHandler 这两个 Annotation,也便利我们后续做一些 Bean 扫描、IDE 插件帮助等。

@SpringRSocketService("com.example.calculator.MathCalculatorService") public class MathCalculatorImpl implements MathCalculatorService { @RSocketHandler("square") public Mono<Integer> square(Integer input) { System.out.println("received: " + input); return Mono.just(input * input); } }

最后我们添加一下 spring-cloud-starter-consul-discovery 依附,设置一下 bootstrap.properties,然后在 application.properties 设置一下 RSocket 监听的端口和元信息,我们还将该应用供给的办事接口列表作为 tags 传给办事注册中间,当然这个也是便利我们后续的办事治理。样例如下:

spring.application.name=com-example-calculator spring.cloud.consul.discovery.instance-id=com-example-calculator-${random.uuid} spring.cloud.consul.discovery.prefer-ip-address=true server.port=0 spring.rsocket.server.port=6565 spring.cloud.consul.discovery.metadata.rsocketPort=${spring.rsocket.server.port} spring.cloud.consul.discovery.tags=com.example.calculator.ExchangeCalculatorService,com.example.calculator.MathCalculatorService

RSocket 办事应用启动后,我们在 Consul 控制台就可以看到办事注册上来的信息,截屏如下:

RSocket 客户端接入

客户端接入稍微有一点复杂,主如果要基于办事接口周全要做一系列相干的操作,然则前面我们已经有了定名规范,所以问题也不大年夜。客户端应用同样会接入办事注册中间,如许我们就可以获得 ReactiveDiscoveryClient bean,接下来就是根据办事接口全名,如 com.example.calculator.ExchangeCalculatorService 构建出具有负载均衡的 RSocketRequester。

道理也异常简单,前面说过,根据办事接口全称,获得其对应的应用名称,然后调用 ReactiveDiscoveryClient.getInstances(appName) 获得办事应用对应的实例列表,接下来将办事实例(ServiceInstance)列表转换为 RSockt 的 LoadbalanceTarget 列表,其实就是 POJO 转换,最后将转 LoadbalanceTarget 列表进行 Flux 封装(如应用 Sink 接口),传递给 RSocketRequester.Builder 就完成具有负载均衡才能的 RSocketRequester 构建,具体的代码细节大年夜家可以参考项目标代码库。

这里要留意的是接下来若何感知办事端实例列表的变更,如应用高低线,办事暂停等。这里我采取一个准时义务筹划,准时查询办事对应的地址列表。当然还有其他的机制,假如是标准的 Spring Cloud 办事发明接口,今朝是须要客户端轮询的,当然也可以结合 Spring Cloud Bus 或者消息中心件,实现办事端列表变更的监听。假如客户端感知到办事列表的变更,只须要调用 Reactor 的 Sink 接口发送新的列表即可,RSocket Load Balance 在感知到变更后,会主动做出响应,如封闭即将掉效的连接、创建新的连接等工作。

在实际的应用之间的互相通信,会存在一些办事供给方弗成用的情况,如办事方忽然宕机或者其收集弗成用,这就导致了办事应用列表中部分办事弗成用,那么 RSocket 这个时刻会若何处理?不消担心,RSocket Load Balance 有重试机制,当一个办事调用出现连接等异常,会从新从列表中获取一个连接进行通信,而那个缺点的连接也会标识为可用性为 0,不会再被后续请求所应用。办事列表推送和通信时代的容错重试机制,这两者包管了分布式通信的高可用性。

最后让我们启动 client-app,然后从客户端提议一个长途的 RSocket 调用,截屏如下:

上图中 com-example-calculator 办事应用包含三个实例,办事的调用会在这三个办事实例瓜代进行(RoundRobin 策略)。

开辟体验的一些考量

固然办事注册和发明、客户端的负载均衡这些都完成啦,调用和容错这些都没有问题,然则还有一些应用体验上的问题,这里我们也阐述一下,闪开辟体验做的更好。

1. 基于办事接口通信

大年夜多半 RPC 通信都是基于接口的,如 Apache Dubbo、gRPC 等。那么 RSocket 可否做到?谜底是其实完全可以。在办事端,我们已经是基于办事接口来实现 RSocket 办事啦,接下来我们只须要在客户端实现基于该接口的调用就可以。对于 Java 开辟者来说,这不是大年夜问题,我们只须要基于 Java Proxy 机制构建就可以,而 Proxy 对应的 InvocationHandler 会应用 RSocketRequester 来实现 invoke() 的函数调用。具体的细节请参考应用代码中的的 RSocketRemoteServiceBuilder.java 文件,并且在 client-app module 中也已经包含懂得基于接口调用的 bean 实现。

2. 办事接口函数的单参数问题

应用 RSocketRequester 调用长途接口时,对应的处理函数只能接收单个参数,这个和 gRPC 的设计是类似的,当然也推敲了不合对象序列化框架的支撑问题。然则推敲到实际的应用体验,可能会涉及到多参函数的情况,让调用方开辟体验更好,那么这个时刻该若何处理?其实从 Java 1.8 后,interface 是许可增长 default 函数的,我们可以添加一些体验更友爱的 default 函数,并且还不影响办事通信接口,样例如下:

public interface ExchangeCalculatorService { double exchange(ExchangeRequest request); default double rmbToDollar(double amount) { return exchange(new ExchangeRequest(amount, "CNY", "USD")); } }

经由过程 interface 的 default method,我们可认为调用方供给给便捷函数,如在收集传输的是字节数组 (byte[]),然则在 default 函数中,我们可以添加 File 对象支撑,便利调用方应用。Interface 中的函数 API 负责办事通信规约,default 函数来晋升应用方的体验,这两者的合营,可以异常轻易解决函数多参问题,当然 default 函数在必定程度上还可以作为数据验证的前哨来应用。

3. RSocket Broker 支撑

前面我们说到,RSocket 还有一种 Broker 架构,也就是办事供给方是隐蔽在 Broker 之后的,请求主如果由 Broker 承接,然后再转发给办事供给方处理,架构样例如下:

那么基于办事发明的机制负载均衡,可否和 RSocket Broker 模式混淆应用呢?如一些长尾或者复杂收集下的应用,可以注册到 RSocket Broker,然后由 Broker 处理请求调用和转发。这个其实也不不复杂,前面我们说到应用和办事接口定名规范,这里我们只须要添加一个应用名前缀就可以解决。假设我们有一个 RSocker Broker 集群,暂且我们称之为 broker0 集群,当然该 broker 集群的实例也都注册到办事注册中间(如 Consul)啦。那么在调用 RSocket Broker 上的办事时,办事名称就被调剂为 broker0:com.example.calculator.MathCalculatorService,也就是办事名前添加了 appName: 如许的前缀,这个其实是 URI 的另一种规范情势,我们就可以提取冒号之前的应用名,然后去办事注册中间查询获得应用对应的实例列表。

回到 Broker 互通的场景,我们会向办事注册中间查询 broker0 对应的办事列表,然后和 broker0 集群的实例列表创建连接,如许后续基于该接口的办事调用就会发送给 Broker 进行处理,也就是完成了办事注册发明和 Broker 模式的混淆应用的模式。

借助于这种定向指定办事接口和应用间的接洽关系,也便利我们做一些 beta 测试,如你想将 com.example.calculator.MathCalculatorService 的调用导流到 beta 应用,你就可以应用 com-example-calculator-beta1:com.example.calculator.MathCalculatorService 这种方法调用办事,如许办事调用对应的流量就会转发给 com-example-calculator-beta1 对应的实例,起到 beta 测试的后果。

回到最前面说到的规范,假如应用名和办事接口的绑定关系你其实做不到,那么你可以应用这种方法实现办事调用,如 calculator-server:com.example.calculator.math.MathCalculatorService,只是你须要更完全的文档解释,当然这种方法也可以解决之前体系接入到今朝的架构上,应用的迁徙成本也比较小。假如你之前的面向办事化架构设计也是基于 interface 接口通信的,那么经由过程该方法迁徙到 RSocket 上完全没有问题,对客户端代码调剂也最小。

总结

经由过程整合办事注册发明,结合一个实际的定名规范,就完成了办事注册发明和 RSocket 路由之间的优雅合营,当然负载均衡也是包含个中啦。比较其他的 RPC 筹划,你不须要引入 RPC 本身的办事注册中间,复用 Spring Cloud 的办事注册中间就可以,如 Alibaba Nacos, Consul, Eureka 和 ZooKeeper 等,没有多余的开销和保护成本。

作者: 雷卷

本文为阿里云原创内容,未经许可不得转载