面试知识库
高 困难

Dubbo-RPC原理#

一句话答案#

Dubbo RPC 调用链:代理拦截→负载均衡选节点→序列化(Hessian2)→网络传输(Netty)→反序列化→执行→返回。

核心要点

核心对比表:

维度REST (HTTP/JSON)gRPCDubbo
传输协议HTTP/1.1(文本)HTTP/2(二进制帧)自定义 TCP 协议(Dubbo Protocol);Dubbo 3 新增 Triple(基于 HTTP/1、HTTP/2,兼容 gRPC)
序列化JSON(文本,可读性强)Protobuf(二进制,体积小 3-10 倍)Hessian2 / Protobuf / JSON
IDLOpenAPI / Swagger(可选).proto 文件(强制)Java 接口(强制)
代码生成可选(Swagger CodeGen)强制(protoc 编译器生成客户端/服务端代码)强制(共享 API JAR 包)
性能低(JSON 解析慢,HTTP/1.1 连接开销)高(二进制序列化 + HTTP/2 多路复用)高(长连接 + 二进制序列化)
流式传输不支持(SSE/WebSocket 是补充方案)原生支持(单向流、双向流)dubbo 协议不支持(2.x 只有异步调用);Dubbo 3 Triple 支持服务端流、双向流
跨语言天然跨语言(HTTP + JSON)强跨语言(官方支持 10+ 语言)dubbo 协议偏 Java;Dubbo 3 Triple 兼容 gRPC,另有 dubbo-go、dubbo-js 等多语言实现
服务治理无内置(需搭配 Spring Cloud)无内置(需自行集成)内置丰富(负载均衡、熔断、路由)
浏览器调用支持不支持(需 gRPC-Web 代理)dubbo 协议不支持;Triple 可用 HTTP 直接调用
可读性/调试高(curl、Postman 直接调试)低(二进制,需专用工具)dubbo 协议低(需要 Dubbo Admin/telnet);Triple 可用 curl 调试
生态最广泛(所有语言、所有框架)Google、CNCF 生态阿里巴巴、Apache 生态

选型建议:

gRPC Protobuf 示例:

Dubbo 调用原理(深挖)#

上面是选型对比,这一节才是「Dubbo-RPC 原理」真正要答的内容:协议帧、调用链路、异步转同步、SPI。

协议帧:16 字节定长头 + 变长 body

Dubbo 协议是「私有 TCP 协议」,每个报文 = 16 字节定长 Header + 变长 Body:

字节偏移字段说明
0~1Magic(魔数 0xdabb)标识 Dubbo 协议,用于粘包/半包时找帧边界
2标志位 Flag高 1 位 Req/Res(请求还是响应)、1 位 Two-Way(是否需要返回值)、1 位 Event(心跳等事件包)、低 5 位 序列化标识 ID(Hessian2=2、FastJson、Kryo…)
3Status仅响应包用,标识 OK / 超时 / 服务端异常等状态码
4~11Request ID(8 字节)全局唯一请求号,异步转同步的核心,用来把响应和请求对上
12~15Data Length(4 字节)Body 长度,解决 TCP 粘包/半包:读满 16 字节头拿到 length,再精确读 length 字节 body

为什么需要 Magic + Length?TCP 是字节流没有消息边界,靠魔数定位帧头、靠长度字段切出完整一帧,这是所有私有协议解决粘包的标准做法。

调用链路(消费端一次同步调用穿过的层)

业务接口(Proxy 代理,JDK/Javassist 动态代理)
   ↓ 拦截方法调用,封装成 Invocation(方法名/参数类型/参数值/attachment)
Cluster(集群容错层)—— Failover 失败重试 / Failfast / Failsafe / Forking
   ↓ 从 Directory 拿到可用 Invoker 列表,交给路由+负载均衡
LoadBalance(选址)—— Random / RoundRobin / LeastActive / ConsistentHash
   ↓ 选出一个 Invoker(一个 Invoker ≈ 一个可调用的远程服务地址)
Protocol → DubboInvoker(协议层,发起真正调用)
   ↓
序列化(Hessian2 默认;3.2.x 曾把默认改成 fastjson2,3.3 又改回 Hessian2)把 Invocation 编码成字节
   ↓
Netty Client 发送(长连接,单连接多路复用)
plaintext

服务端收到后反向走一遍:Netty 解码 → 反序列化出 Invocation → 找到对应 Exporter/Invoker → 反射执行真实方法 → 结果序列化回写。

两个核心抽象:Invoker 与 Invocation

  • Invocation:一次调用的「请求参数包」——方法名、参数类型、参数值、附加隐式参数(attachment,如 traceId)。
  • Invoker:Dubbo 最核心的抽象,代表「一个可执行的调用实体」,Result invoke(Invocation)。消费端的 Invoker 指向远程服务,服务端的 Invoker 包裹真实实现类。Cluster、Filter、Listener 全是对 Invoker 的层层包装(装饰器模式),所以拦截链、Mock 降级、监控统计都能透明插入。

异步转同步:DefaultFuture + reqId 关联

Dubbo 底层 Netty 调用天生是异步的(发出去就返回,响应稍后由 IO 线程回调),但业务接口看起来是同步阻塞的。怎么把异步变同步?

  1. 发送前生成全局唯一 Request ID,创建一个 DefaultFuture 并以 reqId → future 存入全局 FUTURES Map。
  2. 把请求写进 Netty 发出去,业务线程在 future.get(timeout) 上阻塞等待。
  3. 服务端响应回来时,IO 线程读出响应头里的 Request ID,从 Map 里找到对应的 DefaultFuture,调用 received() 填入结果并唤醒阻塞的业务线程。
  4. 超时由时间轮(HashedWheelTimer)扫描未完成的 future,到点设置超时异常。

