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

4次阅读

共计 8209 个字符,预计需要花费 21 分钟才能阅读完成。

作者 | 雷卷
起源 | 阿里巴巴云原生公众号

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.replace("-", ".") + "." + 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 等,没有多余的开销和保护老本。如果你想更多理解 RSocket RPC 相干的细节,能够参考 Spring 官网博客《Easy RPC with RSocket》。

欢送退出 alibaba-rsocket-broker 钉钉群:

更多具体的代码细节,能够 点击链接查看文章对应的代码库!

正文完
 0