Dubbo探究
目录:
什么是Dubbo
Dubbo是一个高性能、轻量级、致力于分布式、云原生、透明化的远程服务调用方案。
以前的官网介绍:Apache Dubbo是一款高性能、轻量级的开源Java RPC框架。
现在的官网是这么介绍的:Apache Dubbo是一款云原生微服务开发框架,构建具备内置RPC、流量管控、安全、可观测能力的应用,支持Kubernetes和VM部署环境。
为什么会将RPC改为服务?
Dubbo一开始的定位就是RPC,专注于两个服务之间的调用。但随着微服务的盛行,除开服务调用之外,Dubbo也在逐步涉猎服务治理、服务监控、服务网关等等,所以现在Dubbo的目标已经不止是RPC框架了,而是和Spring Cloud类似想成为一个服务框架。
既然是服务框架,那我们一起看看Dubbo的架构是怎样的,这里放一张官网提供的核心概念架构图:
以上是 Dubbo 的工作原理图,从抽象架构上分为两层:服务治理抽象控制面 和 Dubbo 数据面 。
- 服务治理控制面。服务治理控制面不是特指如注册中心类的单个具体组件,而是对 Dubbo 治理体系的抽象表达。控制面包含协调服务发现的注册中心、流量管控策略、Dubbo Admin 控制台等,如果采用了 Service Mesh 架构则还包含 Istio 等服务网格控制面。
- Dubbo 数据面。数据面代表集群部署的所有 Dubbo 进程,进程之间通过 RPC 协议实现数据交换,Dubbo 定义了微服务应用开发与调用规范并负责完成数据传输的编解码工作。
- 服务消费者 (Dubbo Consumer),发起业务调用或 RPC 通信的 Dubbo 进程
- 服务提供者 (Dubbo Provider),接收业务调用或 RPC 通信的 Dubbo 进程
Dubbo 数据面
从数据面视角,Dubbo 帮助解决了微服务实践中的以下问题:
- Dubbo 作为 服务开发框架 约束了微服务定义、开发与调用的规范,定义了服务治理流程及适配模式
- Dubbo 作为 RPC 通信协议实现 解决服务间数据传输的编解码问题
服务开发框架
微服务的目标是构建足够小的、自包含的、独立演进的、可以随时部署运行的分布式应用程序,几乎每个语言都有类似的应用开发框架来帮助开发者快速构建此类微服务应用,比如 Java 微服务体系的 Spring Boot,它帮 Java 微服务开发者以最少的配置、最轻量的方式快速开发、打包、部署与运行应用。
微服务的分布式特性,使得应用间的依赖、网络交互、数据传输变得更频繁,因此不同的应用需要定义、暴露或调用 RPC 服务,那么这些 RPC 服务如何定义、如何与应用开发框架结合、服务调用行为如何控制?这就是 Dubbo 服务开发框架的含义,Dubbo 在微服务应用开发框架之上抽象了一套 RPC 服务定义、暴露、调用与治理的编程范式,比如 Dubbo Java 作为服务开发框架,当运行在 Spring 体系时就是构建在 Spring Boot 应用开发框架之上的微服务开发框架,并在此之上抽象了一套 RPC 服务定义、暴露、调用与治理的编程范式。
Dubbo 作为服务开发框架包含的具体内容如下:
- RPC 服务定义、开发范式。比如 Dubbo 支持通过 IDL 定义服务,也支持编程语言特有的服务开发定义方式,如通过 Java Interface 定义服务。
- RPC 服务发布与调用 API。Dubbo 支持同步、异步、Reactive Streaming 等服务调用编程模式,还支持请求上下文 API、设置超时时间等。
- 服务治理策略、流程与适配方式等。作为服务框架数据面,Dubbo 定义了服务地址发现、负载均衡策略、基于规则的流量路由、Metrics 指标采集等服务治理抽象,并适配到特定的产品实现。
再放一张更老的图:
什么是RPC
既然Dubbo是由RPC框架进化而来,所以要理解Dubbo首先要理解RPC的概念。
维基百科是这么定义RPC的:
在分布式计算,远程过程调用(英语:Remote Procedure Call,缩写为 RPC)是一个计算机通信协议。该协议允许运行于一台计算机的程序调用另一个地址空间(通常为一个开放网络的一台计算机)的子程序,而程序员就像调用本地程序一样,无需额外地为这个交互作用编程(无需关注细节)。RPC是一种服务器-客户端(Client/Server)模式,经典实现是一个通过发送请求-接受回应进行信息交互的系统。
如果涉及的软件采用面向对象编程
,那么远程过程调用亦可称作远程调用或远程方法调用,例:Java RMI
远程方法调用和本地方法调用是相对的两个概念,本地方法调用指的是进程内部的方法调用,而远程方法调用指的是两个进程内的方法相互调用。
所以就有了:
- RPC over Http:基于Http协议来传输数据
- RPC over Tcp:基于Tcp协议来传输数据
对于所传输的数据,可以交由RPC的双方来协商定义,但基本都会包括:
- 调用的是哪个类或接口
- 调用的是哪个方法,方法名和方法参数类型(考虑方法重载)
- 调用方法的入参
手写模拟Dubbo
知道了RPC和Dubbo的概念和核心框架,是否可以自己写一个RPC框架模拟Dubbo的工作流程呢?这里提前准备了一个写好的demo
Provicer暴露服务到ZK上
此时ZK节点存放服务信息。包括接口全限定名,ip和端口
Consumer打印消费日志,成功进行了远程过程调用
整个流程可以简化成:
Dubbo的基本使用
Dubbo服务分为Consumer,Provider,也就是服务的消费者和服务提供者。通常情况下,服务不是单一角色。一个服务即是Provider又是Consumer的情况比较多。下面给出一个接入Dubbo的例子: dubbolearn
本地启动nacos 单例
Provider未启动,nacos服务列表是空的
启动ProviderApplication,再看nacos服务列表,多出来一个Provider服务
服务信息,包含:服务版本、序列化协议、传输协议、端口等等,并且也支持多协议同时暴露。
这时启动Consumer,在nacos服务列表点击订阅者,跳转到订阅者详情界面:
同时也能远程调用Provider的方法。
再回到Dubbo的定义上,Dubbo本质上还是基于一个RPC协议拓展出来的,可以方便使用者进行远程过程调用,像调用本地方法一样调用远程的方法。对比SpringCloud的Feign调用,相当于颗粒度减小,要对其进行降级限流则更加细致精确。
Dubbo的高级功能
- 负载均衡、集群容错、服务降级
- 本地存根、本地伪装、参数回调
- 异步调用、泛化调用、泛化实现
- 管理台、动态配置、服务路由
既然作为一款云原生微服务开发框架,肯定具备一系列强大的服务治理工具。
负载均衡:
官网给出的是支持如下7中负载均衡策略,可以通过配置的方式修改生效。
算法 | 特性 | 备注 |
---|---|---|
Weighted Random LoadBalance | 加权随机 | 默认算法,默认权重相同 |
RoundRobin LoadBalance | 加权轮询 | 借鉴于 Nginx 的平滑加权轮询算法,默认权重相同, |
LeastActive LoadBalance | 最少活跃优先 + 加权随机 | 背后是能者多劳的思想 |
Shortest-Response LoadBalance | 最短响应优先 + 加权随机 | 更加关注响应速度 |
ConsistentHash LoadBalance | 一致性哈希 | 确定的入参,确定的提供者,适用于有状态请求 |
P2C LoadBalance | Power of Two Choice | 随机选择两个节点后,继续选择“连接数”较小的那个节点。 |
Adaptive LoadBalance | 自适应负载均衡 | 在 P2C 算法基础上,选择二者中 load 最小的那个节点 |
如果在消费端和服务端都配置了负载均衡策略,以消费端为准。
这其中比较难理解的就是最少活跃调用数是如何进行统计的?
讲道理,最少活跃数应该是在服务提供者端进行统计的,服务提供者统计有多少个请求正在执行中。
但在Dubbo中,就是不讲道理,它是在消费端进行统计的,为什么能在消费端进行统计?
逻辑是这样的:
- 消费者会缓存所调用服务的所有提供者,比如记为p1、p2、p3三个服务提供者,每个提供者内都有一个属性记为active,默认位0
- 消费者在调用次服务时,如果负载均衡策略是leastactive
- 消费者端会判断缓存的所有服务提供者的active,选择最小的,如果都相同,则随机
- 选出某一个服务提供者后,假设位p2,Dubbo就会对p2.active+1
- 然后真正发出请求调用该服务
- 消费端收到响应结果后,对p2.active-1
- 这样就完成了对某个服务提供者当前活跃调用数进行了统计,并且并不影响服务调用的性能
服务超时
在服务提供者和服务消费者上都可以配置服务超时时间,这两者是不一样的。
消费者调用一个服务,分为三步:
- 消费者发送请求(网络传输)
- 服务端执行服务
- 服务端返回响应(网络传输)
如果在服务端和消费端只在其中一方配置了timeout,那么没有歧义,表示消费端调用服务的超时时间,消费端如果超过时间还没有收到响应结果,则消费端会抛超时异常,但,服务端不会抛异常,服务端在执行服务后,会检查执行该服务的时间,如果超过timeout,则会打印一个超时日志。服务会正常的执行完。
如果在服务端和消费端各配了一个timeout,那就比较复杂了,假设
- 服务执行为5s
- 消费端timeout=3s
- 服务端timeout=6s
那么消费端调用服务时,消费端会收到超时异常(因为消费端超时了),服务端一切正常(服务端没有超时)。
集群容错
集群容错表示:服务消费者在调用某个服务时,这个服务有多个服务提供者,在经过负载均衡后选出其中一个服务提供者之后进行调用,但调用报错后,Dubbo所采取的后续处理策略。
服务降级
服务降级表示:服务消费者在调用某个服务提供者时,如果该服务提供者报错了,所采取的措施。
集群容错和服务降级的区别在于:
- 集群容错是整个集群范围内的容错
- 服务降级是单个服务提供者的自身容错
本地存根与本地伪装
本地存根,名字很抽象,但实际上不难理解,本地存根就是一段逻辑,这段逻辑是在服务消费端执行的,这段逻辑一般都是由服务提供者提供,服务提供者可以利用这种机制在服务消费者远程调用服务提供者之前或之后再做一些其他事情,比如结果缓存,请求参数验证等等。这很像我们经常使用的AOP的环绕增强,背后的原理也不难理解,肯定和动态代理有关。
参数回调
Dubbo支持参数回调,可以允许服务端调用客户端接口,Dubbo是基于长连接生成反向代理。参数回调的意义在于:可以用回调函数通知客户端执行结果,或发送通知,在方法执行时间比较长时,有些类似异步调用,比如在审批工作流中回调客户端审批结果。那假设在服务端执行完主动调用客户端逻辑,不回调行不行?如果不依赖回调,则需要服务端依赖客户端的jar包,存在循环依赖的问题。而参数回调还有一个好处,就是可以由客户端决定服务端调用的回调函数,由服务端主导,变为客户端主导。
异步调用
在微服务场景下,服务之间的交互往往对性能要求很高,如果使用同步调用会显著增加服务的响应时间,并对系统造成一定的压力。而Dubbo通过异步调用这种方式,可以在保证业务逻辑完整的同时,大幅提升性能和并发能力。
Dubbo的Provider和Consumer都支持异步调用,一共有四种组合
没有使用Dubbo框架的时候,我们也经常有这样的优化项:把远程接口调用改成异步执行,增加处理效率,减少调用方等待时间。而Dubbo中只需要一些配置就能轻松实现异步调用。“异步”作为性能优化的利器之一,对于系统优化是一种常见思路;Dubbo天然的异步模式,不需要启动多线程即可完成并行调用多个远程服务,相对多线程开销较小。
Dubbo3在使用异步调用的时候,Provider使用CompleteableFuture签名的接口,
public interface AsyncService { CompletableFuture<String> sayHello(String name); }
接口实现:
public class AsyncServiceImpl implements AsyncService { @Override public CompletableFuture<String> sayHello(String name) { return CompletableFuture.supplyAsync(() -> { System.out.println(name); try { Thread.sleep(5000); } catch (InterruptedException e) { e.printStackTrace(); } return "async response from provider."; }); } }
Consumer端在进行接口调用的时候:
// 此调用会立即返回null asyncService.sayHello("world"); // 拿到调用的Future引用,当结果返回后,会被通知和设置到此Future CompletableFuture<String> helloFuture = RpcContext.getServiceContext().getCompletableFuture(); // 为Future添加回调 helloFuture.whenComplete((retValue, exception) -> { if (exception == null) { System.out.println(retValue); } else { exception.printStackTrace(); } });
泛化调用
在Dubbo中,如果某个服务想要支持泛化调用,就可以将该服务的generic属性设置为true,那对于服务消费者来说,就可以不用依赖该服务的接口,换句话说,没有Provider提供的接口jar包,只要有接口全限定名定义,方法定义,参数列表,参数类型定义,只使用map组装参数,依然可以进行RPC调用。直接利用GenericService接口来进行服务调用。
比如如果要基于Dubbo做上层封装或者要做一个测试平台,此时就可以用到泛化调用。
demo中也给出了泛化调用的例子。dubbolearn
核心代码如下:
@GetMapping("/genget/{name}") public ResponseEntity<String> genericSay(@PathVariable("name") String name){ HashMap map = new HashMap(); map.put("userName","一只小Coder"); map.put("passWord","root"); Object result = DubboGenericInvoke.invoke("job",//表示需要调用哪个实例的服务,通过Dubbo的tag属性来区分 "com.joey.service.BarService",//需要调用的服务的全限定类名 "sayHello", //方法 new String[]{"com.joey.model.User"},//参数类型,不仅支持java的基本数据类型,同时支持自定义类型 new Object[]{map});//如果是基本数据类型,组装成Map集合就好了 return ResponseEntity.ok(result.toString()); }
private static Map<String, ReferenceConfig<GenericService>> referenceConfigMap = new ConcurrentHashMap<>(); public static Object invoke(String tag,String interfaceClass, String methodName, String[] paramTypes, Object[] params) { ReferenceConfig<GenericService> referenceConfig; String key = interfaceClass+"---"+tag; try { referenceConfig = referenceConfigMap.get(key); if (referenceConfig == null) { referenceConfig = new ReferenceConfig<>(); RegistryConfig registry = new RegistryConfig("nacos://127.0.0.1:8848/username=nacos&password=nacos"); referenceConfig.setRegistry(registry); referenceConfig.setRetries(4);//重试次数 ConsumerConfig consumerConfig = new ConsumerConfig(); referenceConfig.setConsumer(consumerConfig); referenceConfig.setGeneric("true"); referenceConfig.setInterface(interfaceClass); referenceConfig.setTimeout(3000);//ms,如果服务方超时时间超过了这里的时间,将进行重试,达到重试次数时,报超时异常。 if(tag.length()!=0){ referenceConfig.setTag(tag); } referenceConfig.setCheck(true); referenceConfigMap.put(key, referenceConfig); } GenericService genericService = referenceConfigMap.get(key).get(); Object result = genericService.$invoke(methodName, paramTypes, params); if (result == null) { System.out.println("远程服务结果返回为空,请注意查看远程服务的参数:"+ params); } return result; } catch (GenericException e) { System.out.println("发起远程调用失败,错误信息:{}"+e.getMessage()); referenceConfigMap.remove(key); return null; } catch (Exception e) { System.out.println("远程服务获取结果失败,错误信息:{}"+e.getMessage()); referenceConfigMap.remove(key); return null; } }
管理台
作为微服务框架,Dubbo具备服务的实时监控、服务治理等功能。dubbo-control-plane支持可视化的展示、监控集群状态,还支持实时下发流量管控规则
Dubbo与SpringCloud Feign对比
- 通信效率:
Dubbo采用短连接的方式,通信效率较高。而Feign基于Http传输协议,通信效率相对较低。在实际应用中,如果服务调用频繁且对延迟要求较高,Dubbo可能更适合。
- 负载均衡算法:
Dubbo支持4种负载均衡算法(随机、轮询、活跃度、Hash一致性),并引入权重的概念,可以根据实际需求进行灵活配置。而Feign只支持轮询、随机、ResponseTime加权等策略,负载均衡算法是Client级别的。因此,在负载均衡方面,Dubbo具有更高的灵活性和可扩展性。
- 容错机制:
Dubbo支持多种容错策略,如failover、failfast、broadcast、forking等,并引入了retry次数、timeout等配置参数。而Feign则利用熔断机制来实现容错。在实际应用中,根据具体需求选择合适的容错策略,可以提高系统的稳定性和可用性。
- 集成与扩展:
Dubbo作为一个独立的框架,与其他系统的集成可能需要额外的开发工作。而Feign与Spring Cloud等微服务框架集成良好,可以方便地与其他系统协同工作。此外,Feign还提供了丰富的扩展点,方便开发者根据实际需求进行定制开发。
综上所述,Dubbo和Feign在性能上各有优势。如果你的项目已经选择了Spring Cloud或微服务架构,并且主要依赖于HTTP/HTTPS协议的RESTful API调用,那么使用Feign可能更加合适。然而,如果你的项目对通信效率、负载均衡算法和容错机制有较高要求,或者需要与其他系统进行集成,那么Dubbo可能是一个更好的选择。
Dubbo3新特性
主要分为以下几个方面:
注册模型的改变
- HTTP1.x协议中,多余无用的字符太多了,比如回车符、换行符,这每一个字符都会占用一个字节,这些字节占用了网络带宽,降低了网络IO的效率
- HTTP1.x协议中,一条Socket连接,一次只能发送一个HTTP请求,因为如果连续发送两个HTTP请求,然后收到了一个响应,那怎么知道这个响应对应的是哪个请求呢,这样导致Socket连接的利用低,并发、吞吐量低。
- triple协议基于的是HTTP2,rest协议目前基于的是HTTP1,都可以做到跨语言。
- triple协议兼容了gPRC(Triple服务可以直接调用gRPC服务,反过来也可以),rest协议不行
- triple协议支持流式调用,rest协议不行
- rest协议更方便浏览器、客户端直接调用,triple协议不行(原理上支持,当得对triple协议的底层实现比较熟悉才行,得知道具体的请求头、请求体是怎么生成的)
- dubbo协议是Dubbo3.0之前的默认协议,triple协议是Dubbo3.0之后的默认协议,优先用Triple协议
- dubbo协议不是基于的HTTP,不够通用,triple协议底层基于HTTP所以更通用(比如跨语言、跨异构系统实现起来比较方便)
- dubbo协议不支持流式调用
Triple协议的流式调用
Stream 是 Dubbo3 新提供的一种调用类型,在以下场景时建议使用流的方式:
- 接口需要发送大量数据,这些数据无法被放在一个 RPC 的请求或响应中,需要分批发送,但应用层如果按照传统的多次 RPC 方式无法解决顺序和性能的问题,如果需要保证有序,则只能串行发送
- 流式场景,数据需要按照发送顺序处理, 数据本身是没有确定边界的
- 推送类场景,多个消息在同一个调用的上下文中被发送和处理
public interface UserService { // UNARY String sayHello(String name); // SERVER_STREAM default void sayHelloServerStream(String name, StreamObserver<String> response) { } // CLIENT_STREAM / BI_STREAM default StreamObserver<String> sayHelloStream(StreamObserver<String> response) { return response; } }
<dependency> <groupId>org.apache.dubbo</groupId> <artifactId>dubbo-rpc-triple</artifactId> <version>3.0.7</version> </dependency>
UNARY
// UNARY @Override public String sayHello(String name) { return "Hello " + name; }
Consumer定义:
String result = userService.sayHello("dubbo3");
SERVER_STREAM
Provicer定义:
// SERVER_STREAM @Override public void sayHelloServerStream(String name, StreamObserver<String> response) { response.onNext(name + " hello"); response.onNext(name + " world"); response.onCompleted(); }
Consumer定义:
userService.sayHelloServerStream("dubbo", new StreamObserver<String>() { @Override public void onNext(String data) { // 服务端返回的数据 System.out.println(data); } @Override public void onError(Throwable throwable) {} @Override public void onCompleted() { System.out.println("complete"); } });
CLIENT_STREAM
Provider定义:
// CLIENT_STREAM @Override public StreamObserver<String> sayHelloStream(StreamObserver<String> response) { return new StreamObserver<String>() { @Override public void onNext(String data) { // 接收客户端发送过来的数据,然后返回数据给客户端 response.onNext("result:" + data); } @Override public void onError(Throwable throwable) {} @Override public void onCompleted() { System.out.println("completed"); } }; }
Consumer定义:
StreamObserver<String> streamObserver = userService.sayHelloStream(new StreamObserver<String>() { @Override public void onNext(String data) { System.out.println("接收到响应数据:"+ data); } @Override public void onError(Throwable throwable) {} @Override public void onCompleted() { System.out.println("接收到响应数据完毕"); } }); // 发送数据 streamObserver.onNext("request dubbo hello"); streamObserver.onNext("request dubbo world"); streamObserver.onCompleted();
BI_STREAM
- Dubbo一开始是用Java语言实现的,那现在就需要一个go语言实现的Dubbo框架,也就是现在的dubbo-go,然后在go项目中引入dubbo-go,从而可以在go项目中使用dubbo,比如使用go语言去暴露和使用Dubbo服务。
- 我们在使用Java语言开发一个Dubbo服务时,会把服务接口和相关类,单独抽象成为一个Maven项目,实际上就相当于一个单独的jar包,这个jar能被Java项目所使用,但不能被go项目所使用,所以go项目中该如何使用Java语言所定义的接口呢?直接用是不太可能的,只能通过间接的方式来解决这个问题,除开Java语言之外,那有没有其他技术也能定义接口呢?并且该技术也是Java和go都支持,这就是protobuf。
syntax = "proto3"; package api; option go_package = "./;api"; option java_multiple_files = true; option java_package = "com.joey"; option java_outer_classname = "UserServiceProto"; service UserService { rpc GetUser (UserRequest) returns (User) {} } // The response message containing the greetings message UserRequest { string uid = 1; } // The response message containing the greetings message User { string uid = 1; string username = 2; }
@DubboService public class UserServiceImpl implements UserService { public User getUser(UserRequest userRequest) { User user = User.newBuilder().setUid(userRequest.getUid()).setUsername("dubbo").build(); return user; } }
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· CSnakes vs Python.NET:高效嵌入与灵活互通的跨语言方案对比
· DeepSeek “源神”启动!「GitHub 热点速览」
· 我与微信审核的“相爱相杀”看个人小程序副业
· Plotly.NET 一个为 .NET 打造的强大开源交互式图表库
· 上周热点回顾(2.17-2.23)