RPC 面试
RPC 面试
RPC 简介
【简单】什么是 RPC?RPC 有什么用?⭐⭐⭐
🎯 目标等级:L1 | ⏱ 建议用时:5 min | 🏷 标签:分布式通信 / RPC 概念
💎 关键结论
RPC(Remote Procedure Call,远程过程调用)就是让你"像调用本地方法一样调用远程方法",把网络通信的复杂性藏起来。理由:它屏蔽了远程调用与本地调用的差异,开发者只需聚焦业务逻辑本身。
⚡记忆卡片
- 口诀:像调本地一样调远程,通信细节全屏蔽。
- 关键词:远程过程调用/本地化体验/微服务通信基石
- 链路:业务代码调用接口方法 → 框架接管并屏蔽网络细节 → 完成远程调用 → 开发者无感知、聚焦业务
📖 核心知识
- RPC 的全称是 Remote Procedure Call,即远程过程调用。
- RPC 的主要作用是:
- 屏蔽远程调用跟本地调用的差异,让用户像调用本地一样去调用远程方法。
- 隐藏底层网络通信的复杂性,让用户更聚焦于业务逻辑。
- RPC 是微服务架构的基石,它提供了一种应用间通信的方式。
🔬 扩展知识
详情
- 【L3】RPC 与 REST 的差异:REST 基于 HTTP + JSON,面向资源、通用性强、调试方便;RPC 面向方法调用,协议更紧凑、性能更高,更适合内部服务间的高频调用。
- 【L4】在云原生演进中,RPC 的流量治理能力(路由、限流、熔断)正逐步从 SDK 下沉到 Service Mesh 的 Sidecar 代理中,应用本身只保留最基础的调用能力。
🔀 发散问题
RPC 具体是怎么跑通一次调用的? 核心是把方法、参数序列化后按约定协议通过网络传输,服务端还原并执行后原路返回,详见本文档『RPC 是怎样工作的?』。
既然有 HTTP,为什么还要设计 RPC 协议? 因为 HTTP 报文冗余多、无状态,难以满足高性能 RPC 对紧凑协议和连接复用的要求,详见本文档『为何需要 RPC 协议?』。
为什么远程调用能做到"像本地一样"? 关键在于动态代理拦截了方法调用并替换为远程调用逻辑,详见本文档『RPC 如何将远程调用转为本地调用的?』。
【中等】RPC 是怎样工作的?⭐⭐
🎯 目标等级:L2 | ⏱ 建议用时:10 min | 🏷 标签:分布式通信 / RPC 工作流程
💎 关键结论
RPC 的工作流程可以拆成"序列化、协议、传输、动态代理"几个环节:调用方把方法参数序列化为二进制,按双方约定的协议封装后通过网络(一般是 TCP)传输,服务端反序列化后调用本地方法,响应原路返回。理由:网络中只能传二进制,双方必须对报文格式达成共识才能互相识别。
⚡记忆卡片
- 口诀:代理拦截、序列化封装、网络传输、对端还原、本地执行。
- 关键词:序列化/反序列化/协议(数据头+消息体)/TCP 传输/动态代理
- 链路:方法调用被动态代理拦截 → 参数序列化为二进制 → 按协议封装消息 → 网络传输到服务端 → 反序列化还原参数 → 执行本地方法 → 响应原路返回
📖 核心知识
RPC 是一种应用间通信的方式,它的通信流程中需要注意以下环节:
- 传输方式:RPC 是一个远程调用,因此必然需要通过网络传输数据,且 RPC 常用于业务系统之间的数据交互,需要保证其可靠性,所以 RPC 一般默认采用 TCP 来传输。
- 序列化:在网络中传输的数据只能是二进制数据,而 RPC 请求时,发送的都是对象。因此,请求方需要将请求参数转为二进制数据,即序列化。
- 反序列化:RPC 响应方接受到请求,要将二进制数据转换为请求参数,需要反序列化。
- 协议:请求方和响应方要互相识别彼此的信息,需要约定好彼此数据的格式,即协议。大多数的协议至少分成两部分,分别是数据头和消息体。数据头一般用于身份识别,包括协议标识、数据大小、请求类型、序列化类型等信息;消息体主要是请求的业务参数信息和扩展属性等。
- 动态代理:为了屏蔽底层通信细节,使用户聚焦自身业务,因此 RPC 框架一般引入了动态代理,通过依赖注入等技术,拦截方法调用,完成远程调用的通信逻辑。

一次完整 RPC 调用的 9 个步骤:
- 服务消费方(client)调用以本地调用方式调用服务;
- client stub 接收到调用后负责将方法、参数等组装成能够进行网络传输的消息体;
- client stub 找到服务地址,并将消息发送到服务端;
- server stub 收到消息后进行解码;
- server stub 根据解码结果调用本地的服务;
- 本地服务执行并将结果返回给 server stub;
- server stub 将返回结果打包成消息并发送至消费方;
- client stub 接收到消息,并进行解码;
- 服务消费方得到最终结果。
🔬 扩展知识
详情
- 【L3】按交互方式,RPC 调用可分为同步调用(调用线程阻塞等待响应)、异步调用(通过 Future/回调获取结果)与单向调用 Oneway(只发送不等响应),三者底层通信流程一致,差异在于调用端对响应的处理方式。
- 【L4】基于 HTTP/2 的 RPC 协议(如 gRPC、Dubbo3 Triple)可以在一条连接上多路复用多个请求流,将上述流程中的"连接管理"从一问一答升级为流式并发,显著提升单机吞吐。
🔀 发散问题
序列化环节怎么选? 要在安全性、兼容性、性能、易用性之间权衡,详见本文档『什么是序列化?有哪些常见的序列化方式?』。
协议头应该包含哪些字段? 至少有协议标识、消息长度、请求类型、序列化类型等,详见本文档『设计一个 RPC 协议的要点?』。
调用端怎么做到不阻塞? 通过 Future 与消息 ID 的映射在响应到达时回填结果,详见本文档『如何实现 RPC 异步调用?』。
RPC 框架对比
【中等】主流 RPC 框架有哪些?它们有什么区别?⭐⭐
🎯 目标等级:L2 | ⏱ 建议用时:10 min | 🏷 标签:分布式通信 / RPC 框架选型
💎 关键结论
主流 RPC 框架各有侧重:Java 生态重服务治理选 Dubbo,跨语言与云原生选 gRPC,多语言高性能二进制通信选 Thrift,Spring 全家桶选 Spring Cloud。理由:选型的核心是语言生态、协议性能与服务治理能力三者的匹配。
⚡记忆卡片
- 口诀:Java 治理用 Dubbo,跨语言云原生用 gRPC,二进制高效用 Thrift,Spring 生态用 OpenFeign。
- 关键词:Dubbo/gRPC/Thrift/Motan/Spring Cloud
- 链路:明确语言生态与场景 → 对比协议/序列化/治理能力 → 选出框架 → 落地微服务通信
📖 核心知识
目前主流的 RPC 框架有 Dubbo、gRPC、Thrift、Motan 等,它们在设计理念、生态、跨语言支持等方面各有侧重。
| 框架 | 出品方 | 跨语言 | 序列化默认协议 | 服务治理 | 通信协议 | 适用场景 |
|---|---|---|---|---|---|---|
| Dubbo | 阿里/Apache | 否(Java 为主,Dubbo3 支持多语言) | Hessian2 | 完善 | TCP/HTTP/HTTP2 | Java 微服务体系,重服务治理 |
| gRPC | 是 | Protobuf | 弱 | HTTP/2 | 跨语言通信,云原生场景 | |
| Thrift | 是 | Thrift | 弱 | TCP | 跨语言,二进制高效通信 | |
| Motan | 微博 | 否 | Hessian2 | 中等 | TCP | 中小规模 Java 微服务 |
| Spring Cloud | Pivotal | 否(HTTP) | JSON | 完善 | HTTP | Spring 生态,REST 风格微服务 |
选型建议:
- 纯 Java 体系,重服务治理 → Dubbo(生态完善,国内使用广泛)
- 跨语言通信,云原生 → gRPC(Protobuf 高效,HTTP/2 多路复用)
- 多语言、对协议紧凑性要求高 → Thrift(IDL 定义清晰,性能优秀)
- Spring 全家桶生态 → Spring Cloud OpenFeign(HTTP+JSON,开发体验好)
Dubbo 与 gRPC 的核心差异:
- 定位不同:Dubbo 定位是微服务框架(包含服务治理、注册发现、配置中心等),gRPC 定位是通信协议与 RPC 框架。
- 协议不同:Dubbo 默认使用 Dubbo2 协议(基于 TCP),Dubbo3 引入 Triple 协议(兼容 gRPC);gRPC 基于 HTTP/2 + Protobuf。
- 服务治理:Dubbo 内置丰富的服务治理能力(路由、限流、集群容错),gRPC 需要配合 Istio、xDS 等实现。
- 跨语言:gRPC 原生支持多语言;Dubbo 主要面向 Java,Dubbo3 开始支持 Go、Rust、Node 等多语言 SDK。
🔬 扩展知识
详情
- 【L3】IDL(接口定义语言)是跨语言框架的关键差异:gRPC/Thrift 依赖
.proto/.thrift预定义接口并生成代码,契约清晰但开发多一步;Dubbo 以 Java 接口直接定义服务,开发更自然但跨语言能力较弱。 - 【L4】Dubbo3 与 gRPC 在协议层已打通(Triple 兼容 gRPC),这意味着两套体系可以在同一网格内互通,是企业从 Dubbo2 平滑迁移到云原生体系的常见路径。
🔀 发散问题
Dubbo 和 gRPC 在协议层面的差异是什么? Dubbo2 协议基于 TCP 私有协议,Triple/gRPC 基于 HTTP/2,详见本文档『RPC 通信应该选择 TCP 还是 HTTP?』与『什么是 Triple 协议?』。
为什么跨语言框架普遍用 Protobuf? 因为它体积小、速度快、通过字段编号实现良好的版本兼容,详见本文档『常见序列化协议的深度对比?』。
协议
【中等】为何需要 RPC 协议?⭐
🎯 目标等级:L2 | ⏱ 建议用时:8 min | 🏷 标签:分布式通信 / RPC 协议
💎 关键结论
RPC 必须设计协议,是因为二进制流本身没有边界,收发双方必须像"用标点断句"一样,对报文的格式达成共识才能正确识别每条消息。理由:HTTP 等通用协议报文冗余、无状态,难以满足高性能 RPC 对紧凑私有协议的需求。
⚡记忆卡片
- 口诀:二进制没有边界,协议就是标点符号。
- 关键词:二进制传输/消息边界/收发共识/紧凑私有协议
- 链路:请求参数转二进制 → 数据拆包/粘包 → 需识别消息归属 → 双方约定协议 → 按协议解析报文
📖 核心知识
只有二进制才能在网络中传输,所以 RPC 请求需要把方法调用的请求参数先转成二进制,然后再通过网络传输。
传输的数据可能很大,RPC 请求需要将数据分解为多个数据包;传输的数据也可能较小,需要和其他请求的数据包进行合并。当接收方收到请求时,需要从二进制数据中识别出不同的请求。问题是,如何从二进制数据中识别出其所属的请求呢?
这就需要发送方、接收方在通信过程中达成共识,严格按照协议处理二进制数据。这就好比让你读一篇没有标点符号的文章,你要怎么识别出每一句话到哪里结束呢?很简单啊,我们加上标点,完成断句就好了。这里有个潜在的含义,写文章和读文章的人,都遵循标点符号的用法。
再进一步探讨,既然已经有很多成熟的网络协议,为何还要设计 RPC 协议?
有必要。因为 HTTP 这些通信标准协议,数据包中的实际请求数据相对于数据包本身要小很多,有很多无用的内容;并且 HTTP 属于无状态协议,无法将请求和响应关联,每次请求要重新建立连接。这对于高性能的 RPC 来说,HTTP 协议难以满足需求,所以有必要设计一个紧凑的私有协议。
🔀 发散问题
私有协议头里应该放哪些字段? 协议标识、数据大小、请求类型、序列化类型等,详见本文档『设计一个 RPC 协议的要点?』。
RPC 为什么常用 TCP 而不是 HTTP/1? TCP 协议紧凑、可靠且便于长连接复用,详见本文档『RPC 通信应该选择 TCP 还是 HTTP?』。
【中等】设计一个 RPC 协议的要点?⭐⭐
🎯 目标等级:L2 | ⏱ 建议用时:10 min | 🏷 标签:分布式通信 / RPC 协议设计
💎 关键结论
设计 RPC 协议首先要明确消息边界(消息长度 + 消息内容),再演进为"数据头 + 消息体"结构;为了平滑升级,协议头要支持可扩展,最终形成"固定部分 + 协议头内容 + 协议体内容"的三段式结构。理由:定长协议头加字段会造成线上兼容问题,不定长扩展头需要一个固定字段来指明头部长度。
⚡记忆卡片
- 口诀:先定边界,再分头体,扩展靠"固定长度+变长头"。
- 关键词:消息边界/数据头+消息体/定长协议头/可扩展三段式
- 链路:明确消息长度定边界 → 协议头承载元信息 → 定长头无法平滑加字段 → 用固定字段记录头部长度 → 协议头可扩展、平滑升级
📖 核心知识
首先,必须先明确消息的边界,即确定消息的长度。因此,至少要分为:消息长度+消息内容两部分。
接下来,我们会发现,在使用过程中,仅消息长度,不足以明确通信中的很多细节:如序列化方式是怎样的?是否消息压缩?压缩格式是怎样的?如果协议发生变化,需要明确协议版本等等。
大多数的协议会分成两部分,分别是数据头和消息体。数据头一般用于身份识别,包括协议标识、数据大小、请求类型、序列化类型等信息;消息体主要是请求的业务参数信息和扩展属性等。
综上,一个 RPC 协议大概会由下图中的这些参数组成:

