RPC与Dubbo
开篇:HTTP 调用和 RPC 调用有什么区别?
假设你在餐厅点菜。一种方式是你亲自走到后厨窗口,用普通话跟厨师说"来一份宫保鸡丁"——这就是 HTTP 调用,通用、谁都听得懂,但流程比较重(你得走过去、排队、大声说)。
另一种方式是你按下桌上的呼叫按钮,厨房的智能系统自动接收你的需求,分配给合适的厨师,做好了直接送到你桌上——这就是 RPC 调用,高效、透明,你甚至感觉不到后厨的存在。
RPC(Remote Procedure Call,远程过程调用)的核心目标就是:让调用远程服务像调用本地方法一样简单。而 Dubbo 是 RPC 框架中最成熟、功能最丰富的代表之一。
一、什么是 RPC?
RPC 是指计算机 A 上的进程调用计算机 B 上的进程的一种技术。调用方发起请求后被挂起等待结果,被调用方执行完毕后返回结果。对于开发者来说,整个过程是透明的——你写代码时感觉就像在调用一个本地方法。
1.1 为什么需要 RPC?
在分布式系统中,一个业务操作可能涉及多个服务:
下单 {
库存服务 -> 扣减库存
支付服务 -> 扣款
红包服务 -> 红包抵扣
物流服务 -> 生成物流单
}这些服务部署在不同的机器上,要互相调用就需要网络通信。如果每次都自己写网络连接、序列化、反序列化的代码,不仅复杂而且容易出错。RPC 框架就是把这些脏活累活全部封装起来,让你只关注业务逻辑。
1.2 RPC 的工作原理
一次完整的 RPC 调用经历以下步骤:
关键技术点:
- 动态代理:生成客户端和服务端的代理类,让调用方感觉在调本地方法。可以用 JDK 动态代理或 CGLIB/Javassist 等字节码工具。
- 序列化:将方法参数和返回值转换成字节流在网络上传输。常用的有 Hessian、Protobuf、Kryo、JSON 等。
- 网络通信:负责底层的数据传输。主流 RPC 框架基本都用 Netty 作为通信框架。
- 服务注册与发现:通过注册中心(ZooKeeper、Nacos 等)管理服务提供者的地址列表。
二、RPC vs HTTP
严格来说,RPC 和 HTTP 不是同一层面的概念。RPC 是一种远程调用的范式,HTTP 是一种传输协议。RPC 可以基于 HTTP 实现(如 gRPC 基于 HTTP/2),也可以基于自定义的 TCP 协议实现(如 Dubbo 协议)。
但在实际讨论中,人们常常把"RPC 调用"和"HTTP 调用"做对比,主要是比较像 Dubbo 这样的 RPC 框架和直接用 HTTP+JSON 这种方式的区别:
| 对比项 | RPC(如 Dubbo) | HTTP(如 RESTful) |
|---|---|---|
| 序列化 | 二进制协议(Hessian/Protobuf),紧凑高效 | 文本协议(JSON/XML),可读性好但体积大 |
| 协议头 | 自定义协议头,非常轻量 | HTTP Header 较重 |
| 连接方式 | 长连接 + 连接复用 | 短连接或 keep-alive |
| 服务治理 | 内置负载均衡、熔断、路由等 | 需要额外组件 |
| 跨语言 | 部分支持(gRPC 支持好) | 天然跨语言 |
| 适用场景 | 企业内部微服务通信 | 对外 API、跨组织调用 |
为什么 RPC 比 HTTP 快? 主要原因:
- 序列化效率:二进制协议比 JSON 紧凑得多,序列化/反序列化速度也快几倍。
- 协议开销小:自定义协议头比 HTTP Header 轻量很多。
- 长连接复用:避免了反复建立 TCP 连接的开销。
- 内部网络:RPC 用于企业内部,网络链路更短。
什么场景必须用 HTTP?
- 跨语言、跨平台(HTTP 兼容性最好)。
- 对外提供 API(不可能让外部系统装你的 RPC 客户端)。
- 跨公司、跨组织的调用。
三、Dubbo 架构
Dubbo 是阿里巴巴开源的高性能 RPC 框架,目前已是 Apache 顶级项目。它的架构围绕四个核心角色:
- Provider(服务提供者):启动时将自己提供的服务注册到注册中心(服务名、IP、端口、协议等)。
- Consumer(服务消费者):启动时从注册中心订阅所需的服务,获取提供者列表。
- Registry(注册中心):维护服务列表,服务变更时实时通知消费者。常用 ZooKeeper、Nacos。
- Monitor(监控中心):统计调用次数、调用时间等指标。
一次 Dubbo 调用的完整流程
- Consumer 发起调用,先经过 Proxy(动态代理)层。
- 经过 Filter 链,处理缓存、Mock 等逻辑。
- 通过 Cluster 层从 Directory 获取所有可用的 Provider 列表。
- 根据 LoadBalance 策略选出一个具体的 Provider。
- 再经过一层 Filter(上下文传递、限流、计数等)。
- 通过 Protocol 层选择协议,使用 Serialization 进行序列化。
- 通过 Transport 层(如 Netty)发送请求。
- Provider 接收到请求后反序列化,找到对应的服务实现执行方法。
- 将结果序列化后原路返回。
四、Dubbo 核心特性
4.1 SPI 机制
Dubbo 的扩展性建立在 SPI(Service Provider Interface)机制之上。Dubbo 的 SPI 和 JDK 的 SPI 有几个关键区别:
| 对比项 | JDK SPI | Dubbo SPI |
|---|---|---|
| 加载方式 | 全量加载所有实现 | 懒加载,按需加载 |
| 扩展名 | 无 | 支持键值对命名 |
| 依赖注入 | 不支持 | 自动注入其他扩展点 |
| AOP | 不支持 | 支持 Wrapper 包装 |
| 自动激活 | 不支持 | 按条件自动选择实现 |
Dubbo 不用 JDK SPI 的根本原因:JDK SPI 会加载所有实现类,不够灵活,性能也有损耗。Dubbo SPI 支持按需加载、依赖注入和 AOP,扩展性远超 JDK SPI。
4.2 负载均衡策略
Dubbo 提供客户端负载均衡,由 Consumer 根据算法选择 Provider:
| 策略 | 原理 | 适用场景 |
|---|---|---|
| 加权随机(默认) | 按权重随机选择 | 通用场景 |
| 轮询 | 按顺序逐个选择,支持权重 | 处理能力均匀的集群 |
| 最少活跃调用 | 优先选活跃调用数最少的 | 处理时间长短不一的场景 |
| 最短响应优先 | 优先选响应时间最短的 | 性能敏感场景 |
| 一致性哈希 | 相同参数总是路由到同一节点 | 需要会话亲和性的场景 |
配置方式:
// 注解方式
@DubboReference(loadbalance = "roundrobin")
private UserService userService;
<dubbo:reference interface="com.example.UserService" loadbalance="roundrobin" />4.3 容错策略
当调用失败时,Dubbo 提供多种容错机制:
| 策略 | 行为 | 适用场景 |
|---|---|---|
| Failover(默认) | 失败后自动切换到其他节点重试 | 读操作、幂等写操作 |
| Failfast | 失败后立即报错 | 非幂等写操作 |
| Failsafe | 失败后忽略错误 | 日志记录等不重要的操作 |
| Failback | 失败后记录,定时重发 | 消息通知等可以延迟的操作 |
| Forking | 并行调用多个节点,取最快的结果 | 实时性要求高的读操作 |
| Broadcast | 广播给所有节点 | 更新缓存等需要通知所有节点的操作 |
4.4 序列化协议
Dubbo 支持多种序列化协议:
- Hessian2(默认):跨语言的二进制协议,性能和兼容性平衡得好。
- Java 序列化:JDK 原生,性能较差,不推荐。
- Protobuf:Google 出品,性能极好,但需要预定义 .proto 文件。
- Kryo/FST:高性能 Java 序列化,不跨语言。
- Fury:蚂蚁出品,号称比 Hessian 快 100 倍。
4.5 通信协议
Dubbo 支持多种通信协议,默认使用 dubbo 协议(基于 Netty 的自定义 TCP 协议,单一长连接,NIO 异步通信)。
其他协议包括 RMI、Hessian、HTTP、gRPC、Triple(Dubbo 3 新增,兼容 gRPC)等。
4.6 泛化调用
正常调用需要依赖服务方提供的 API(JAR 包)。但在网关、测试平台等场景中,不可能提前引入所有服务的 JAR 包。
泛化调用允许在没有服务方 API 的情况下发起调用,只需知道服务的接口名、方法名和参数类型:
GenericService genericService = (GenericService) context.getBean("userService");
Object result = genericService.$invoke(
"getUserById", // 方法名
new String[]{"java.lang.Long"}, // 参数类型
new Object[]{1L} // 参数值
);4.7 异步调用
Dubbo 支持两种异步调用方式:
Consumer 端异步:服务方提供同步接口,调用方用 CompletableFuture 包装:
CompletableFuture<String> future = CompletableFuture.supplyAsync(
() -> someService.invoke("request")
);
future.whenComplete((result, error) -> {
// 处理结果
});Provider 端异步:服务方直接返回 CompletableFuture:
@DubboService
public class AsyncServiceImpl implements AsyncService {
@Override
public CompletableFuture<String> asyncInvoke(String param) {
return CompletableFuture.supplyAsync(() -> {
// 耗时操作
return "result: " + param;
});
}
}4.8 优雅停机
Dubbo 通过 JDK 的 ShutdownHook 和 Spring 的事件机制实现优雅停机:
Provider 端:先标记为不接受新请求,等待正在执行的线程完成。
Consumer 端:不再发起新调用,等待已发出的请求返回结果。
默认等待 10 秒,超时后强制关闭。
五、Dubbo 服务治理
Dubbo 的服务治理能力是其最大的竞争优势之一:
- 注册中心:支持 ZooKeeper、Nacos、Consul、Etcd 等。
- 负载均衡:五种内置策略,支持自定义扩展。
- 集群容错:六种容错策略,覆盖各种失败场景。
- 服务路由:按 IP、标签、版本等条件路由请求。
- 流量管控:A/B 测试、金丝雀发布、按比例分流。
- 服务降级:Provider 异常时自动降级。
- 监控:Dubbo Admin 可视化控制台。
- 安全:支持 TLS 加密传输。
六、Dubbo vs OpenFeign
| 对比项 | Dubbo | OpenFeign |
|---|---|---|
| 协议 | 自定义 TCP 协议(默认) | HTTP |
| 性能 | 高(二进制序列化 + 长连接) | 较低(JSON + HTTP) |
| 服务治理 | 非常丰富(内置) | 需要搭配其他组件 |
| 使用方式 | 接口 + 注解 | 接口 + 注解 |
| 跨语言 | Dubbo 3 的 Triple 协议支持 | 天然支持 |
| 生态 | 阿里生态(Nacos/Sentinel/Seata) | Spring Cloud 生态 |
| 适用场景 | 高性能内部通信 | HTTP 场景、与 Spring Cloud 配合 |
选择建议:如果是阿里生态(Nacos + Sentinel + Seata),用 Dubbo 性能更好;如果是 Spring Cloud 生态且对性能要求不极端,用 OpenFeign 更简单。两者也可以混用。
七、常见面试题精选
Q1:RPC 和 HTTP 有什么区别?
RPC 是远程调用的范式,HTTP 是传输协议,不是同一层面。通常比较的是"用 RPC 框架"和"用 HTTP+JSON"的区别:RPC 框架性能更好(二进制序列化、长连接、轻量协议头),且内置服务治理能力。HTTP 的优势是通用性和跨语言。
Q2:Dubbo 如何实现像本地方法一样调用远程服务?
核心是动态代理。Dubbo 在启动时扫描服务接口,生成代理类。调用接口方法时,代理类将参数序列化、通过网络发送到 Provider,Provider 反序列化后调用真实实现,再将结果原路返回。对开发者完全透明。
Q3:Dubbo 的 SPI 和 JDK 的 SPI 有什么区别?
JDK SPI 会加载所有实现类,不够灵活;Dubbo SPI 支持懒加载(按需加载)、键值对命名、依赖注入、AOP(Wrapper 包装)和条件自动激活。Dubbo 不用 JDK SPI 就是因为后者在性能和灵活性上不够。
Q4:Dubbo 支持哪些负载均衡策略?
加权随机(默认)、轮询、最少活跃调用、最短响应优先、一致性哈希。都是客户端负载均衡,由 Consumer 选择 Provider。
Q5:什么是泛化调用?
泛化调用是在没有服务方 API(JAR 包)的情况下发起远程调用,只需知道接口名、方法名和参数类型。通过 GenericService.$invoke() 实现。常用于网关和测试平台。
八、Dubbo 的缓存机制
Dubbo 提供了结果缓存机制,可以缓存调用结果以减少重复调用,提高性能。
服务端缓存
服务端缓存将方法返回结果缓存在内存中,下次相同请求直接返回缓存结果:
@DubboService(cache = "lru")
public class UserServiceImpl implements UserService {
@Override
public User getUserById(Long id) {
// 数据库查询
}
}支持三种缓存策略:
- LRU Cache:基于最近最少使用算法,空间满时淘汰最久未使用的缓存。
- Thread Local Cache:线程本地缓存,每个线程独享一份缓存。
- Concurrent Map Cache:基于 ConcurrentHashMap,支持并发读写,效率最高。
客户端缓存
客户端缓存将远程调用的返回结果缓存在消费方本地,避免重复的网络请求:
@DubboReference(cache = "lru")
private UserService userService;缓存虽然能提升性能,但要注意数据一致性问题:当服务端数据变更后,客户端缓存中的数据就过期了。建议只对不常变化的数据使用缓存。
九、Dubbo 的连接管理
Dubbo 在连接管理上做了精心设计:
Provider 与 Consumer 之间:默认使用 单一长连接,通过 NIO 异步通信实现多路复用。一个 TCP 连接可以同时处理多个请求和响应,不需要为每次调用建立新连接。
Provider 与 Registry 之间:以 ZooKeeper 为例,使用长连接 + Watch 机制。Provider 启动时通过长连接注册服务,Consumer 通过 Watch 监听服务列表变化,有变更时实时通知。
心跳机制:Dubbo 内置心跳检测,定期发送心跳包维持连接活性。如果超过一定时间没有收到心跳响应,就认为连接断开,进行重连。
十、开源 RPC 框架对比
| 框架 | 出品方 | 特点 | 语言支持 |
|---|---|---|---|
| Dubbo | 阿里/Apache | 功能最全面,服务治理能力强 | 主要 Java,3.0 支持多语言 |
| gRPC | 基于 HTTP/2 + Protobuf,跨语言能力强 | 多语言 | |
| Thrift | Facebook/Apache | 跨语言 RPC,自带 IDL | 多语言 |
| Motan | 新浪微博 | 轻量级,微博内部大规模使用 | Java |
| brpc | 百度 | C++ 实现,性能极高 | C++ |
在 Java 生态中,Dubbo 是最成熟的选择。如果需要跨语言,gRPC 是首选。Dubbo 3 的 Triple 协议也兼容了 gRPC,可以同时享受两者的优势。
十一、服务发现与服务路由
这两个概念容易混淆,但职责完全不同:
服务发现:回答"有哪些 Provider 可以调用"。Consumer 启动时从注册中心获取 Provider 列表,并监听变更。当有 Provider 上线或下线时,注册中心实时通知 Consumer 更新列表。
服务路由:回答"这次调用应该发给哪个 Provider"。在已知的 Provider 列表中,根据路由规则筛选出符合条件的 Provider 子集,然后再通过负载均衡选出一个具体的。
路由规则可以基于 IP、标签、版本等条件。典型应用场景:
- 灰度发布:新版本只路由 10% 的流量。
- 机房隔离:同机房的请求优先路由到同机房的 Provider。
- 版本隔离:不同版本的 Consumer 只调用对应版本的 Provider。
路由在负载均衡之前执行:先路由筛选,再负载均衡选择。
十二、什么场景只能用 HTTP?
虽然 RPC 在性能上有优势,但以下场景只能用 HTTP:
- 异构系统调用:对方是 Python、Go 等非 Java 服务,不一定支持 Dubbo 协议。
- 对外提供 API:不可能要求外部客户接入你的 RPC 框架。
- 跨组织/跨公司:无法共享注册中心,只能用更通用的 HTTP。
- 浏览器/APP 直接调用:前端只能发 HTTP 请求。
实际项目中,RPC 和 HTTP 往往共存:内部微服务之间用 Dubbo 高效通信,对外接口用 HTTP 提供 RESTful API。
小结
RPC 框架的核心目标是让远程调用像本地调用一样简单。Dubbo 作为最成熟的 Java RPC 框架,在以下几个方面表现出色:
- 高性能:自定义二进制协议 + Netty 长连接 + 高效序列化,吞吐量远超 HTTP。
- 丰富的服务治理:负载均衡、容错、路由、降级、监控一应俱全。
- 极致的扩展性:基于 SPI 机制,几乎所有组件都可以替换和扩展。
选择 RPC 还是 HTTP,取决于场景:内部高性能通信用 RPC(Dubbo),对外接口和跨组织调用用 HTTP(OpenFeign/RESTful)。两者不是替代关系,而是互补关系。