从 HTTP 到 RPC
微服务之间要通信,最简单的方式是 HTTP 接口:语义清晰、工具链丰富、排障直观。但服务多了以后,HTTP 的短板开始显形:每个请求都要做协议解析与序列化,JSON 文本体积大,网关与依赖链上延迟叠加明显,而且接口文档、重试、负载均衡这些基础设施每套服务都要自己搭。
RPC 远程过程调用把"跨进程调方法"伪装成"本地调方法":调用方写一个接口,框架负责序列化、传输、寻址、负载均衡与容错。Dubbo 是经典的 Java RPC 框架,阿里巴巴开源,与 Spring 生态结合紧密,国内微服务架构中应用极广。
- RPC让远程调用像本地方法一样简单。
- 协议Dubbo 协议与 Hessian 序列化。
- 注册中心服务发现与地址变更的枢纽。
- 集群容错失败重试、快速失败与熔断。
RPC 的核心要素
一个 RPC 框架至少包含四件套:通信协议负责在网络上传输字节,Dubbo 默认的 Dubbo 协议基于 TCP 长连接,配合 Hessian2 序列化,单连接多路复用,性能远好于 HTTP 短连接;序列化负责把对象变成字节流,Hessian2 体积小速度快,JSON 则更易读但性能差;注册中心负责服务地址的注册与发现;集群层负责负载均衡与容错。
调用链路上还有一个容易被忽视的角色:代理。客户端持有一个接口的代理对象,调用时代理把方法名、参数类型与参数值打包成请求,交给网络层发往目标服务;服务端收到后反序列化、找到对应实现、执行并返回结果。整个过程对业务代码完全透明。
<!-- provider 暴露接口 -->
<dubbo:service interface="com.demo.UserService"
ref="userServiceImpl" version="1.0.0"/>
<!-- consumer 引用接口 -->
<dubbo:reference id="userService"
interface="com.demo.UserService"
version="1.0.0" timeout="3000"/>
第一个 Dubbo 服务
搭建一个 Dubbo 服务只需要三步:定义接口、实现接口、配置暴露与引用。接口是契约,provider 和 consumer 都要依赖同一份接口定义,最好抽成独立的 API 模块,避免两端接口漂移。
public interface UserService {
User findById(Long id);
}
@Service
public class UserServiceImpl implements UserService {
public User findById(Long id) {
return userRepo.findById(id);
}
}
启动 provider 后,服务信息会写入注册中心;consumer 启动时从注册中心拉取地址列表,建立连接后就可以注入代理直接调用。第一次调通后,再补上版本号、分组与超时配置,接口升级用 version 区分新旧实现,实现平滑发布。
注册中心与服务发现
没有注册中心的 RPC 只能把地址写死在配置里,扩容要改配置重启,故障节点要人工摘除。注册中心解决两个问题:地址的注册与发现、状态的健康检查。Dubbo 支持多种注册中心,ZooKeeper 用临时节点感知上下线,Nacos 提供更完整的注册配置能力,还有基于 Redis 的轻量实现。
服务发现的关键指标是收敛时间:节点挂了,多久能被其他消费者感知并摘除。ZooKeeper 的临时节点靠会话超时判定,默认可能几十秒;Nacos 支持心跳与秒级摘除。线上故障演练时,这个收敛时间直接决定故障影响面。
# 消费者视角的发现流程
1. 从注册中心拉取 provider 地址列表
2. 订阅变更通知,地址增删实时刷新
3. 按负载均衡策略选出一个地址发起调用
4. 调用失败触发容错策略
负载均衡与集群容错
Dubbo 内置多种负载均衡:随机按权重,平滑加权轮询适合均等流量,一致性哈希让相同参数落到同一节点以利用本地缓存,最少活跃数把请求分给处理快的节点。默认的随机加权在大多数场景够用,缓存敏感的服务建议一致性哈希。
集群容错决定调用失败后怎么办:Failover 失败自动换节点重试,适合读操作;Failfast 立即失败抛异常,适合写操作避免重复;Failsafe 失败静默吞掉,适合日志上报;Forking 并行调用多个节点取最快结果,适合对延迟极敏感的场景。重试次数要克制,重试本质是放大流量,设置不当会把雪崩引到自己身上。
@DubboReference(cluster = "failover", retries = 2)
private OrderService orderService;
@DubboReference(cluster = "failfast")
private PaymentService paymentService;
超时、重试与治理
RPC 最大的坑是超时与重试的联动。consumer 超时 3 秒,provider 慢查询 5 秒,consumer 发起重试后,两个请求同时压在 provider 上,慢接口变成更慢接口。治理原则是:consumer 超时略大于 provider 的预期处理时间;重试只用于可幂等且临时失败的场景;链路上设置全局超时预算,防止请求在服务间无限传递。
- 超时逐接口配置,链路总预算封顶。
- 重试只对幂等读重试,写操作不重试。
- 降级熔断后返回兜底数据或空结果。
# 慢调用治理示意
dubbo.consumer.timeout=3000 # 默认超时
dubbo.consumer.retries=0 # 默认不重试
dubbo.provider.timeout=5000 # provider 侧兜底
配合 Sentinel 或自研熔断,把失败率、慢调用比例作为熔断信号,开闸后进入半开状态试探恢复。治理的目标不是消灭失败,而是让失败可控、可观测、不放大。
落地清单
RPC 改造是一把手工程,牵一发动全身。先挑一个调用频率高、对性能敏感的链路试点,验证序列化、连接数与超时配置,再逐步推广。上线前把下面的清单过一遍:接口定义抽独立模块,禁止两端各自维护;provider 设置合理的线程池与连接数,防止慢调用耗尽资源;consumer 超时与重试按接口精细配置;注册中心加权限与隔离,避免测试环境互相干扰;全链路埋点接上 trace,RPC 的每次调用都要能追溯到调用方与耗时。
Dubbo 的价值不在框架本身,而在它强制你思考服务契约与调用治理。把接口当契约、把超时当预算、把失败当信号,这套方法论在任何 RPC 框架下都成立。
- 接口契约独立成模块,版本化发布。
- 超时按接口配置,链路预算封顶。
- 重试仅限幂等读,写操作 failfast。
- 接入全链路追踪,调用可追溯。