前面所述的协议属于定长协议头,那也就是说往后就不能再往协议头里加新参数了,如果加参数就会导致线上兼容问题。
为了保证能平滑地升级改造前后的协议,我们有必要设计一种支持可扩展的协议。其关键在于让协议头支持可扩展,扩展后协议头的长度就不能定长了。那要实现读取不定长的协议头里面的内容,在这之前肯定需要一个固定的地方读取长度,所以我们需要一个固定的写入协议头的长度。整体协议就变成了三部分内容:固定部分、协议头内容、协议体内容。

🔬 扩展知识
详情
- 【L3】协议版本字段是平滑升级的关键:新旧节点混布期间,服务端按协议头中的版本号选择对应的解码逻辑,实现多版本并存与灰度升级。
- 【L4】除长度外,协议头还可承载魔数(Magic Number)校验、压缩类型、请求序列号(用于异步响应匹配)等字段;魔数可快速丢弃非法报文,序列号是请求-响应关联的基础。
🔀 发散问题
为什么需要消息体压缩? 当业务参数较大时,压缩可以减少网络传输开销,代价是 CPU,需要按报文大小做开关控制。
消息长度字段一般用几个字节? 常用 4 字节整型表示,意味着单条消息上限约 4GB 内的约定值,实际框架通常会再设置更小的业务上限防止内存被打爆。
序列化
【简单】什么是序列化?有哪些常见的序列化方式?⭐⭐
🎯 目标等级:L1 | ⏱ 建议用时:8 min | 🏷 标签:分布式通信 / 序列化
💎 关键结论
序列化是把对象转成可传输的二进制,反序列化是还原回来,且转换必须可逆。理由:网络中只能传二进制,而 RPC 出入参都是对象。选型时按安全性 > 兼容性 > 性能 > 易用性的顺序权衡。
⚡记忆卡片
- 口诀:对象变二进制叫序列化,变回来叫反序列化;选型先看安全和兼容。
- 关键词:序列化/反序列化/二进制可逆/安全性优先
- 链路:对象无法直接网络传输 → 序列化为二进制 → 网络传输 → 对端反序列化还原对象
📖 核心知识
由于,网络传输的数据必须是二进制数据,而调用方请求的出参、入参都是对象。因此,必须将对象转换可传输的二进制,并且要求转换算法是可逆的。
- 序列化(serialize):序列化是将对象转换为二进制数据。
- 反序列化(deserialize):反序列化是将二进制数据转换为对象。

