面试知识库
困难

Dubbo-RPC原理#

一句话答案#

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

核心要点

核心对比表:

维度REST (HTTP/JSON)gRPCDubbo
传输协议HTTP/1.1(文本)HTTP/2(二进制帧)自定义 TCP 协议(Dubbo Protocol)
序列化JSON(文本,可读性强)Protobuf(二进制,体积小 3-10 倍)Hessian2 / Protobuf / JSON
IDLOpenAPI / Swagger(可选).proto 文件(强制)Java 接口(强制)
代码生成可选(Swagger CodeGen)强制(protoc 编译器生成客户端/服务端代码)强制(共享 API JAR 包)
性能低(JSON 解析慢,HTTP/1.1 连接开销)高(二进制序列化 + HTTP/2 多路复用)高(长连接 + 二进制序列化)
流式传输不支持(SSE/WebSocket 是补充方案)原生支持(单向流、双向流)不支持(2.x 有异步调用)
跨语言天然跨语言(HTTP + JSON)强跨语言(官方支持 10+ 语言)弱(主要 Java,Go 版本不成熟)
服务治理无内置(需搭配 Spring Cloud)无内置(需自行集成)内置丰富(负载均衡、熔断、路由)
浏览器调用支持不支持(需 gRPC-Web 代理)不支持
可读性/调试高(curl、Postman 直接调试)低(二进制,需专用工具)低(需要 Dubbo Admin)
生态最广泛(所有语言、所有框架)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 默认)把 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分钟版)

微服务间的通信方式主要有三种:REST、gRPC 和 Dubbo。REST 基于 HTTP/JSON,通用性最强,跨语言、可读性好,用 curl 就能调试,适合对外暴露 API 和跨团队协作,但性能相对较低。gRPC 基于 HTTP/2 和 Protobuf 二进制序列化,性能高、体积小、原生支持流式传输,适合多语言微服务间的高性能内部通信。Dubbo 是阿里开源的 RPC 框架,调用链路是代理拦截、负载均衡选节点、Hessian2 序列化、Netty 网络传输、反序列化执行返回,自定义 TCP 协议长连接性能高,最大优势是内置丰富的服务治理能力包括负载均衡、熔断、路由、权重等,适合以 Java 为主的大规模微服务集群。选型建议是对外 API 用 REST,内部高频调用按技术栈选 gRPC 或 Dubbo——多语言混合架构选 gRPC,纯 Java 且需要丰富服务治理选 Dubbo。Dubbo 和 Feign 的关键区别是 Dubbo 走自定义 RPC 协议而 Feign 走 HTTP。

追问与易错

追问方向:

  • “Dubbo SPI 和 Java SPI 区别?”→ Java SPI 一次性加载所有实现类,无法按需加载;Dubbo SPI 支持按 key 按需加载、支持 IoC 和 AOP(自动注入和 Wrapper 增强)、支持 @Adaptive 自适应扩展,扩展性更强
  • “Dubbo 服务降级怎么做?”→ 在消费端配置 mock 参数:mock=return+null 直接返回空值、mock=fail:return+null 失败时降级、mock=force:return+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,读不够就等下一个包
  • “@Adaptive 是怎么做到运行时按参数选实现的?”→ ExtensionLoader 用 Javassist 动态生成一个适配类,方法体里从 URL 取参数 key(如 loadbalance),再 getExtension(key) 派发到真实实现,等于把「选哪个实现」延迟到调用时由 URL 决定

易错点:

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