在"现代分布式系统 / 微服务底层通信"里,有两个常用的 RPC 框架:gRPC + Protocol Buffers 和 OpenFeign。
- gRPC + Protobuf
- 真正的"高性能RPC框架"
- 基于 HTTP/2 + 二进制协议
- 强类型IDL(.proto)
- 多语言友好(Go / Java / Python / C++ 等)
- OpenFeign
- 本质是"声明式 HTTP 客户端"
- 通常基于 REST(HTTP/1.1 + JSON)
- 强依赖 Spring Cloud 生态
- 更像是"调用远程 REST API 的优雅封装",不算纯RPC
👉 换句话说:
- gRPC 是"协议级RPC方案"
- OpenFeign 是"开发体验层封装工具"
现代大厂更偏向:
- gRPC + Protocol Buffers
典型场景:
- 微服务内部通信
- 高QPS系统(交易、推荐、实时计算)
- 多语言服务互调
原因:
- 性能高(比 JSON 快很多)
- 网络开销小(序列化体积小)
- 支持 streaming(长连接)
传统Java微服务 / Spring Cloud体系
更偏向:
- OpenFeign
典型场景:
- B端业务系统(订单、用户、库存)
- 内部系统之间调用
- CRUD为主的服务
原因:
- 上手极其简单(写接口就能调)
- 和 Spring Boot 无缝集成
- 调试方便(直接 HTTP)
趋势比较明确:
gRPC 在上升,OpenFeign 在"稳定但不再增长"
原因:
- 云原生(K8s)推动 gRPC
- 多语言架构越来越普遍
- 性能要求越来越高
- REST + JSON 在内部调用显得"太重"
但注意:
- 外部 API(对前端 / 对第三方)仍然是 REST(不会用 gRPC)
- 所以现实是:
- 内部:gRPC
- 外部:REST(内部调用外部服务时可能用 Feign)
用 OpenFeign 调第三方服务,并不需要它注册到注册中心。
只有在这种情况下才需要注册中心(比如 Nacos):调用"自己体系里的微服务"
例如:
text
order-service → user-service
这时候你可以写:
java
@FeignClient("user-service")
Feign 会:
- 去注册中心找
user-service - 做服务发现 + 负载均衡
调用第三方服务时直接写死 URL(或配置 URL)
java
@FeignClient(name = "payment", url = "https://api.xxx.com")
public interface PaymentClient {
@PostMapping("/pay")
PayResponse pay(PayRequest req);
}
👉 这里:
name只是一个标识url才是真正访问地址- ❗ 不走注册中心
有时候会这样设计:混合(网关 + 第三方)
text
Feign → 网关 → 第三方
👉 "Feign → 网关 → 第三方"本质就是:你不直接调第三方,而是先调你自己的一个'中间服务(或网关)',由它统一转发到第三方。"
这里的"网关"不一定是传统意义上的 API Gateway(比如 Spring Cloud Gateway)。
更常见的是两种实现:
✅ 方式1(更主流):专门的"第三方接入服务"
text
Feign → payment-adapter-service → 第三方支付
✅ 方式2:网关转发(较少用于复杂业务)
text
Feign → API Gateway → 第三方
👉 实际生产中:方式1更多
架构:
text
[order-service]
↓ Feign
[payment-adapter-service] ←(自己写的服务)
↓ HTTP
[支付宝 / Stripe / PayPal]
涉及:
- OpenFeign(内部调用)
- Spring Boot(实现 adapter 服务)
不直接 Feign 调第三方,原因在于:
✅ 1. 统一接入(解耦)
如果直接调第三方:
text
order-service → 支付宝
user-service → 支付宝
coupon-service → 支付宝
👉 改一个接口,全崩
而用 adapter:
text
所有服务 → payment-adapter → 支付宝
👉 改动只在一处
✅ 2. 安全控制(必须)
第三方支付通常需要:
- 私钥
- 签名算法
- token
👉 不能把这些散落在各个服务里
✅ 3. 统一处理复杂逻辑
比如:
- 重试
- 限流
- 幂等
- 日志
- 异常转换
✅ 4. 协议转换
内部你可能用:
- JSON
第三方可能用:
- form-data
- XML(很多支付还在用)
👉 adapter 负责"翻译"
👉 Feign ≠ 服务发现工具
👉 它只是一个"声明式 HTTP 客户端"
- 有注册中心 👉 可以做服务发现
- 没有注册中心 👉 就是普通 HTTP 调用工具
👉 gRPC + Protocol Buffers 本身也不依赖注册中心,它们只是"通信协议 + 序列化方式"。
✅ gRPC 可以直连调用,也可以配注册中心
gRPC 最基础的调用方式(没有注册中心)最原始、也是最简单的方式就是直连:
服务端:
text
启动在 10.0.0.1:50051
客户端:
java
ManagedChannel channel = ManagedChannelBuilder
.forAddress("10.0.0.1", 50051)
.usePlaintext()
.build();
👉 这就已经能调了
👉 完全不需要 Nacos
生产环境中之所以配置注册中心是因为实际生产环境有这些问题:
服务实例不止一个
text
user-service:
10.0.0.1
10.0.0.2
10.0.0.3
👉 不能写死一个 IP
实例会动态变化(扩缩容)
- Kubernetes 扩容
- 容器重启
- 节点漂移
👉 IP 会变
需要负载均衡
gRPC 默认是:
- 单连接
- 单目标
👉 需要额外机制做 LB
于是就引入"服务发现"(注册中心)
👉 架构变成:
text
client → 注册中心 → service 实例列表 → gRPC 调用
gRPC + 注册中心有两种主流方式:
✅ 方式1:自己从注册中心拉地址(最常见)
text
1. 从 Nacos 拿到实例列表
2. 自己做负载均衡
3. 建立 gRPC 连接
👉 这种方式灵活,但需要自己封装
✅ 方式2:用 gRPC 的服务发现机制
gRPC 支持:
- DNS
- xDS(Envoy / Service Mesh)
比如在 Kubernetes 里:
text
user-service.default.svc.cluster.local
👉 gRPC 直接用服务名解析
gRPC 性能更高的原因
👉 gRPC 比基于 REST(HTTP + JSON)的方案性能更高,核心不是"某一项优化",而是"协议层 + 序列化 + 连接模型"整体更高效。
二进制序列化更高效
gRPC 使用 Protocol Buffers,相比于 JSON:
json
{"userId":123,"userName":"Tom"}
vs Protobuf(实际是二进制):
text
更短、更紧凑(不可读)
- 体积更小(通常小 30%~70%)
- 解析更快(不用字符串解析)
- 字段是编号而不是字符串 key
👉 直接结果:
- 网络传输更快
- CPU 消耗更低
基于 HTTP/2(而不是 HTTP/1.1)
gRPC 默认跑在 HTTP/2 上,而 REST 大多还是 HTTP/1.1,HTTP/2 带来的优势:
1️⃣ 多路复用(Multiplexing)
👉 一个 TCP 连接可以同时跑多个请求
text
HTTP/1.1:一个连接基本串行(或少量并发)
HTTP/2:一个连接并发 N 个请求
2️⃣ 头部压缩(HPACK)
- HTTP header 不再重复传
- 减少带宽占用
3️⃣ 长连接(减少握手)
- 不用频繁建连接
- 避免 TCP + TLS 开销
减少不必要的"文本处理"
REST(JSON)的问题:
- 字符串解析(慢)
- 字段名重复(浪费带宽)
- 类型不严格(需要额外校验)
gRPC:
- 强类型(.proto 定义)
- 编译期生成代码
- 直接映射内存结构
👉 本质是:少做了很多"解释工作"
代码生成(零反射/低反射)
使用 Protocol Buffers 后:
text
.proto → 自动生成 Java / Go / Python 代码
调用时:
java
stub.getUser(request)
- 不需要像 JSON 那样反射解析
- 没有 Map → Object 转换
- 几乎是"函数调用体验"
支持流式通信(Streaming)
gRPC 原生支持:
- Server Streaming
- Client Streaming
- Bidirectional Streaming
text
一个连接上持续传数据
👉 对比 REST:
- REST 通常是"一问一答"
- 做流式要靠轮询 / WebSocket(更重)
gRPC 不适用的场景
1️⃣ 对外公开 API(前端 / 第三方)
👉 几乎一定不能用 gRPC
原因:
- 浏览器原生不支持(需要 gRPC-Web,限制多)
- 调试困难(不像 JSON 能直接看)
- 生态不通用(合作方不一定支持)
👉 标准做法:
- 外部统一用 REST(HTTP + JSON)
- 内部才用 gRPC
2️⃣ 要求"强可调试性"的系统
👉 比如:
- B 端后台系统
- 运维接口
- 调试频繁的业务 API
问题:
- gRPC 是二进制(不可读)
- 无法直接用 curl / Postman 调
👉 对比:
- REST:一眼看懂请求和响应
- gRPC:需要工具 + .proto
3️⃣ 多团队 / 异构系统且"协议无法统一"
👉 如果对接方:
- 不愿意维护
.proto - 技术栈混乱(老系统、PHP等)
- 无法引入 gRPC SDK
👉 强行用 gRPC 会带来:
- 接入成本极高
- 推不动
4️⃣ 简单 CRUD / 低并发系统
👉 比如:
- 后台管理系统
- 内部工具平台
问题:
- 性能不是瓶颈
- 引入 gRPC 增加复杂度
👉 现实情况:
- REST + OpenFeign 更简单
5️⃣ 对接第三方(支付 / 短信 / OAuth 等)
👉 现实是:
- 第三方几乎全部是 REST
比如:
- 支付接口
- 登录授权
- 短信服务
👉 只能用 HTTP
Feign 底层原理
👉 OpenFeign 的本质就是"用动态代理把接口方法调用,转成一次 HTTP 请求"。
👉 Feign = 动态代理 + 注解解析 + HTTP 客户端封装
代码结构:
java
@FeignClient("user-service")
public interface UserClient {
@GetMapping("/user/{id}")
User getUser(@PathVariable("id") Long id);
}
调用:
java
userClient.getUser(1L);
启动时:
- Spring 扫描到
@FeignClient - 为这个接口生成一个代理对象(Proxy)
👉 用的是:
- JDK 动态代理(
Proxy.newProxyInstance)
2️⃣ 方法调用被"拦截"
当你调用:
java
userClient.getUser(1L);
👉 实际进入的是:
text
InvocationHandler.invoke()
3️⃣ 解析注解(核心步骤)
Feign 会解析:
@GetMapping("/user/{id}")@PathVariable- 参数值
👉 生成:
text
HTTP Method: GET
URL: http://user-service/user/1
4️⃣ 构建 RequestTemplate
Feign 内部有个核心对象:
text
RequestTemplate
里面包含:
- URL
- Header
- Body
- Query 参数
5️⃣ 负载均衡(如果有注册中心)
如果你写的是:
java
@FeignClient("user-service")
👉 会结合:
- Spring Cloud LoadBalancer(或 Ribbon)
流程:
text
user-service → 注册中心 → 获取实例列表 → 选一个实例
6️⃣ 发起 HTTP 请求
Feign 本身不发请求,它会委托底层客户端:
常见实现:
- JDK HttpURLConnection(默认)
- Apache HttpClient
- OkHttp(最常用)
👉 最终发出真实 HTTP 请求
7️⃣ 解析响应(Decoder)
返回的是 JSON:
json
{"id":1,"name":"Tom"}
Feign 会用:
- Jackson(默认)
👉 转成:
java
User 对象
核心组件拆解
Feign 内部关键组件:
1️⃣ Contract(契约解析器)
👉 作用:解析注解
- Spring MVC 注解 → HTTP 语义
2️⃣ Encoder(编码器)
👉 作用:把 Java 对象 → HTTP 请求体
比如:
java
User → JSON
3️⃣ Decoder(解码器)
👉 作用:HTTP 响应 → Java 对象
4️⃣ Client(HTTP执行器)
👉 真正发请求的地方
5️⃣ LoadBalancer(负载均衡)
👉 结合注册中心使用
6️⃣ Retryer(重试机制)
👉 请求失败自动重试
7️⃣ Logger(日志)
👉 打印请求/响应
Feign 是"伪 RPC"
写的是:
java
userClient.getUser(1);
看起来像:
text
本地方法调用
但实际上:
text
→ HTTP 请求
→ JSON 序列化
→ 网络通信
❗ Feign 只是"让 HTTP 看起来像 RPC"
五、vs gRPC
| 点 | Feign | gRPC |
|---|---|---|
| 调用本质 | HTTP + JSON | HTTP/2 + Protobuf |
| 是否真RPC | ❌ 伪RPC | ✅ 真RPC |
| 性能 | 一般 | 高 |
| 易用性 | 高 | 较复杂 |
Feign 这么优雅的本质原因:
👉 "用接口 + 注解 + 动态代理,把模板代码全隐藏了"
如果不用 Feign,你要写:
java
RestTemplate.exchange(...)
👉 Feign 帮你自动生成
总结:
👉 "Feign 通过动态代理拦截接口调用,结合注解解析生成 HTTP 请求,并委托底层 HTTP 客户端执行,从而实现声明式远程调用。"
gRPC 实现负载均衡的方式
👉 gRPC 默认是"客户端负载均衡(Client-side LB)",而不是服务端负载。
但在生产里,两种模式都能用,只是侧重点不同。
✅ 客户端负载均衡(gRPC 默认)
text
client 拿到所有实例列表 → 自己选一个 → 直连
text
client
↓
(自己做LB)
↓
server1 / server2 / server3
✅ 服务端负载均衡(传统模式)
text
client → 负载均衡器 → 转发到某个 server
text
client
↓
Load Balancer(Nginx / Envoy)
↓
server1 / server2 / server3
gRPC 选用"客户端负载的核心原因:性能 + 连接模型
1️⃣ gRPC 是长连接(HTTP/2)
- 一个连接会复用很多请求
- 如果走服务端 LB:
👉 所有请求会"粘"在同一个后端实例上
text
client → LB → server1(一直是它)
👉 负载就不均匀了 ❗
2️⃣ 客户端可以"更聪明地选节点"
客户端拿到所有实例后可以:
- round-robin(轮询)
- pick-first(选一个)
- weighted(权重)
- latency-aware(延迟优先)
👉 比单纯转发更灵活
3️⃣ 少一跳网络
text
客户端LB: client → server
服务端LB: client → LB → server
👉 少一次转发:
- 延迟更低
- 吞吐更高
客户端负载实现主要依靠 gRPC 内部的两个核心组件:
1️⃣ Name Resolver(服务发现)
负责拿到服务实例列表:
来源可以是:
- DNS
- Kubernetes Service
- 注册中心(如 Nacos)
👉 输出:
text
server1, server2, server3
2️⃣ Load Balancer(负载策略)
常见策略:
✅ pick_first(默认)
text
选一个实例,一直用
👉 优点:简单
👉 缺点:不均衡
✅ round_robin(最常用)
text
轮流选 server1 → server2 → server3
👉 需要开启配置
✅ xDS(高级)
结合:
- Istio
- Envoy
支持:
- 动态权重
- 熔断
- 灰度发布
虽然 gRPC 偏向客户端 LB,但下面场景会用服务端 LB:
1️⃣ 跨语言 / 多团队统一入口
text
client → Envoy → gRPC service
👉 好处:
- 所有流量统一控制
- 不需要每个客户端实现 LB
2️⃣ Service Mesh(主流大厂方案)
比如:
- Istio + Envoy
架构:
text
client sidecar → server sidecar → service
👉 LB 在 sidecar 做
👉 本质:
- "伪客户端LB"(代理帮你做)
3️⃣ 对外暴露 gRPC 服务
text
外部 → LB → gRPC service
👉 必须要入口层
两种方式对比
| 维度 | 客户端 LB | 服务端 LB |
|---|---|---|
| 延迟 | 更低 | 多一跳 |
| 负载均衡效果 | 更好 | 容易不均 |
| 实现复杂度 | 客户端复杂 | 服务端复杂 |
| 控制能力 | 分散 | 集中 |
| 扩展性 | 强 | 一般 |
大厂不会只用一种,而是:
text
外部流量
↓
LB / Gateway
↓
内部服务(gRPC)
↓
客户端负载均衡
或者:
text
Service Mesh(Istio)
→ sidecar 做 LB(xDS)
👉 本质是:
- 外部:服务端 LB
- 内部:客户端 LB / mesh LB
总结:
👉 "gRPC 默认采用客户端负载均衡,由客户端获取服务实例并选择目标节点,从而避免 HTTP/2 长连接导致的负载不均问题,同时减少一次网络跳转,提高性能。"
Feign 负载均衡
👉 OpenFeign 是"客户端负载均衡"(Client-side LB)
text
Feign client
↓
拿到服务实例列表(注册中心)
↓
在本地选一个实例(负载均衡)
↓
直接发 HTTP 请求到该实例
👉 关键点在于:
"选哪个服务实例,是在客户端决定的"
Feign 本身不做负载均衡,它会配合:
- Spring Cloud LoadBalancer(现在主流)
- 或早期的 Ribbon
流程:
text
@FeignClient("user-service")
↓
LoadBalancer 从注册中心拿实例列表
↓
选择一个实例(轮询等策略)
↓
Feign 发请求
对比"服务端负载均衡",服务端 LB(比如 Nginx)
text
client → Nginx → server1/server2/server3
- 客户端只知道一个入口
- 不知道后面有多少实例
- 转发由中间层完成
Feign(客户端 LB)
text
client(Feign)
↓
server1 / server2 / server3(直接选)
- 客户端知道所有实例
- 自己做选择
- 没有中间转发
Spring Cloud 选择客户端 LB 原因和 gRPC 类似:
✅ 1. 少一跳,性能更好
text
客户端LB: client → server
服务端LB: client → LB → server
✅ 2. 更灵活
可以做:
- 权重
- 灰度发布
- 按标签路由
✅ 3. 和注册中心天然结合
配合:
- Nacos
👉 客户端可以直接拿到服务列表
现实中经常是"混合模式"
text
外部请求
↓
Nginx / Gateway ←(服务端LB)
↓
微服务(Feign)
↓
内部调用(客户端LB)
| 层级 | 负载方式 |
|---|---|
| 外部流量 | 服务端 LB |
| 内部调用 | 客户端 LB(Feign) |
总结:
👉 "Feign 采用客户端负载均衡,通过从注册中心获取服务实例列表并在客户端选择目标节点来完成请求分发。"
Feign 的负载均衡和 gRPC 的客户端负载本质区别
👉 本质区别不在"谁来选节点",而在"连接模型 + 负载粒度 + 实现位置"。
👉 OpenFeign 的负载均衡是"基于请求级别"的;
👉 gRPC 的客户端负载是"基于连接/流(stream)级别"的。
✅ Feign:HTTP 请求级负载
text
每发一次 HTTP 请求 → 选一次实例
- 粒度:请求级
- 每个请求都重新负载均衡
- 天然均匀
✅ gRPC:连接 / Stream 级负载
gRPC 基于 HTTP/2:
text
一个连接(connection) → 多个请求(stream)
text
client
├─ connection1 → server1
│ ├─ stream1
│ ├─ stream2
│ └─ stream3
└─ connection2 → server2
- 粒度:连接 / 子连接(subchannel)
- 请求复用连接
- 不一定每个请求都重新选节点
1️⃣ Feign 更"平均",但成本更高
- 每个请求重新选节点
- 不存在连接粘性问题
👉 优点:
- 简单
- 均匀
👉 缺点:
- 每次都有选择开销
- 不能复用连接(HTTP/1.1为主)
2️⃣ gRPC 更高效,但可能"不均匀"
- 一旦连接建立,请求会复用连接
- 容易出现:
text
大部分流量集中在某几个连接
👉 这就是经典问题:
❗ "gRPC round_robin 还是不均匀"
原因就在这里
负载均衡"位置"的差异
Feign
text
Feign → LoadBalancer → HTTP Client
👉 负载均衡:
- 在 调用前
- 每次请求都会触发
依赖:
- Spring Cloud LoadBalancer
gRPC
text
gRPC Channel
↓
Subchannel(连接池)
↓
LoadBalancer
👉 负载均衡:
- 在 连接管理层
- 不是每次请求都触发
连接模型差异
| 点 | Feign | gRPC |
|---|---|---|
| 协议 | HTTP/1.1(通常) | HTTP/2 |
| 连接 | 短连接 / 连接池 | 长连接 |
| 请求复用 | 弱 | 强(多路复用) |
| 负载粒度 | 请求级 | 连接 / stream 级 |
带来的实际影响
1️⃣ Feign 很少出现"不均衡问题"
因为:
- 每次请求都重新选
2️⃣ gRPC 容易"热点连接"
比如:
- 某个连接承载大量流量
- 某些实例空闲
👉 需要:
- 连接池调优
- subchannel 数量控制
- xDS / Istio
3️⃣ gRPC 更适合高并发
因为:
- 连接复用
- 少 TCP / TLS 开销
gRPC 不做"请求级负载的原因是:
👉 HTTP/2 的设计目标就是"减少连接",而不是"每次重新选择连接"
如果每次请求都选:
- 就破坏了多路复用优势
总结:
👉 "Feign 的负载均衡发生在请求层,每个请求独立选择实例;而 gRPC 的负载均衡发生在连接层,通过管理 subchannel 和 stream 分发流量,因此在性能更高的同时,也带来了连接级不均衡的问题。"
解决 gRPC 负载不均衡问题
👉 gRPC 的"不均衡",本质不是负载算法不行,而是"HTTP/2 长连接 + 多路复用"导致流量粘在少数连接/实例上。
所以解决思路也不是只换个算法,而是从"连接层 + 策略层 + 架构层"一起下手。
👉 从低成本到重方案:
- 调整客户端 LB 策略(round_robin 等)
- 增加连接数(subchannel / channel)
- 控制连接生命周期(定期重建)
- 引入请求级打散(限流/并发控制)
- 使用 xDS / Service Mesh(如 Istio)
text
一个 gRPC 连接(HTTP/2)
↓
承载大量 stream(请求)
↓
流量"粘"在这个连接对应的 server 上
👉 即使你用 round_robin:
- 只在"连接建立时"生效
- 后续请求不会再重新选节点
✅ 方案1:开启 round_robin(基础但不够)
默认是 pick_first(选一个一直用)
👉 改成:
text
round_robin
效果:
text
连接分散到多个实例
⚠️ 但问题:
- 仍然是"连接级均衡"
- 如果连接数少,还是会不均
👉 结论:必须做,但不够
✅ 方案2:增加连接数(最关键)
👉 核心思想:
❗ 既然是"连接级均衡",那就多建几个连接
text
1 个 Channel → N 个 Subchannel(连接)
或者:
text
多个 Channel(连接池)
效果:
text
client
├─ conn1 → server1
├─ conn2 → server2
├─ conn3 → server3
👉 流量自然更均匀
✅ 方案3:限制单连接并发(防止热点)
如果一个连接可以跑无限 stream:
👉 会出现:
text
某个连接被打爆
解决:
- 限制 max concurrent streams
- 控制客户端并发
👉 让请求"被迫分散到其他连接"
✅ 方案4:连接定期重建
👉 问题:
- 长连接会"越用越偏"
解决:
text
定期关闭连接 → 重新建连接 → 重新负载均衡
👉 常见策略:
- TTL(比如几分钟)
- 空闲连接回收
✅ 方案5:客户端"智能负载"(进阶)
自定义策略,比如:
- latency-aware(选延迟低的)
- least-request(最少请求)
- 权重(weighted)
👉 比简单 round_robin 更稳定
✅ 方案6:使用 xDS(大厂方案)
结合:
- Envoy
- Istio
👉 架构:
text
client → sidecar(Envoy) → service
优点:
- 动态下发 LB 策略
- 支持:
- 权重
- 熔断
- 限流
- 灰度发布
👉 本质:
把负载均衡"从代码里拿出来,交给基础设施"
👉 一般不是单点优化,而是组合:
text
✔ round_robin
✔ 多连接(connection pool)
✔ 限制并发
✔ 定期重建连接
如果是大厂:
text
✔ + Service Mesh(Istio)
总结
👉 "gRPC 负载不均衡的本质是连接级调度问题,解决方案是增加连接粒度并控制连接生命周期,而不是单纯依赖负载算法。"
👉 "由于 gRPC 基于 HTTP/2 长连接,请求会复用连接,导致负载均衡发生在连接层而非请求层,从而可能出现流量倾斜。解决方法包括使用 round_robin 策略、增加 subchannel 数量、限制单连接并发、定期重建连接,以及在复杂场景下引入 xDS 或 Service Mesh 来实现更精细的流量调度。"