Java 领域,常见的序列化技术如下
市面上有如此多的序列化技术,那么我们在应用时如何选择呢?
一般而言,序列化技术选型需要考量的维度,根据重要性从高到低,依次有:
- 安全性:是否存在漏洞。如果存在漏洞,就有被攻击的可能性。
- 兼容性:版本升级后的兼容性是否很好,是否支持更多的对象类型,是否是跨平台、跨语言的。服务调用的稳定性与可靠性,要比服务的性能更加重要。
- 性能
- 时间开销:序列化、反序列化的耗时性能自然越小越好。
- 空间开销:序列化后的数据越小越好,这样网络传输效率就高。
- 易用性:类库是否轻量化,API 是否简单易懂。
鉴于以上的考量,序列化技术的选型建议如下:
- JDK 序列化:性能较差,且有很多使用限制,不建议使用。
- Thrift、Protobuf:适用于对性能敏感,对开发体验要求不高。
- Hessian:适用于对开发体验敏感,性能有要求。
- Jackson、Gson、Fastjson:适用于对序列化后的数据要求有良好的可读性(转为 json 、xml 形式)。
🔀 发散问题
主流序列化协议之间怎么深度对比? 从跨语言、性能、体积、IDL、兼容性、安全性多维度对比,详见本文档『常见序列化协议的深度对比?』。
序列化使用中有哪些坑? 对象过于复杂庞大、复杂继承关系、使用框架不支持的类,详见本文档『序列化的使用中需要注意哪些问题?』。
【中等】常见序列化协议的深度对比?⭐⭐
🎯 目标等级:L2 | ⏱ 建议用时:10 min | 🏷 标签:分布式通信 / 序列化选型
💎 关键结论
序列化协议直接影响 RPC 的性能、兼容性和安全性:追求性能与兼容选 Protobuf,Java 生态开发体验选 Hessian2,可读性优先选 JSON,JDK 序列化有漏洞且性能差、生产慎用。理由:没有全能协议,只有与场景匹配的协议。
⚡记忆卡片
- 口诀:性能兼容 Protobuf,Java 体验 Hessian,可读性 JSON,JDK 原生别上生产。
- 关键词:IDL/向前向后兼容/serialVersionUID/反序列化漏洞
- 链路:明确场景诉求(性能/兼容/可读)→ 按维度对比候选协议 → 验证版本兼容与安全 → 确定选型
📖 核心知识
下面从多个维度对主流序列化协议进行深度对比。
| 序列化协议 | 跨语言 | 性能 | 体积 | 是否需 IDL | 兼容性 | 安全性 | 典型应用 |
|---|---|---|---|---|---|---|---|
| JDK | 否 | 差 | 大 | 否 | 差 | 低 | Java 内部 |
| Hessian2 | 部分 | 中 | 小 | 否 | 好 | 中 | Dubbo 默认 |
| Kryo | 否 | 高 | 极小 | 否 | 中 | 中 | 纯 Java 高性能 |
| FST | 否 | 高 | 小 | 否 | 中 | 中 | Java 高性能替代 |
| Protobuf | 是 | 高 | 极小 | 是 | 极好 | 高 | gRPC/Dubbo3 |
| Thrift | 是 | 高 | 小 | 是 | 好 | 高 | Thrift RPC |
| JSON | 是 | 低 | 大 | 否 | 极好 | 高 | 前后端、REST |
| MessagePack | 是 | 中 | 中 | 否 | 好 | 高 | 跨语言轻量 |
关键差异分析:
- IDL(接口定义语言):Protobuf 和 Thrift 需要预定义
.proto/.thrift文件,编译生成代码。优点是强类型、版本兼容性好;缺点是开发流程多一步。 - 向前/向后兼容:Protobuf 通过字段编号实现优秀的向前/向后兼容(新增字段使用新编号,旧版本忽略未知字段);Hessian2 支持字段增删;JDK 序列化对
serialVersionUID敏感。 - 安全性:JDK 序列化存在反序列化漏洞(如 Apache Commons Collections 漏洞链),生产环境慎用;Protobuf、JSON 等相对安全。
🔬 扩展知识
详情
- 【L3】Protobuf 的编码原理是 Varint 变长整数 + 字段标签(field number + wire type),字段名不参与编码,因此体积小、且字段重命名不影响兼容。
- 【L4】高性能 Java 框架常在 Hessian2 之外提供 Kryo/FST 作为内网高性能选项,但跨版本兼容能力弱于 Hessian2,切换前需评估接口模型变更频率。
📚 延伸阅读:深入理解 Java 序列化
🔀 发散问题
为什么 Dubbo 默认用 Hessian2 而不是 Protobuf? Hessian2 无需 IDL、对 Java 对象模型友好、支持字段增删,兼顾开发体验与兼容性;Protobuf 则更适合跨语言强契约场景。
选型时还需要注意什么使用问题? 即使选对了协议,对象过于复杂或使用不当类型仍会拖垮性能,详见本文档『序列化的使用中需要注意哪些问题?』。
【简单】序列化的使用中需要注意哪些问题?⭐
🎯 目标等级:L1 | ⏱ 建议用时:5 min | 🏷 标签:分布式通信 / 序列化使用
💎 关键结论
选对序列化框架只是第一步,还要保证传输对象"简单、小巧、原生、无复杂继承"。理由:序列化贯穿每次 RPC 通信,对象越复杂,序列化耗时与传输开销越大,直接拉高响应时延。
⚡记忆卡片
- 口诀:对象要简单,体积要小,集合用原生,继承要少搞。
- 关键词:嵌套复杂/大集合/继承遍历/原生集合类
- 链路:对象复杂庞大 → 序列化慢、报文大 → 时延上升 → 因此约束对象设计
📖 核心知识
由于 RPC 每次通信,都要经过序列化、反序列化的过程,所以序列化方式,会直接影响 RPC 通信的性能。除了选择合适的序列化技术,如何合理使用序列化也非常重要。
RPC 序列化常见的使用不当的情况如下:
对象过于复杂、庞大 - 对象过于复杂、庞大,会降低序列化、反序列化的效率,并增加传输开销,从而导致响应时延增大。
- 过于复杂:存在多层的嵌套,比如 A 对象关联 B 对象,B 对象又聚合 C 对象,C 对象又关联聚合很多其他对象
- 过于庞大:比如一个大 List 或者大 Map
对象有复杂的继承关系 - 对象关系越复杂,就越浪费性能,同时又很容易出现序列化上的问题。大多数序列化框架在进行序列化时,如果发现类有继承关系,会不停地寻找父类,遍历属性。
使用序列化框架不支持的类作为入参类 - 比如 Hessian 框架,他天然是不支持 LinkHashMap、LinkedHashSet 等,而且大多数情况下最好不要使用第三方集合类,如 Guava 中的集合类,很多开源的序列化框架都是优先支持编程语言原生的对象。因此如果入参是集合类,应尽量选用原生的、最为常用的集合类,如 HashMap、ArrayList。
前面已经列举了常见的序列化问题,既然明确了问题,就要针对性预防。RPC 序列化时要注意以下几点:
- 对象要尽量简单,没有太多的依赖关系,属性不要太多,尽量高内聚;
- 入参对象与返回值对象体积不要太大,更不要传太大的集合;
- 尽量使用简单的、常用的、开发语言原生的对象,尤其是集合类;
- 对象不要有复杂的继承关系,最好不要有父子类的情况。
🔀 发散问题
大集合一定要传怎么办? 应改为分页/分批传输,或用流式调用分块发送,避免单条报文过大导致超时与内存压力。
怎么从协议层面减少这些问题? 用 IDL 强约束接口模型可以在设计期就拦截不合理的对象结构,详见本文档『常见序列化协议的深度对比?』。
通信
【简单】HTTP 与 RPC 有什么区别?⭐⭐⭐
🎯 目标等级:L2 | ⏱ 建议用时:10 min | 🏷 标签:通信协议 / 选型对比
💎 关键结论
HTTP 是通用通信协议,重标准化与互操作;RPC 是高性能调用方式,重效率与内部治理。核心差异:HTTP 文本格式(JSON/XML)、相对低性能、治理靠外部组件;RPC 二进制格式(Protobuf 等)、长连接高性能、治理内置。对外 API、跨语言选 HTTP,内部高频调用选 RPC。
⚡记忆卡片
- 口诀:HTTP 通用走文本,RPC 高效走二进制
- 关键词:JSON / Protobuf / 长连接 / 服务治理内置 / 跨语言
- 链路:对外接口 → HTTP(通用互操作);内部调用 → RPC(高性能治理)
📖 核心知识
HTTP 是通用通信协议,侧重标准化与互操作性;RPC 是高性能调用方式,侧重效率与内部服务治理。
| 维度 | HTTP | RPC |
|---|---|---|
| 协议定位 | 应用层通信标准 | 远程调用方式 |
| 数据格式 | 文本为主(JSON/XML) | 二进制为主(Protobuf 等) |
| 性能 | 相对低(文本解析、短连接) | 高(长连接、二进制传输) |
| 服务治理 | 需额外组件(注册中心、网关) | 框架内置(负载均衡、熔断) |
| 适用场景 | 对外 API、跨语言异构 | 内部服务间高性能调用 |
🔀 发散问题
- Q:具体到框架怎么选? → 对外走 HTTP(如 Feign),内部走 RPC(如 Dubbo),详见本文档「主流 RPC 框架有哪些?它们有什么区别?」。
- Q:RPC 通信协议怎么选? → TCP vs HTTP 的权衡,详见本文档「RPC 通信应该选择 TCP 还是 HTTP?」。
【中等】RPC 通信应该选择 TCP 还是 HTTP?⭐⭐
🎯 目标等级:L2 | ⏱ 建议用时:10 min | 🏷 标签:分布式通信 / 传输协议选型
💎 关键结论
RPC 通信选 TCP 还是 HTTP 没有标准答案:内网高性能、纯 Java 体系选 TCP 私有协议,需要跨网关、浏览器或云原生场景选 HTTP/2。理由:TCP 协议紧凑性能高但穿透性差,HTTP/2 靠多路复用把性能差距拉得很小且生态通用。
⚡记忆卡片
- 口诀:内网高性能用 TCP,穿透网关上 HTTP/2。
- 关键词:私有协议/HTTP/2/多路复用/网关穿透
- 链路:明确部署环境(内网/跨网关/云原生)→ 对比性能与穿透性 → 选定传输协议 → 落地框架配置
📖 核心知识
核心结论:RPC 通信可以选择 TCP,也可以选择 HTTP,两者各有优劣。现代 RPC 框架(如 gRPC、Dubbo3 Triple)倾向于基于 HTTP/2 构建。
TCP vs HTTP 对比:
| 维度 | TCP 私有协议(如 Dubbo2) | HTTP/HTTP2(如 gRPC、Triple) |
|---|---|---|
| 性能 | 高(协议紧凑,无 HTTP 头开销) | HTTP/1 较低;HTTP/2 接近 TCP |
| 可读性 | 差(二进制,需抓包工具) | HTTP/2 较差;HTTP/1 可读 |
| 穿透性 | 差(不易穿过网关、代理) | 好(标准协议,网关友好) |
| 多路复用 | 需自行实现 | HTTP/2 原生支持 |
| 流式通信 | 需自行实现 | HTTP/2 原生支持 Stream |
| 浏览器访问 | 不支持 | HTTP/1、HTTP/2 支持 |
| 跨语言 | 需各语言实现协议 | 通用,生态好 |
选型建议:
- 内网高性能调用,纯 Java 体系 → TCP 私有协议(如 Dubbo2 协议)
- 需要跨网关、代理、浏览器访问 → HTTP/2(如 Triple、gRPC)
- 云原生、服务网格场景 → HTTP/2(更易与 Istio 等集成)
🔬 扩展知识
详情
- 【L3】HTTP/2 性能能追平 TCP 私有协议的关键在于二进制分帧与头部压缩(HPACK),消除了 HTTP/1 文本协议的解析开销与连接数瓶颈。
- 【L4】HTTP/3(基于 QUIC)进一步解决 TCP 层队头阻塞并内建连接迁移,适合弱网与跨地域调用,但当前 RPC 框架落地仍以 HTTP/2 为主。
🔀 发散问题
选了 TCP/HTTP 之后连接怎么管理? RPC 通常用长连接复用并配合心跳保活,详见本文档『长连接 vs 短连接?』。
Dubbo3 基于 HTTP 的协议是什么? Triple 协议,基于 HTTP/2 且兼容 gRPC,详见本文档『什么是 Triple 协议?』。
【中等】长连接 vs 短连接?⭐⭐
🎯 目标等级:L2 | ⏱ 建议用时:8 min | 🏷 标签:分布式通信 / 连接管理
💎 关键结论
RPC 框架通常采用长连接,避免每次请求都经历 TCP 三次握手的开销。理由:RPC 是高频调用场景,建连成本摊薄到多次请求上才划算,代价是要用心跳保活和断线重连来维持连接健康。
⚡记忆卡片
- 口诀:高频调用用长连接,心跳保活防假死。
- 关键词:三次握手开销/连接复用/心跳保活/断线重连
- 链路:高频 RPC 调用 → 频繁建连开销大 → 建立长连接复用 → 心跳检测存活 → 断开后自动重连
📖 核心知识
核心结论:RPC 框架通常采用长连接,以避免频繁建连带来的握手开销。
| 维度 | 短连接 | 长连接 |
|---|---|---|
| 连接建立 | 每次请求新建连接 | 连接建立后复用,多次请求共享 |
| 性能 | 低(TCP 三次握手开销) | 高(省去握手开销) |
| 资源占用 | 低(请求结束即释放) | 高(需维持连接,心跳保活) |
| 适用场景 | 低频、请求量小 | 高频、高并发 RPC 调用 |
| 典型应用 | HTTP/1.0 | Dubbo2、gRPC、HTTP/1.1 KeepAlive |
长连接的关键问题:
- 心跳保活:定期发送心跳包,检测连接是否存活(Dubbo 默认 60s 心跳)。
- 连接断开重连:连接异常断开后,需自动重连机制。
- 连接复用与多路复用:HTTP/2 在单连接上支持多路复用,避免队头阻塞。
🔬 扩展知识
详情
- 【L3】长连接数量与连接方向需要治理:双向连接、消费者与提供者间连接数过多时会占用文件描述符与内存,常见手段是合并连接(单连接多路复用)或按服务分组限流连接数。
- 【L4】心跳间隔要与中间设备(LB、NAT)的空闲超时配合,若心跳周期大于链路上设备的空闲断连阈值,连接会被静默掐断,表现为偶发超时。
🔀 发散问题
心跳除了保活还有什么用? 可作为健康检查的探活手段,连续失败可将节点移入亚健康列表,详见本文档『如何实现 RPC 的健康检查?』。
短连接就一无是处吗? 低频、请求量小的场景下短连接资源占用低、无需维护心跳,反而更简单可靠。
【中等】什么是 Triple 协议?⭐
🎯 目标等级:L2 | ⏱ 建议用时:8 min | 🏷 标签:分布式通信 / Triple 协议
💎 关键结论
Triple 是 Dubbo3 设计的、基于 HTTP 的 RPC 协议规范,也是 Dubbo3 的默认协议,完全兼容 gRPC。理由:它让 Dubbo 获得 HTTP/2 的多路复用、流式通信与网关穿透能力,同时无缝接入 gRPC 生态。
⚡记忆卡片
- 口诀:Dubbo3 默认协议,跑在 HTTP 上,和 gRPC 互通。
- 关键词:Dubbo3/HTTP/2/兼容 gRPC/流式通信/不绑定 IDL
- 链路:Dubbo3 需要 HTTP 生态能力 → 设计 Triple 协议 → 基于 HTTP/1、HTTP/2 运行 → 兼容 gRPC、穿透网关 → 支持流式与多路复用
📖 核心知识
Triple 协议是 Dubbo3 设计的基于 HTTP 的 RPC 通信协议规范,是 Dubbo3 的默认协议。
核心特性:
- 完全兼容 gRPC:基于 HTTP/2,与 gRPC 协议互通。
- 支持多种通信模型:Request-Response(Unary)、Streaming 流式(Unary/Server/Client/Bi-directional)。
- 运行在 HTTP/1 和 HTTP/2 之上:可直接用 curl、浏览器访问后端 Dubbo 服务。
- 不绑定 IDL:虽然支持 Protobuf,但也可以使用 Java 接口定义服务。
- 网关穿透性好:标准 HTTP 协议,易于穿过 Nginx、API Gateway、Service Mesh。
Triple vs Dubbo2 协议:
| 维度 | Dubbo2 协议 | Triple 协议 |
|---|---|---|
| 传输层 | TCP | HTTP/2(也可 HTTP/1) |
| 序列化 | Hessian2 | Protobuf(推荐)/Hessian2 |
| 流式通信 | 不支持 | 支持(4 种流式模式) |
| 网关穿透 | 差 | 好 |
| 浏览器访问 | 不支持 | 支持 |
| 跨语言 | 弱 | 强(兼容 gRPC 生态) |
| 多路复用 | 单连接串行 | HTTP/2 原生多路复用 |
🔀 发散问题
流式通信有哪几种模式? Unary、Server Streaming、Client Streaming、双向流四种,详见本文档『什么是 RPC 流式调用?有哪些模式?』。
为什么不直接在 Dubbo2 协议上改进? Dubbo2 协议是定长头的 TCP 私有协议,难以承载 HTTP 生态能力与平滑扩展,因此 Dubbo3 选择另起 HTTP 协议栈。
【中等】RPC 在网络通信上倾向选择哪种网络 IO 模型?⭐⭐
🎯 目标等级:L2 | ⏱ 建议用时:10 min | 🏷 标签:分布式通信 / 网络 IO 模型
💎 关键结论
RPC 网络通信通常选择 IO 多路复用(Reactor 模式)。理由:RPC 是典型高并发场景,多路复用让少量线程管理海量连接,兼顾内核与语言生态支持,Java 首选基于 Reactor 的 Netty。
⚡记忆卡片
- 口诀:高并发选多路复用,Java 网络通信找 Netty。
- 关键词:BIO/NIO/IO 多路复用/AIO/Reactor
- 链路:RPC 高并发连接 → 一线程一连接扛不住 → IO 多路复用统一监听 → Reactor 分发事件 → 少量线程处理海量连接
📖 核心知识
一次 RPC 调用,本质就是服务消费者与服务提供者间的一次网络信息交换的过程。可见,通信是 RPC 实现的核心。
常见的网络 IO 模型分为四种:同步阻塞 IO(BIO)、同步非阻塞 IO(NIO)、IO 多路复用和异步非阻塞 IO(AIO)。在这四种 IO 模型中,只有 AIO 为异步 IO,其他都是同步 IO。
什么是 IO 多路复用?字面上的理解,多路就是指多个通道,也就是多个网络连接的 IO,而复用就是指多个通道复用在一个复用器上。IO 多路复用(Reactor 模式)在高并发场景下使用最为广泛,很多知名软件都应用了这一技术,如:Netty、Redis、Nginx 等。
RPC 调用在大多数的情况下,是一个高并发调用的场景,考虑到系统内核的支持、编程语言的支持以及 IO 模型本身的特点,在 RPC 框架的实现中,在网络通信的处理上,通常会选择 IO 多路复用的方式。开发语言的网络通信框架的选型上,最优的选择是基于 Reactor 模式实现的框架,如 Java 语言,首选的框架便是 Netty 框架(Java 还有很多其他 NIO 框架,但目前 Netty 应用得最为广泛),并且在 Linux 环境下,也要开启 epoll 来提升系统性能(Windows 环境下是无法开启 epoll 的,因为系统内核不支持)。
🔬 扩展知识
详情
- 【L3】Reactor 模式可细分为单 Reactor 单线程、单 Reactor 多线程、主从 Reactor 多线程三种形态;Netty 的 BossGroup/WorkerGroup 即主从 Reactor,Boss 负责 accept,Worker 负责 IO 读写。
- 【L4】Linux 下 epoll 相比 select/poll 通过回调机制将就绪事件时间复杂度降到 O(1) 级别,且无最大文件描述符数量限制,是海量连接服务的基础;io_uring 则是更新的异步 IO 方向。
🔀 发散问题
为什么不选 BIO? 一连接一线程在海量连接下线程数爆炸,上下文切换与内存开销不可接受。
为什么 RPC 很少直接用 AIO? Linux 原生 AIO 支持不成熟(多数实现仍基于 epoll 模拟),且编程模型复杂,收益不如 IO 多路复用明显。
【中等】什么是零拷贝?⭐⭐
🎯 目标等级:L3 | ⏱ 建议用时:15 min | 🏷 标签:分布式通信 / 零拷贝
💎 关键结论
零拷贝就是取消用户空间与内核空间之间的多余数据拷贝,让数据通过 DMA 直接在内存与网卡间流动,省去 CPU 拷贝与上下文切换。理由:传统读写要在用户态和内核态间来回拷贝两次,CPU 与性能损耗大。
⚡记忆卡片
- 口诀:少一次拷贝,少一次切换;DMA 干活,CPU 歇着。
- 关键词:用户空间/内核空间/DMA/sendfile/mmap
- 链路:传统 IO 用户态内核态来回拷贝 → CPU 上下文切换开销大 → 零拷贝绕过用户态中转 → DMA 直接搬运数据 → CPU 与带宽节省
📖 核心知识
系统内核处理 IO 操作分为两个阶段——等待数据和拷贝数据。等待数据,就是系统内核在等待网卡接收到数据后,把数据写到内核中;而拷贝数据,就是系统内核在获取到数据后,将数据拷贝到用户进程的空间中。
应用进程的每一次写操作,都会把数据写到用户空间的缓冲区中,再由 CPU 将数据拷贝到系统内核的缓冲区中,之后再由 DMA 将这份数据拷贝到网卡中,最后由网卡发送出去。这里我们可以看到,一次写操作数据要拷贝两次才能通过网卡发送出去,而用户进程的读操作则是将整个流程反过来,数据同样会拷贝两次才能让应用程序读取到数据。
应用进程的一次完整的读写操作,都需要在用户空间与内核空间中来回拷贝,并且每一次拷贝,都需要 CPU 进行一次上下文切换(由用户进程切换到系统内核,或由系统内核切换到用户进程),这样很浪费 CPU 和性能。
所谓的零拷贝,就是取消用户空间与内核空间之间的数据拷贝操作,应用进程每一次的读写操作,可以通过一种方式,直接将数据写入内核或从内核中读取数据,再通过 DMA 将内核中的数据拷贝到网卡,或将网卡中的数据 copy 到内核。
Netty 中的零拷贝优化(偏向用户空间的数据操作优化):
- Netty 框架中很多内部的 ChannelHandler 实现类,都是通过 CompositeByteBuf、slice、wrap 操作来处理 TCP 传输中的拆包与粘包问题的,这对处理 TCP 传输中的拆包粘包问题有着重要的意义,对应用程序处理请求数据与返回数据也有重要的意义。
- Netty 的 ByteBuffer 可以采用 Direct Buffers,使用堆外直接内存进行 Socket 的读写操作,最终的效果与虚拟内存映射所实现的效果是一样的。
- Netty 还提供 FileRegion 中包装 NIO 的 FileChannel.transferTo() 方法实现了零拷贝,这与 Linux 中的 sendfile 方式在原理上也是一样的。
🔬 扩展知识
详情
- 【L3】操作系统层面的零拷贝主要有两种:sendfile(文件描述符到 socket 的直接传输)与 mmap(将内核缓冲区映射到用户空间减少一次拷贝),Kafka 高吞吐的重要原因之一就是大量使用 sendfile。
- 【L4】Netty 的"零拷贝"与操作系统零拷贝层次不同:它主要在用户态通过 ByteBuf 组合、切片、堆外内存避免 JVM 内部的冗余拷贝,二者常结合使用。
📚 延伸阅读:深入剖析Linux IO原理和几种零拷贝机制的实现
🔀 发散问题
零拷贝和堆外内存是什么关系? 使用 Direct Buffer 可避免 JVM 堆与堆外之间的一次中转拷贝,是 Netty 用户态零拷贝的手段之一。
为什么拆包粘包处理也能"零拷贝"? 用 CompositeByteBuf、slice 等逻辑视图组合数据,避免了物理合并时的内存复制。
动态代理
【中等】RPC 如何将远程调用转为本地调用的?⭐⭐
🎯 目标等级:L2 | ⏱ 建议用时:10 min | 🏷 标签:分布式通信 / 动态代理
💎 关键结论
RPC 的远程过程调用是通过动态代理实现的:框架为服务接口生成代理类,方法调用被代理拦截后注入远程调用逻辑,用户感知到的仍是本地调用。理由:代理把"调哪个方法、传什么参数"转译为网络请求,是屏蔽通信细节的关键。
⚡记忆卡片
- 口诀:接口生成代理,调用即拦截,远程变本地。
- 关键词:动态代理/InvocationHandler/Javassist/Byte Buddy
- 链路:注入服务接口 → 实际绑定代理类 → 方法调用被拦截 → 代理组装远程请求 → 返回结果如同本地调用
📖 核心知识
RPC 的远程过程调用是通过动态代理实现的。
RPC 框架会自动为要调用的接口生成一个代理类。当在项目中注入接口的时候,运行过程中实际绑定的就是这个接口生成的代理类。在接口方法被调用时,会被代理类拦截,这样,就可以在生成的代理类中,加入远程调用逻辑。

