苏州知名高端网站建设机构,做网站开发的有哪些公司好,查看网站流量,个人网站建设服务器简介#xff1a; RSocket 作为通讯协议的后起之秀#xff0c;核心是二进制异步化消息通讯#xff0c;是否也能和 Spring Cloud 技术栈结合#xff0c;实现服务注册发现、客户端负载均衡#xff0c;从而更高效地实现面向服务的架构#xff1f;这篇文章我们就讨论一下 Spri… 简介 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.replace(-, .) . serviceInterfaceName;
例如以下的服务命名都是合乎规范的
com.example.calculator.MathCalculatorServicecom.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 MonoInteger 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 MonoInteger 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.namecom-example-calculator spring.cloud.consul.discovery.instance-idcom-example-calculator-${random.uuid} spring.cloud.consul.discovery.prefer-ip-addresstrue server.port0 spring.rsocket.server.port6565 spring.cloud.consul.discovery.metadata.rsocketPort${spring.rsocket.server.port} spring.cloud.consul.discovery.tagscom.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 等没有多余的开销和维护成本。
作者 雷卷
原文链接
本文为阿里云原创内容未经允许不得转载