从 HTTP 到 RPC

微服务之间要通信,最简单的方式是 HTTP 接口:语义清晰、工具链丰富、排障直观。但服务多了以后,HTTP 的短板开始显形:每个请求都要做协议解析与序列化,JSON 文本体积大,网关与依赖链上延迟叠加明显,而且接口文档、重试、负载均衡这些基础设施每套服务都要自己搭。

RPC 远程过程调用把"跨进程调方法"伪装成"本地调方法":调用方写一个接口,框架负责序列化、传输、寻址、负载均衡与容错。Dubbo 是经典的 Java RPC 框架,阿里巴巴开源,与 Spring 生态结合紧密,国内微服务架构中应用极广。

本文核心知识点
  • RPC让远程调用像本地方法一样简单。
  • 协议Dubbo 协议与 Hessian 序列化。
  • 注册中心服务发现与地址变更的枢纽。
  • 集群容错失败重试、快速失败与熔断。
RPC 与 Dubbo 配图:蓝紫渐变背景中央发光菱形核心,左右两侧各三行纯色圆角方块阵列以水平直线连接,扁平科技插画
菱形核心连接两侧服务方阵,正是 RPC 服务总线与调用方的呼应。

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 或自研熔断,把失败率、慢调用比例作为熔断信号,开闸后进入半开状态试探恢复。治理的目标不是消灭失败,而是让失败可控、可观测、不放大。