除了 JDK 默认的 InvocationHandler 能完成代理功能,还有很多其他的第三方框架也可以,比如像 Javassist、Byte Buddy 这样的框架。
单纯从代理功能上来看,JDK 默认的代理功能是有一定的局限性的,它要求被代理的类只能是接口。原因是因为生成的代理类会继承 Proxy 类,但 Java 是不支持多重继承的。此外,由于它生成后的代理类是使用反射来完成方法调用的,而这种方式相对直接用编码调用来说,性能会降低。
🔬 扩展知识
详情
- 【L3】JDK 动态代理基于接口 + 反射;Javassist/Byte Buddy 通过字节码生成代理类,可直接调用方法、避免反射开销,因此高性能 RPC 框架常选字节码方案。
- 【L4】代理对象通常在框架启动阶段一次性生成并缓存,运行期只执行拦截逻辑,避免重复生成代理带来的启动与内存开销。
📚 延伸阅读:深入理解 Java 反射和动态代理
🔀 发散问题
代理拦截到调用之后做什么? 组装消息、序列化并发送请求,详见本文档『RPC 是怎样工作的?』。
没有接口的情况下还能发起调用吗? 可以,通过泛化调用以字符串方式描述接口与方法,详见本文档『RPC 如何实现那泛化调用?』。
服务注册发现
【中等】如何实现一个注册中心?⭐⭐⭐
🎯 目标等级:L2 | ⏱ 建议用时:15 min | 🏷 标签:分布式通信 / 服务注册发现
💎 关键结论
注册中心的核心就两件事:服务注册(提供方启动时把 IP+接口写入注册中心)与服务订阅(调用方启动时拉取并缓存地址列表,后续靠变更通知保持新鲜)。理由:集群里节点动态变化,调用方必须有个权威来源才能找到通信地址。
⚡记忆卡片
- 口诀:提供方注册,调用方订阅,变更靠通知,地址本地缓存。
- 关键词:服务注册/服务订阅/Watcher 通知/本地缓存/AP 优先
- 链路:提供方启动注册地址 → 调用方订阅并缓存 → 节点变更触发通知 → 调用方更新本地列表 → 负载均衡选节点发起调用
📖 核心知识
RPC 框架必须要有服务注册和发现机制,这样,集群中的节点才能知道通信方的请求地址。