单连接多路复用为何不乱? 一条 TCP 长连接上并发跑着成百上千个请求,响应回来的顺序和发送顺序不一定一致。靠 Request ID 把每个响应精确对应回它的请求,所以乱序也不会错乱——这就是 reqId 的根本作用。

SPI 机制(Dubbo 微内核可扩展的基石)

Dubbo 几乎所有组件(Protocol、LoadBalance、Cluster、序列化…)都是 SPI 扩展点,靠 ExtensionLoader 加载:

  • 配置位置:META-INF/dubbo/ 下以接口全限定名为文件名,内容是 key=实现类全限定名,按 key 懒加载(区别于 Java SPI 一次性实例化全部)。
  • @Adaptive 自适应:Dubbo 在运行时根据 URL 参数(如 loadbalance=roundrobin)决定用哪个实现。ExtensionLoader 会动态生成一个适配类(XxxProtocol$Adaptive,Javassist 字节码生成),方法体里从 URL 取参数 key、再 getExtension(key) 派发到真实实现——实现「运行时按参数选实现」。
  • @Activate 自动激活:一批同类扩展(典型是 Filter 链)按条件批量激活,比如 @Activate(group="consumer") 的 Filter 只在消费端生效,无需手动声明。
  • IoC + AOP:扩展实例的 setter 依赖会被 SPI 自动注入(IoC);带包装构造器的 Wrapper 类会自动套在真实扩展外层(AOP),Dubbo 的监控、缓存等增强就是 Wrapper。

面试回答(2分钟版)

Dubbo 是阿里开源、现属 Apache 的 RPC 框架,一次同步调用的链路是:消费端通过动态代理拦截接口方法,封装成 Invocation;Cluster 层从 Directory 拿到可用 Invoker 列表,经路由和负载均衡(默认加权随机)选出一个节点,失败按 Failover 等容错策略处理;然后 DubboInvoker 用 Hessian2 序列化,经 Netty 长连接发出。协议是 16 字节定长头加变长 body,头里有魔数 0xdabb 和 body 长度,用来解决 TCP 粘包半包;还有 8 字节 Request ID,发送时把 reqId 和 DefaultFuture 放进全局 Map,业务线程阻塞在 future 上,响应回来 IO 线程按 reqId 找到 future 唤醒,这就是异步转同步,也是单连接多路复用不乱序的原因,超时由时间轮检测。整个框架是微内核加 SPI:Protocol、LoadBalance、Cluster、Filter 都是扩展点,按 key 懒加载,@Adaptive 在运行时按 URL 参数选实现,@Activate 批量激活 Filter。Dubbo 3 又加了 Triple 协议(兼容 gRPC、支持流式)和应用级服务发现。选型上,对外 API 用 REST,内部纯 Java 且要丰富治理选 Dubbo,多语言混合选 gRPC 或 Dubbo Triple。

追问与易错

追问方向:

  • “Dubbo SPI 和 Java SPI 区别?”→ Java SPI 一次性加载所有实现类,无法按需加载;Dubbo SPI 支持按 key 按需加载、支持 IoC 和 AOP(自动注入和 Wrapper 增强)、支持 @Adaptive 自适应扩展,扩展性更强
  • “Dubbo 服务降级怎么做?”→ 在消费端配置 mock 参数:mock=fail:return+null 先调远程、失败(RpcException)后返回 null(不写前缀的 mock=return+null 等同于 fail 策略);mock=force:return+null 强制降级、直接返回 null 不调用远程;也可实现 Mock 类做复杂降级逻辑
  • “Dubbo 和 Spring Cloud 怎么选?”→ Dubbo 偏 RPC 通信层,性能高(二进制协议+长连接),适合内部服务间高性能调用;Spring Cloud 是完整微服务生态(网关、配置、熔断一站式),适合 REST 风格和快速搭建;两者可混用
  • “Dubbo 一条 TCP 连接并发那么多请求,响应回来怎么不乱?”→ 协议头里有 8 字节全局唯一 Request ID,发送时把 reqId→DefaultFuture 存进全局 Map,业务线程阻塞在 future.get;响应回来 IO 线程按响应里的 reqId 找到对应 future 唤醒,所以单连接多路复用乱序也能精确对应
  • “Dubbo 怎么解决 TCP 粘包/半包?”→ 16 字节定长头里有魔数 0xdabb 定位帧边界 + 4 字节 body length,先读满头拿到长度再精确读 body,读不够就等下一个包
  • “Dubbo 3 相比 2.x 主要变了什么?”→ ① 新增 Triple 协议:基于 HTTP/1、HTTP/2,兼容 gRPC,支持流式,可以直接用 Java 接口定义不强制 Protobuf,解决了 dubbo 协议跨语言和网关穿透差的问题;② 应用级服务发现:注册中心按应用而不是按接口注册,地址数据量大幅下降,也便于和 Spring Cloud、K8s 互通,迁移期默认双注册(接口级 + 应用级);③ 云原生支持(Proxyless Mesh、K8s 服务发现)
  • “@Adaptive 是怎么做到运行时按参数选实现的?”→ ExtensionLoader 用 Javassist 动态生成一个适配类,方法体里从 URL 取参数 key(如 loadbalance),再 getExtension(key) 派发到真实实现,等于把「选哪个实现」延迟到调用时由 URL 决定

易错点:

  • ❌ Dubbo 和 Feign 一样——Dubbo 是 RPC Feign 是 HTTP
  • ❌ Dubbo 只能用 ZK——支持多种注册中心