- 服务注册:在服务提供方启动的时候,将对外暴露的接口注册到注册中心之中,注册中心将这个服务节点的 IP 和接口保存下来。
- 服务订阅:在服务调用方启动的时候,去注册中心查找并订阅服务提供方的 IP,然后缓存到本地,并用于后续的远程调用。
基于 ZooKeeper 的服务发现:
使用 ZooKeeper 作为服务注册中心,是 Java 分布式系统的经典方案。
搭建一个 ZooKeeper 集群作为注册中心集群,服务注册的时候只需要服务节点向 ZooKeeper 节点写入注册信息即可,利用 ZooKeeper 的 Watcher 机制完成服务订阅与服务下发功能。

🔬 扩展知识
详情
- 【L3】通常可以使用 ZooKeeper、etcd 或者分布式缓存(如 Hazelcast)来解决事件通知问题,但当集群达到一定规模之后,依赖的 ZooKeeper 集群、etcd 集群可能就不稳定了,无法满足需求。
- 【L3】在超大规模的服务集群下,注册中心所面临的挑战就是超大批量服务节点同时上下线,注册中心集群接受到大量服务变更请求,集群间各节点间需要同步大量服务节点数据,最终导致:注册中心负载过高;各节点数据不一致;服务下发不及时或下发错误的服务节点列表。
- 【L4】RPC 框架依赖的注册中心的服务数据的一致性其实并不需要满足 CP,只要满足 AP 即可——短暂的数据不一致可由调用端容错兜底,而注册中心不可用则会阻断全部新连接建立。
🔀 发散问题
订阅之后,调用方如何保持地址列表的新鲜? 依赖注册中心的变更通知(如 ZooKeeper 的 Watcher 机制)实时推送 + 本地缓存,节点上下线时及时更新列表。
注册中心与负载均衡是什么关系? 注册中心负责提供可用的节点列表,负载均衡负责在列表内选出具体节点,详见本文档『负载均衡有哪些策略?』。
负载均衡
【中等】负载均衡有哪些策略?⭐⭐
🎯 目标等级:L2 | ⏱ 建议用时:10 min | 🏷 标签:分布式通信 / 负载均衡
💎 关键结论
负载均衡的本质是"从多个节点中选一个来处理请求",常见策略从简单的随机、轮询,到加权、最少活跃、一致性哈希,复杂度和效果递增。理由:不同策略在公平性、性能感知、缓存亲和性上各有取舍,要按场景选。
⚡记忆卡片
- 口诀:随机轮询打底,加权最少活跃进阶,一致性哈希保缓存。
- 关键词:随机/轮询/加权轮询/最少活跃/一致性哈希
- 链路:拿到服务节点列表 → 按策略计算目标节点 → 分发请求 → 依据反馈调整权重
📖 核心知识
常见的负载均衡策略:
- 随机(Random):按概率随机选择节点,实现简单,请求量足够大时趋向均匀;节点性能不一致时不够公平。
- 轮询(Round Robin):按顺序依次选择节点,绝对公平;但不感知节点实际负载,慢节点会拖累整体。
- 加权轮询(Weighted Round Robin):为节点设置权重,按权重比例分配流量,适合异构机器混布。
- 最少活跃(Least Active):选择当前活跃请求数最少的节点,能自动把流量从慢节点挪走,需要调用端统计每个节点的活跃数。
- 一致性哈希(Consistent Hash):按参数哈希落到固定节点,同源请求总打到同一节点,利于缓存亲和;需引入虚拟节点解决数据倾斜。
🔬 扩展知识
详情
- 【L3】静态策略(随机/轮询)不感知节点状态,动态策略(最少活跃/自适应打分)依赖运行时指标,准确但实现复杂,详见本文档『如何设计自适应的负载均衡?』。
- 【L4】负载均衡可以叠加预热权重:新启动节点逐步升权,避免冷节点被瞬时流量打垮,详见本文档『如何实现 RPC 优雅启动?』。
📚 延伸阅读:负载均衡基本原理
🔀 发散问题
为什么最少活跃能避开慢节点? 慢节点请求处理慢,积压的活跃请求数就高,策略自然会少选它,实现隐式的性能感知。
一致性哈希适合什么场景? 适合同参数请求希望落到同节点的场景,如会话保持、本地缓存命中优化。
【中等】如何设计自适应的负载均衡?⭐⭐
🎯 目标等级:L2 | ⏱ 建议用时:15 min | 🏷 标签:分布式通信 / 自适应负载均衡
💎 关键结论
自适应负载均衡的核心是"指标收集 + 打分 + 动态权重":调用方采集每个节点的负载、耗时、状态指标,综合打分后按比例修正节点权重,再配合随机权重策略分发流量。理由:静态权重反映不了节点实时健康状况,让分数驱动权重才能把流量自动导向健康节点。
⚡记忆卡片
- 口诀:收指标、算打分、改权重、随机选。
- 关键词:指标收集器/综合打分/动态权重/随机权重策略
- 链路:心跳与请求中收集指标 → 按权重综合打分 → 分数折算节点最终权重 → 随机权重策略选节点 → 流量自动倾斜向健康节点
📖 核心知识
可以采用一种打分的策略,服务调用者收集与之建立长连接的每个服务节点的指标数据,如服务节点的负载指标、CPU 核数、内存大小、请求处理的耗时指标(如请求平均耗时、TP99、TP999)、服务节点的状态指标(如正常、亚健康)。通过这些指标,计算出一个分数,比如总分 10 分,如果 CPU 负载达到 70%,就减它 3 分,当然了,减 3 分只是个类比,需要减多少分是需要一个计算策略的。可以为每个指标都设置一个指标权重占比,然后再根据这些指标数据,计算分数。
可以配合随机权重的负载均衡策略去控制,通过最终的指标分数修改服务节点最终的权重。例如给一个服务节点综合打分是 8 分(满分 10 分),服务节点的权重是 100,那么计算后最终权重就是 80(100*80%)。服务调用者发送请求时,会通过随机权重的策略来选择服务节点,那么这个节点接收到的流量就是其他正常节点的 80%(这里假设其他节点默认权重都是 100,且指标正常,打分为 10 分的情况)。
到这儿,一个自适应的负载均衡我们就完成了,整体的设计方案如下图所示:

关键步骤:
- 添加服务指标收集器,并将其作为插件,默认有运行时状态指标收集器、请求耗时指标收集器。
- 运行时状态指标收集器收集服务节点 CPU 核数、CPU 负载以及内存等指标,在服务调用者与服务提供者的心跳数据中获取。
- 请求耗时指标收集器收集请求耗时数据,如平均耗时、TP99、TP999 等。
- 可以配置开启哪些指标收集器,并设置这些参考指标的指标权重,再根据指标数据和指标权重来综合打分。
- 通过服务节点的综合打分与节点的权重,最终计算出节点的最终权重,之后服务调用者会根据随机权重的策略,来选择服务节点。
🔬 扩展知识
详情
- 【L3】指标来源分两类:节点侧指标(CPU、内存)靠心跳捎带上报,调用侧指标(耗时、成功率)靠调用端本地统计,二者结合才能既看"机器忙不忙"又看"请求顺不顺"。
- 【L4】打分权重的调参本质是灵敏度权衡:权重对指标变化越敏感,流量切换越快,但抖动与误判风险也越高,通常需要滞回(hysteresis)或平滑窗口抑制震荡。
🔀 发散问题
自适应负载均衡和静态加权轮询的区别? 静态权重人工设定、固定不变;自适应权重由实时指标驱动、动态调整。
打分数据多久更新一次合适? 通常与心跳周期或滑动统计窗口对齐,过密会增加统计与同步开销,过疏则反应迟钝。
路由
【中等】什么是服务路由?有哪些常见的路由规则?⭐⭐
🎯 目标等级:L2 | ⏱ 建议用时:15 min | 🏷 标签:分布式通信 / 服务路由
💎 关键结论
服务路由是通过规则从集群中筛选合适的节点子集,核心目的是实现流量隔离;它与负载均衡近似但分工不同——路由先圈定"能调谁",负载均衡再决定"具体调谁"。理由:分组调用、蓝绿/灰度发布、流量切换等场景都依赖精细化的规则,静态负载均衡算法做不到。
⚡记忆卡片
- 口诀:路由圈范围,均衡挑节点;规则有三类:条件、标签、脚本。
- 关键词:流量隔离/条件路由/标签路由/脚本路由/蓝绿灰度
- 链路:定义路由规则 → 从全量节点筛出候选子集 → 负载均衡在子集内选节点 → 实现流量隔离与发布控制
📖 核心知识
服务路由是指通过一定的规则从集群中选择合适的节点。
负载均衡的作用和服务路由的功能看上去很近似,二者有什么区别呢?
负载均衡的目标是提供服务分发而不是解决路由问题,常见的静态、动态负载均衡算法也无法实现精细化的路由管理,但是负载均衡也可以简单看做是路由方案的一种。
服务路由通常用于以下场景,目的在于实现流量隔离:
- 分组调用:一般来讲,为了保证服务的高可用性,实现异地多活的需求,一个服务往往不止部署在一个数据中心,而且出于节省成本等考虑,有些业务可能不仅在私有机房部署,还会采用公有云部署,甚至采用多家公有云部署。服务节点也会按照不同的数据中心分成不同的分组,这时对于服务消费者来说,选择哪一个分组调用,就必须有相应的路由规则。
- 蓝绿发布:蓝绿发布场景中,一共有两套服务群组:一套是提供旧版功能的服务群组,标记为绿色;另一套是提供新版功能的服务群组,标记为蓝色。两套服务群组都是功能完善的,并且正在运行的系统,只是服务版本和访问流量不同。新版群组(蓝色)通常是为了做内部测试、验收,不对外部用户暴露。
- 如果新版群组(蓝色)运行稳定,并测试、验收通过后,则通过服务路由、负载均衡等手段逐步将外部用户流量导向新版群组(蓝色)。
- 如果新版群组(蓝色)运行不稳定,或测试、验收不通过,则排查、解决问题后,再继续测试、验收。
- 灰度发布:灰度发布(又名金丝雀发布)是指在黑与白之间,能够平滑过渡的一种发布方式。在其上可以进行 A/B 测试,即让一部分用户使用特性 A,一部分用户使用特性 B:如果用户对 B 没有什么反对意见,那么逐步扩大发布范围,直到把所有用户都迁移到 B 上面来。灰度发布可以保证整体系统的稳定,在初始灰度的时候就可以发现、调整问题,以保证其影响度。要支持灰度发布,就要求服务能够根据一定的规则,将流量隔离。
- 流量切换:在业务线上运行过程中,经常会遇到一些不可抗力因素导致业务故障,比如某个机房的光缆被挖断,或者发生着火等事故导致整个机房的服务都不可用。这个时候就需要按照某个指令,能够把原来调用这个机房服务的流量切换到其他正常的机房。
- 线下测试联调:线下测试时,可能会缺少相应环境。可以将测试应用注册到线上,然后开启路由规则,在本地进行测试。
- 读写分离:对于大多数互联网业务来说都是读多写少,所以在进行服务部署的时候,可以把读写分开部署,所有写接口可以部署在一起,而读接口部署在另外的节点上。
常见的路由规则有:
- 条件路由规则 - 条件路由是基于条件表达式的路由规则。各个 RPC 框架的条件路由表达式各不相同。
- 标签路由规则 - 标签路由通过将某一个或多个服务的提供者划分到同一个分组,约束流量只在指定分组中流转,从而实现流量隔离的目的,可以作为蓝绿发布、灰度发布等场景的能力基础。标签主要是指对服务提供者的分组,目前有两种方式可以完成实例分组,分别是动态规则打标和静态规则打标。一般,动态规则优先级比静态规则更高,当两种规则同时存在且出现冲突时,将以动态规则为准。
- 脚本路由规则 - 脚本路由是基于脚本语言的路由规则,具有最高的灵活性,常用的脚本语言比如 JavaScript、Groovy、JRuby 等。
🔬 扩展知识
详情
- 【L3】路由规则的下发通常依赖控制面(配置中心/治理控制台)动态推送,规则变更即时生效、无需重启,这是实现秒级流量切换的前提。
- 【L4】全链路灰度需要路由能力沿调用链逐跳透传标签:入口打标后,标签通过 RPC 上下文在整条链路的每个服务间传递,任何一跳丢失标签都会导致流量"逃逸"到基线环境。
🔀 发散问题
路由和负载均衡到底谁先谁后? 一般路由先行:先按规则筛出候选节点集合,再由负载均衡在集合内选点,详见本文档『负载均衡有哪些策略?』。
动态打标和静态打标的区别? 静态打标在部署/启动时确定(如启动参数),动态打标由控制台运行时下发,冲突时动态规则优先。
Dubbo 中路由规则如何落地? 条件/标签/脚本/动态配置四类规则的下发架构与选型,见《Dubbo 面试之服务治理》『Dubbo 支持哪些路由方式?分别适用于什么场景?』。
监控
【中等】如何实现 RPC 的健康检查?⭐⭐
🎯 目标等级:L2 | ⏱ 建议用时:10 min | 🏷 标签:分布式通信 / 健康检查
💎 关键结论
健康检查用频率适中的心跳检测目标机器状态,并用"可用率"做量化标准:可用率低于阈值的节点挪入亚健康列表。理由:纯连接存活判断太粗糙,可用率能同时兼容高低频接口与不同响应时间的差异。
⚡记忆卡片
- 口诀:心跳探活定状态,可用率量化健康。
- 关键词:心跳探活/健康/亚健康/死亡/可用率/时间窗口
- 链路:建立连接 → 心跳持续探活 → 统计时间窗口内可用率 → 低于阈值移入亚健康列表 → 负载均衡避开该节点
📖 核心知识
使用频率适中的心跳去检测目标机器的健康状态。
- 健康状态:建立连接成功,并且心跳探活也一直成功;
- 亚健康状态:建立连接成功,但是心跳请求连续失败;
- 死亡状态:建立连接失败。
可以使用可用率来作为健康状态的量化标准:
可用率 = 一个时间窗口内接口调用成功次数 / 总调用次数当可用率低于某个比例,就认为这个节点存在问题,把它挪到亚健康列表,这样既考虑了高低频的调用接口,也兼顾了接口响应时间不同的问题。
🔬 扩展知识
详情
- 【L3】心跳频率要适中:太频繁浪费带宽和 CPU,太稀疏则故障发现滞后;通常取秒级周期并允许连续若干次失败再判定异常,避免单次网络抖动误判。
- 【L4】健康检查结果应与摘除策略解耦:亚健康节点不一定立即摘除,可先降权(减少流量),持续恶化再摘除,给瞬时抖动留缓冲,避免节点在健康/异常间频繁震荡。
🔀 发散问题
心跳和长连接保活是一回事吗? 机制相同、目的不同:保活是维持连接不被断开,健康检查是判断节点能否继续承接流量,详见本文档『长连接 vs 短连接?』。
节点被摘除后又恢复了怎么办? 需要恢复探测(如小流量试探或心跳恢复)后再移回健康列表,防止反复抖动。
【中等】什么是链路跟踪?⭐⭐
🎯 目标等级:L2 | ⏱ 建议用时:12 min | 🏷 标签:分布式通信 / 链路跟踪
💎 关键结论
链路跟踪就是把一次分布式请求还原成完整调用链路,用 TraceId 标识整条链路、SpanId 标识链路上的每一跳。理由:微服务依赖关系复杂,出了问题只有靠 Trace/Span 才能定位到具体是哪一跳、哪个节点、耗时多少。
⚡记忆卡片
- 口诀:一次请求一个 Trace,每一跳是一个 Span,核心是埋点和传递。
- 关键词:TraceId/SpanId/父子 Span/埋点/上下文传递
- 链路:请求入口生成 TraceId → 每跳创建 Span 并记录父子关系 → 上下文随 RPC 传递 → 汇总各节点日志 → 还原完整调用链
📖 核心知识
分布式链路跟踪就是将一次分布式请求还原为一个完整的调用链路,我们可以在整个调用链路中跟踪到这一次分布式请求的每一个环节的调用情况,比如调用是否成功,返回什么异常,调用的哪个服务节点以及请求耗时等等。
Trace 就是代表整个链路,每次分布式都会产生一个 Trace,每个 Trace 都有它的唯一标识即 TraceId,在分布式链路跟踪系统中,就是通过 TraceId 来区分每个 Trace 的。
Span 就是代表了整个链路中的一段链路,也就是说 Trace 是由多个 Span 组成的。在一个 Trace 下,每个 Span 也都有它的唯一标识 SpanId,而 Span 是存在父子关系的。还是以讲过的例子为例子,在 A->B->C->D 的情况下,在整个调用链中,正常情况下会产生 3 个 Span,分别是 Span1(A->B)、Span2(B->C)、Span3(C->D),这时 Span3 的父 Span 就是 Span2,而 Span2 的父 Span 就是 Span1。
RPC 在整合分布式链路跟踪需要做的最核心的两件事就是"埋点"和"传递"。
我们前面说是因为各子应用、子服务间复杂的依赖关系,所以通过日志难定位问题。那我们就想办法通过日志定位到是哪个子应用的子服务出现问题就行了。
其实,在 RPC 框架打印的异常信息中,是包括定位异常所需要的异常信息的,比如是哪类异常引起的问题(如序列化问题或网络超时问题),是调用端还是服务端出现的异常,调用端与服务端的 IP 是什么,以及服务接口与服务分组都是什么等等。具体如下图所示:

🔬 扩展知识
详情
- 【L3】埋点通常通过 RPC 框架的拦截器/过滤器统一完成,业务代码零侵入;传递则依赖 RPC 上下文(attachments/隐式参数)携带 TraceId、SpanId 等数据跨进程透传。
- 【L4】业界以 Dapper 论文为理论源头,OpenTelemetry 已成为埋点数据采集的事实标准,可与 Jaeger、Zipkin、SkyWalking 等后端对接。
🔀 发散问题
TraceId 如何跨进程不丢失? 放在 RPC 调用的隐式上下文(attachments)里随请求一起发送,服务端收到后继续向下传递,每一跳都基于父 Span 信息创建新 Span。
链路跟踪和日志有什么区别? 日志是单点视角,链路跟踪是全局视角,用 TraceId 把分散在各服务的日志串成一条因果链。
优雅启停
【中等】如何实现 RPC 优雅关闭?⭐⭐
🎯 目标等级:L2 | ⏱ 建议用时:15 min | 🏷 标签:分布式通信 / 优雅关闭
💎 关键结论
优雅关闭的关键是"先摘流量、再等存量、最后退出":关闭前先通知注册中心下线并设置关闭标识,新请求返回特定异常引导调用方安全重试,存量请求用引用计数等待完成,超时则强制退出。理由:服务发现只保证最终一致性,无法做到实时摘除,必须靠异常重试与存量等待兑现业务无损。
⚡记忆卡片
- 口诀:先下线通知,再拒绝新请求,等存量完成,超时强退。
- 关键词:ShutdownHook/关闭标识/ShutdownException/引用计数器/超时强退
- 链路:捕获关闭信号 → 通知注册中心下线 → 挡板拦截新请求抛特定异常 → 调用方摘节点并安全重试 → 引用计数等待存量请求完成 → 超时则强制退出
📖 核心知识
当服务提供方要上线的时候,一般是通过部署系统完成实例重启。在这个过程中,服务提供方的团队并不会事先告诉调用方他们需要操作哪些机器,从而让调用方去事先切走流量。而对调用方来说,它也无法预测到服务提供方要对哪些机器重启上线,因此负载均衡就有可能把要正在重启的机器选出来,这样就会导致把请求发送到正在重启中的机器里面,从而导致调用方不能拿到正确的响应结果。

在服务重启的时候,对于调用方来说,这时候可能会存在以下几种情况:
- 调用方发请求前,目标服务已经下线。对于调用方来说,跟目标节点的连接会断开,这时候调用方可以立马感知到,并且在其健康列表里面会把这个节点挪掉,自然也就不会被负载均衡选中。
- 调用方发请求的时候,目标服务正在关闭,但调用方并不知道它正在关闭,而且两者之间的连接也没断开,所以这个节点还会存在健康列表里面,因此该节点就有一定概率会被负载均衡选中。
当出现第二种情况的时候,调用方业务会受损,如何避免这种问题呢。当服务提供方关闭前,是不是可以先通知注册中心进行下线,然后通过注册中心告诉调用方进行节点摘除?

如上图所示,整个关闭过程中依赖了两次 RPC 调用,一次是服务提供方通知注册中心下线操作,一次是注册中心通知服务调用方下线节点操作。注册中心通知服务调用方都是异步的。服务发现只保证最终一致性,并不保证实时性,所以注册中心在收到服务提供方下线的时候,并不能成功保证把这次要下线的节点推送到所有的调用方。所以这么来看,通过服务发现并不能做到应用无损关闭。
异常引导重试:当服务提供方正在关闭,如果这之后还收到了新的业务请求,服务提供方直接返回一个特定的异常给调用方(比如 ShutdownException)。这个异常就是告诉调用方"我已经收到这个请求了,但是我正在关闭,并没有处理这个请求",然后调用方收到这个异常响应后,RPC 框架把这个节点从健康列表挪出,并把请求自动重试到其他节点,因为这个请求是没有被服务提供方处理过,所以可以安全地重试到其他节点,这样就可以实现对业务无损。
捕获关闭事件:可以通过捕获操作系统的进程信号来获取,在 Java 语言里面,对应的是 Runtime.addShutdownHook 方法,可以注册关闭的钩子。在 RPC 启动的时候,我们提前注册关闭钩子,并在里面添加了两个处理程序,一个负责开启关闭标识,一个负责安全关闭服务对象,服务对象在关闭的时候会通知调用方下线节点。同时需要在我们调用链里面加上挡板处理器,当新的请求来的时候,会判断关闭标识,如果正在关闭,则抛出特定异常。
等待存量请求:关闭过程中已经在处理的请求会不会受到影响呢?如果进程结束过快会造成这些请求还没有来得及应答,同时调用方也会抛出异常。为了尽可能地完成正在处理的请求,首先我们要把这些请求识别出来。可以在服务对象加上引用计数器,每开始处理请求之前加一,完成请求处理减一,通过该计数器我们就可以快速判断是否有正在处理的请求。服务对象在关闭过程中,会拒绝新的请求,同时根据引用计数器等待正在处理的请求全部结束之后才会真正关闭。但考虑到有些业务请求可能处理时间长,或者存在被挂住的情况,为了避免一直等待造成应用无法正常退出,我们可以在整个 ShutdownHook 里面,加上超时时间控制,当超过了指定时间没有结束,则强制退出应用。超时时间我建议可以设定成 10s,基本可以确保请求都处理完了。

🔬 扩展知识
详情
- 【L3】下线通知与存量等待的顺序很重要:若先断连接再通知注册中心,调用方会在感知断开前仍可能把请求发过来;标准做法是"先设标识+通知下线 → 等存量 → 关连接/退进程"。
- 【L4】在容器化环境中,优雅关闭还需配合 K8s 的 preStop Hook 与 terminationGracePeriodSeconds:preStop 留出让服务发现传播下线信息的时间,优雅期需覆盖应用自身 ShutdownHook 超时。
🔀 发散问题
为什么 ShutdownException 可以安全重试? 因为请求被挡板拦截后根本没有被业务处理,不存在副作用,重试到其他节点不会造成重复执行。
优雅关闭的反面——启动阶段要注意什么? 新节点需要预热与延迟暴露,避免冷启动被流量打垮,详见本文档『如何实现 RPC 优雅启动?』。
【中等】如何实现 RPC 优雅启动?⭐⭐
🎯 目标等级:L2 | ⏱ 建议用时:15 min | 🏷 标签:分布式通信 / 优雅启动
💎 关键结论
优雅启动靠两招:启动预热(新节点权重随运行时间逐渐增加,流量缓慢爬坡)与延迟暴露(应用完全启动后才把服务注册到注册中心)。理由:刚启动的应用没有 JIT 编译红利和类缓存,直接承接全量流量会大面积超时。
⚡记忆卡片
- 口诀:权重随时间爬坡,注册等启动完成。
- 关键词:JIT 预热红利/启动预热/动态权重/延迟暴露/注册 Hook
- 链路:应用重启丢失 JIT/缓存红利 → 直接承流量会超时 → 预热降权、流量爬坡 → 延迟暴露避免提前被感知 → 平稳过渡到正常状态
📖 核心知识
运行了一段时间后的应用,执行速度会比刚启动的应用更快。这是因为在 Java 里面,在运行过程中,JVM 虚拟机会把高频的代码编译成机器码,被加载过的类也会被缓存到 JVM 缓存中,再次使用的时候不会触发临时加载,这样就使得"热点"代码的执行不用每次都通过解释,从而提升执行速度。
但是这些"临时数据",都在应用重启后就消失了。重启后的这些"红利"没有了之后,如果让刚启动的应用就承担像停机前一样的流量,这会使应用在启动之初就处于高负载状态,从而导致调用方过来的请求可能出现大面积超时,进而对线上业务产生损害行为。
启动预热:就是让刚启动的服务提供方应用不承担全部的流量,而是让它被调用的次数随着时间的移动慢慢增加,最终让流量缓和地增加到跟已经运行一段时间后的水平一样。如何做到这点呢?
首先,对于调用方来说,我们要知道服务提供方启动的时间。一种是服务提供方在启动的时候,把自己启动的时间告诉注册中心;另外一种就是注册中心收到的服务提供方的请求注册时间。因为整个预热过程的时间是一个粗略值,即使机器之间的日期时间存在 1 分钟的误差也不影响,并且在真实环境中机器都会默认开启 NTP 时间同步功能,来保证所有机器时间的一致性。
不管你是选择哪个时间,最终的结果就是,调用方通过服务发现,除了可以拿到 IP 列表,还可以拿到对应的启动时间。接着,可以利用加权负载均衡算法来分发流量。现在,需要让这个权重变为动态的,并且是随着时间的推移慢慢增加到服务提供方设定的固定值。

通过这个小逻辑的改动,我们就可以保证当服务提供方运行时长小于预热时间时,对服务提供方进行降权,减少被负载均衡选择的概率,避免让应用在启动之初就处于高负载状态,从而实现服务提供方在启动后有一个预热的过程。
延迟暴露:服务提供方应用在没有启动完成的时候,调用方的请求就过来了,而调用方请求过来的原因是,服务提供方应用在启动过程中把解析到的 RPC 服务注册到了注册中心,这就导致在后续加载没有完成的情况下服务提供方的地址就被服务调用方感知到了。
为了解决这个问题,需要在应用启动加载、解析 Bean 的时候,如果遇到了 RPC 服务的 Bean,只先把这个 Bean 注册到 Spring-BeanFactory 里面去,而并不把这个 Bean 对应的接口注册到注册中心,只有等应用启动完成后,才把接口注册到注册中心用于服务发现,从而实现让服务调用方延迟获取到服务提供方地址。
具体如何实现呢?我们可以在服务提供方应用启动后,接口注册到注册中心前,预留一个 Hook 过程,让用户可以实现可扩展的 Hook 逻辑。用户可以在 Hook 里面模拟调用逻辑,从而使 JVM 指令能够预热起来,并且用户也可以在 Hook 里面事先预加载一些资源,只有等所有的资源都加载完成后,最后才把接口注册到注册中心。

🔬 扩展知识
详情
- 【L3】预热权重曲线常用线性或类 Dubbo 的
1.0 + (1.0 - uptime/warmup) * (weight - 1)式渐进曲线,预热时长一般取分钟级,过短起不到保护作用,过长则扩容生效慢。 - 【L4】除应用内预热外,还可在平台层做分批发布(如每批只替换一定比例实例),与应用级预热叠加,避免大批实例同时冷启动引发集群容量缺口。
🔀 发散问题
预热和自适应负载均衡有什么关系? 预热是启动期的临时降权,自适应负载均衡是运行期的持续调权,二者都是"动态权重"思想在不同阶段的应用,详见本文档『如何设计自适应的负载均衡?』。
为什么要预留注册前的 Hook? 它给了业务方预加载缓存、预热连接的时机,确保注册时节点真正就绪,而不是"注册了但还没准备好"。
流量回放
架构
【困难】如何设计一个 RPC 框架?⭐⭐⭐
🎯 目标等级:L3 | ⏱ 建议用时:20 min | 🏷 标签:分布式通信 / RPC 架构设计
💎 关键结论
设计 RPC 框架要分层推进:先用"通信 + 协议序列化 + 动态代理"满足 P2P 调用,再叠加"服务发现、负载均衡、路由、容错、配置"等服务治理应对集群场景,最后用 SPI 微内核+插件保证可扩展。理由:基础能力解决"能调通",治理能力解决"调得好",扩展性解决"改得动"。
⚡记忆卡片
- 口诀:三层基础能跑通,五块治理能抗住,SPI 插件能扩展。
- 关键词:通信传输/协议+序列化/动态代理/服务治理/SPI 微内核
- 链路:P2P 调用需要通信+协议+代理 → 集群场景叠加服务治理 → 定制需求驱动插件化 → SPI 接口抽象+默认实现 → 形成核心体系+插件体系
📖 核心知识
设计一个 RPC 框架,可以自下而上梳理一下所需要的能力:
- 通信传输模块:RPC 本质上就是一个远程调用,那肯定就需要通过网络来传输数据。
- 协议模块:传输的数据如何定义,就需要通过协议和序列化方式来确定。此外,为了减少传输数据的大小,可以加入压缩功能。
- 代理模块:为了屏蔽用户的感知,让用户更聚焦于自身业务,需要引入动态代理来托管远程调用。
以上,是一个 RPC 框架的基础能力,适用于 P2P 场景。
但是,如果面对集群模式,以上能力就不够了。同一个服务可能有多个提供者。消费者选择调用哪个提供者?消费者怎么找到提供者的访问地址?请求提供者失败了如何处理?这些都依赖于服务治理的能力。
服务治理,需要很多个模块的能力:服务发现、负载均衡、路由、容错、配置管理等。

具备了这些能力就万事大吉了吗?RPC 框架很难一开始就面面俱到,但作为基础能力,在实际应用中,难免会有定制化的要求。这就要求 RPC 框架具备良好的扩展性。
通常来说,框架软件可以通过 SPI 技术来实现微内核+插件架构。根据依赖倒置原则,框架应该先将每个功能点都抽象成接口,并提供默认实现。然后,利用 SPI 机制,可以动态地为某个接口寻找服务实现。
加上了插件功能之后,我们的 RPC 框架就包含了两大核心体系——核心功能体系与插件体系,如下图所示:

案例:Dubbo 的分层设计
Dubbo 是这套设计思路的典型实践,它将框架划分为分层架构:业务层(Service)、接口层(Config/Proxy)、集群层(Cluster:容错/路由/负载均衡)、协议层(Protocol)、交换层(Exchange)、传输层(Transport)。每一层职责单一、面向接口,序列化、负载均衡、集群容错等都以 SPI 扩展点形式提供默认实现,使用者可按需替换,这正是"微内核+插件"的落地形态。
🔬 扩展知识
详情
- 【L3】线程模型是框架设计的关键决策:IO 线程与业务线程分离(Reactor 主从模式),避免慢业务阻塞网络 IO;线程池隔离与派发策略决定了框架的并发上限。
- 【L4】全异步化设计(从网络 IO、序列化到业务处理均不阻塞)配合 CompletableFuture,可以显著提升单机吞吐;同时框架需内建指标采集(耗时、成功率、并发数)支撑自适应负载均衡与健康检查。
🏭 实战场景
详情
推演场景(非生产真实数据):假设某公司有 2000+ 微服务实例,单服务日均调用量千万级。若框架仅实现 P2P 调用而无服务发现,每次扩容都要人工维护地址列表,运维成本不可接受;若无集群容错,单节点抖动会直接传导为业务失败。因此落地时通常按"先通信协议与代理 → 再注册发现与负载均衡 → 后路由、容错、监控"的顺序分期建设,每层通过 SPI 预留替换点,避免后期推倒重来。
⚠️ 常见误区
详情
常见误区:
- ❌ "RPC 框架只需要实现网络通信和序列化就够了" → 集群环境下没有服务发现、负载均衡、容错,框架无法支撑真实生产,治理能力和基础能力同等重要。
- ❌ "扩展性后期再加也不迟" → 扩展点(SPI)需要在架构初期就按接口抽象设计,后期改造需要大面积重构并破坏既有插件兼容。
- ❌ "默认实现越强大越好,不需要插件机制" → 不同业务的序列化、容错策略诉求差异大,硬编码默认逻辑会导致框架无法适配定制场景。
🔀 发散问题
框架的异步能力怎么设计? 通过消息 ID 与 Future 的映射实现响应回填,详见本文档『如何实现 RPC 异步调用?』。
协议层怎么设计才平滑升级? 协议头可扩展、三段式结构,详见本文档『设计一个 RPC 协议的要点?』。
框架的高可用由谁保证? 框架自身靠集群容错(失败重试、快速失败等),注册中心靠 AP 可用性,详见本文档『如何实现一个注册中心?』。
【困难】如何实现 RPC 异步调用?⭐⭐
🎯 目标等级:L3 | ⏱ 建议用时:15 min | 🏷 标签:分布式通信 / 异步调用
💎 关键结论
RPC 框架内部无论同步还是异步都是异步的:每条消息有唯一标识,发送前创建 Future 并存储"消息 ID → Future"映射,响应到达后回填对应 Future;同步调用只是框架帮你主动执行了 Future 的 get。理由:网络响应与请求天然不同步,必须靠映射机制把结果还给正确的调用。
⚡记忆卡片
- 口诀:消息有 ID,Future 有映射,同步只是框架帮你 get。
- 关键词:消息唯一标识/Future 映射/同步阻塞 get/CompletableFuture 全异步
- 链路:发送前创建 Future 并存映射 → 响应到达按 ID 找 Future → 回填结果 → 同步则框架 get,异步则用户自定 get 时机
📖 核心知识
一次 RPC 调用的本质就是调用端向服务端发送一条请求消息,服务端收到消息后进行处理,处理之后响应给调用端一条响应消息,调用端收到响应消息之后再进行处理,最后将最终的返回值返回给动态代理。
对于 RPC 框架,无论是同步调用还是异步调用,调用端的内部实现都是异步的。
调用端发送的每条消息都一个唯一的消息标识,实际上调用端向服务端发送请求消息之前会先创建一个 Future,并会存储这个消息标识与这个 Future 的映射,动态代理所获得的返回值最终就是从这个 Future 中获取的;当收到服务端响应的消息时,调用端会根据响应消息的唯一标识,通过之前存储的映射找到对应的 Future,将结果注入给那个 Future,再进行一系列的处理逻辑,最后动态代理从 Future 中获得到正确的返回值。
所谓的同步调用,不过是 RPC 框架在调用端的处理逻辑中主动执行了这个 Future 的 get 方法,让动态代理等待返回值;而异步调用则是 RPC 框架没有主动执行这个 Future 的 get 方法,用户可以从请求上下文中得到这个 Future,自己决定什么时候执行这个 Future 的 get 方法。

如何做到 RPC 调用全异步?
实现 RPC 调用全异步的方法是让 RPC 框架支持 CompletableFuture。CompletableFuture 是 Java8 原生支持的。如果 RPC 框架能够支持 CompletableFuture,现在发布一个 RPC 服务,服务接口定义的返回值是 CompletableFuture 对象,整个调用过程会分为这样几步:
- 服务调用方发起 RPC 调用,直接拿到返回值
CompletableFuture对象,之后就不需要任何额外的与 RPC 框架相关的操作了,直接就可以进行异步处理; - 在服务端的业务逻辑中创建一个返回值
CompletableFuture对象,之后服务端真正的业务逻辑完全可以在一个线程池中异步处理,业务逻辑完成之后再调用这个CompletableFuture对象的complete方法,完成异步通知; - 调用端在收到服务端发送过来的响应之后,RPC 框架再自动地调用调用端拿到的那个返回值
CompletableFuture对象的complete方法,这样一次异步调用就完成了。
通过对 CompletableFuture 的支持,RPC 框架可以真正地做到在调用端与服务端之间完全异步,同时提升了调用端与服务端的两端的单机吞吐,并且 CompletableFuture 是 Java8 原生支持,业务逻辑中没有任何代码入侵性。
🔬 扩展知识
详情
- 【L3】Future 映射表需处理超时清理:请求发出后若响应迟迟未到,应由超时机制(如定时任务)主动移除映射并异常完成 Future,避免映射表无限增长与调用永久阻塞。
- 【L4】回调执行线程需要设计:响应 IO 线程直接执行用户回调会阻塞网络处理,通常把回调切换到独立业务线程池执行,代价是多一次线程切换。
🔀 发散问题
异步调用和流式调用是一回事吗? 不是:异步仍是一请求一响应只是不阻塞,流式是一次调用内多条消息,详见本文档『什么是 RPC 流式调用?有哪些模式?』。
超时后服务端才处理完怎么办? 客户端已放弃等待,服务端结果被丢弃;对一致性敏感的业务需要配合幂等与对账机制。
【困难】什么是 RPC 流式调用?有哪些模式?⭐⭐
🎯 目标等级:L3 | ⏱ 建议用时:15 min | 🏷 标签:分布式通信 / 流式调用
💎 关键结论
流式调用打破了"一请求一响应"的限制,允许一次 RPC 调用中多次发送/接收数据,共有 Unary、Server Streaming、Client Streaming、双向流四种模式,基于 HTTP/2 的 Stream 语义实现。理由:大数据量分批传输与实时交互场景下,传统单次请求响应既低效又不自然。
⚡记忆卡片
- 口诀:一请求一响应变多请求多响应,四种模式靠 HTTP/2 Stream。
- 关键词:Unary/Server Streaming/Client Streaming/Bidirectional Streaming/StreamObserver
- 链路:传统一问一答不够用 → 基于 HTTP/2 Stream 开放多次收发 → 四种模式覆盖不同方向 → 适配大文件、实时推送等场景
📖 核心知识
流式调用(Streaming Call) 是指在一次 RPC 调用中,允许客户端或服务端多次发送/接收数据,而不是传统的"一请求一响应"模式。流式调用基于 HTTP/2 的 Stream 语义实现。
四种通信模型:
| 模式 | 说明 | 典型场景 |
|---|---|---|
| Unary(一元) | 一次请求,一次响应(传统 RPC) | 普通业务调用 |
| Server Streaming | 一次请求,多次响应 | 大数据量分批返回、实时推送 |
| Client Streaming | 多次请求,一次响应 | 上传文件分块、批量数据采集 |
| Bidirectional Streaming | 双向流,多次请求多次响应 | 实时聊天、实时数据同步、协同编辑 |
Dubbo3 Triple 流式调用示例:
// 服务端流式
public interface GreetService {
StreamObserver<HelloRequest> sayHelloStream(StreamObserver<HelloReply> responseObserver);
}
// 客户端调用
StreamObserver<HelloRequest> requestObserver = greetService.sayHelloStream(new StreamObserver<HelloReply>() {
@Override
public void onNext(HelloReply reply) {
System.out.println("收到响应: " + reply.getMessage());
}
@Override
public void onCompleted() {
System.out.println("响应结束");
}
});
// 发送多次请求
requestObserver.onNext(HelloRequest.newBuilder().setName("A").build());
requestObserver.onNext(HelloRequest.newBuilder().setName("B").build());
requestObserver.onCompleted();流式调用 vs 异步调用:
- 异步调用:仍然是一请求一响应,只是不阻塞等待结果。
- 流式调用:可以在一次调用中传输多条消息,适合大数据量、实时交互场景。
适用场景:
- 大文件传输(分块流式)
- 实时数据推送(股票行情、IM 消息)
- IoT 设备数据上报
- AI/ML 模型推理结果流式返回
🔬 扩展知识
详情
- 【L3】流式调用的背压(flow control)由 HTTP/2 的流量控制窗口保障:接收方处理能力不足时可缩小窗口,发送方自动降速,避免慢消费者被压垮。
- 【L4】双向流场景下需处理半关闭语义:一端调用 onCompleted 只关闭自己的发送方向,另一方向仍可继续发送,正确管理生命周期是避免资源泄漏的关键。
🔀 发散问题
哪些协议支持流式调用? 需要 HTTP/2 Stream 语义,如 Triple、gRPC;基于 TCP 的 Dubbo2 协议不支持,详见本文档『什么是 Triple 协议?』。
流式和多次普通调用有什么区别? 流式共享同一个调用上下文与连接流,开销更低、天然保序;多次普通调用相互独立,无法保证顺序且开销更大。
【中等】RPC 如何实现那泛化调用?⭐⭐
🎯 目标等级:L2 | ⏱ 建议用时:12 min | 🏷 标签:分布式通信 / 泛化调用
💎 关键结论
泛化调用让调用端在没有服务接口 API 的情况下发起 RPC 调用:通过统一的 GenericService 代理,把接口名、分组、方法名、参数全部以字符串/通用类型封装进请求消息发送给服务端。理由:测试平台、轻量网关这类通用入口不可能依赖所有业务方的接口包。
⚡记忆卡片
- 口诀:没有接口也能调,字符串描述接口方法参数。
- 关键词:GenericService/$invoke/接口名+方法名+参数/专属序列化插件
- 链路:调用端无接口 API → 创建 GenericService 代理并指定接口/分组 → $invoke 封装全部调用信息 → 发送请求给服务端 → 专属序列化插件完成编解码
📖 核心知识
在一些特定场景下,需要在没有接口的情况下进行 RPC 调用。例如:
场景一:搭建一个统一的测试平台,可以让各个业务方在测试平台中通过输入接口、分组名、方法名以及参数值,在线测试自己发布的 RPC 服务。

场景二:搭建一个轻量级的服务网关,可以让各个业务方用 HTTP 的方式,通过服务网关调用其它服务。

为了解决这些场景的问题,可以使用泛化调用。
就是 RPC 框架提供统一的泛化调用接口(GenericService),调用端在创建 GenericService 代理时指定真正需要调用的接口的接口名以及分组名,通过调用 GenericService 代理的 $invoke 方法将服务端所需要的所有信息,包括接口名、业务分组名、方法名以及参数信息等封装成请求消息,发送给服务端,实现在没有接口的情况下进行 RPC 调用的功能。
class GenericService {
Object $invoke(String methodName, String[] paramTypes, Object[] params);
CompletableFuture<Object> $asyncInvoke(String methodName, String[] paramTypes);
}而通过泛化调用的方式发起调用,由于调用端没有服务端提供方提供的接口 API,不能正常地进行序列化与反序列化,我们可以为泛化调用提供专属的序列化插件,来解决实际问题。
🔬 扩展知识
详情
- 【L3】泛化调用的参数通常以 Map 形式描述 POJO(类名 + 字段键值对),服务端序列化插件负责把 Map 还原为真实业务对象,因此泛化调用的性能与安全性(参数校验)都弱于普通调用,适合平台型场景而非业务主链路。
- 【L4】轻量网关用泛化调用时,需要自行完成 HTTP 参数到泛化参数的映射、服务发现与鉴权,本质上泛化调用是网关的"最后一公里"执行器。
🔀 发散问题
为什么泛化调用需要专属序列化插件? 调用端没有业务类定义,常规序列化无法编解码,需要用 Map/通用描述代替 POJO 并在服务端还原。
泛化调用和动态代理是什么关系? 泛化调用同样依赖动态代理生成 GenericService 的代理对象,只是代理拦截后组装的是字符串化的调用信息,详见本文档『RPC 如何将远程调用转为本地调用的?』。