OpenFeign vs Dubbo
OpenFeign vs Dubbo
微服务间通信的两种主流方案对比。一句话总结:OpenFeign 走 HTTP+JSON,通用易用;Dubbo 走 TCP+二进制,性能更高。
一、核心差异速览
| 维度 | OpenFeign | Dubbo |
|---|---|---|
| 通信协议 | HTTP | TCP(默认) |
| 序列化 | JSON(文本) | Hessian2(二进制) |
| 连接模型 | 短连接 / 连接池 | 单一长连接 + NIO |
| 底层框架 | JDK 动态代理 + HTTP 客户端 | Netty/NIO 异步非阻塞 |
| 生态 | Spring Cloud 原生集成 | 自成体系,已融入 Spring Cloud Alibaba |
| 跨语言 | ✅ 天然支持 | ❌ 主要 Java 生态 |
| 性能 | 一般 | 较高(内网场景约快 30%) |
| 学习成本 | 低 | 中 |
二、OpenFeign
1. 工作原理
OpenFeign 基于 JDK 动态代理 实现:
- Spring 启动时扫描
@FeignClient注解的接口 - 为每个接口生成动态代理对象
- 调用方法时,代理对象解析注解(如
@GetMapping),拼接参数生成 HTTP 请求 - 通过集成的 HTTP 客户端发送请求,响应结果反序列化为返回值类型
2. 与原生 Feign 的区别
| 对比项 | 原生 Feign | OpenFeign |
|---|---|---|
| 注解支持 | 自有注解 | Spring MVC 注解(@RequestMapping 等) |
| 负载均衡 | 无 | 自动集成 |
| Spring 整合 | 无 | 无缝整合 |
简单说,OpenFeign 就是 Feign 的 “Spring 化”增强版。
3. 为什么第一次调用慢?
OpenFeign 默认懒加载,首次调用时才会:
- 生成动态代理实例
- 初始化负载均衡客户端
- 从注册中心拉取服务列表并缓存
- 初始化 HTTP 客户端(创建连接池、TCP 连接)
这些操作叠加导致首次响应耗时几百毫秒到几秒。
优化:生产环境建议改为饥饿加载模式,启动时即完成初始化。
1
2
3
4
spring:
cloud:
openfeign:
lazy-attributes-resolution: true # 默认 false(懒加载),设为 true 启动时即初始化 Feign 客户端
4. 服务降级(Fallback)
在 @FeignClient 注解上通过 fallback 属性指定降级类。当调用下游失败时,执行降级类中的默认逻辑(如返回默认数据),提高容错能力。
5. 性能优化手段
- 替换 HTTP 客户端:用 Apache HttpClient 5 或 OkHttp(支持连接池)替代默认的 URLConnection
- 合理设置超时:避免线程长时间阻塞
- 启用 Gzip 压缩:减少网络传输量
- 简化参数类型:避免复杂对象,减少序列化开销
三、Dubbo
1. 通信模型与协议
- 底层框架:基于 Netty/NIO,异步非阻塞通信,低延迟、高吞吐
- 默认协议:自研 Dubbo 协议,基于 TCP 单一长连接 + NIO(报文头部精简、字段语义明确,适合内网高频、低延迟的服务间交互)
- 多协议支持:还支持 HTTP、RMI、Redis、Thrift、gRPC 等
2. 序列化机制
- 默认:Hessian2,高效的二进制序列化
- 可选:FastJSON、Kryo、Java 原生序列化等
二进制序列化比 JSON 体积更小、解析更快,这是 Dubbo 性能优势的核心来源。
3. 调用模式
| 模式 | 说明 |
|---|---|
| 同步调用 | 请求/响应双向通信(最常用) |
| 异步回调 | 调用后不阻塞,通过回调处理结果 |
| 单向调用 | 只发请求不等响应(如日志上报) |
| 泛化调用 | 不依赖接口 SDK,常用于网关 |
基于 JDK 动态代理生成 Stub(客户端)和 Skeleton(服务端),对开发者屏蔽网络细节,像调用本地方法一样调用远程服务。
4. 结果缓存
通过 @DubboService(cache = "...") 开启声明式缓存,减少重复网络调用。
| 缓存类型 | SPI 名 | 支持 TTL | 说明 |
|---|---|---|---|
| 无过期 | lru / lfu |
❌ | 基于淘汰策略,永不过期 |
| 带过期 | expiring |
✅ | 可配置 TTL(单位:秒) |
| JCache | jcache |
✅ | 基于 JCache 标准实现(需引入额外依赖) |
1
2
// 推荐:带 TTL 的过期缓存
@DubboService(cache = "expiring", parameters = {"seconds", "30"})
注意:缓存为单机内存缓存,多节点不共享,仅限读操作。生产环境建议用 Spring Cache + Redis 替代。
四、如何选择?
| 场景 | 推荐 | 理由 |
|---|---|---|
| Spring Cloud 生态项目 | Feign | 零成本集成,与生态无缝 |
| 需要跨语言调用 | Feign | HTTP 协议天然跨语言 |
| 内网高并发、低延迟 | Dubbo | 二进制协议性能更高 |
| 对监控运维要求高 | Feign | HTTP 监控体系更成熟(Prometheus + Grafana / ELK) |
| 已有 Dubbo 技术栈 | Dubbo | 保持一致性,降低学习成本 |
五、重试、降级、熔断、限流、监控
微服务调用随时可能失败,需要一套容错体系保障核心链路可用。本项目使用 Feign + Resilience4j 构建 四层防护:
1
2
3
4
5
6
7
8
9
请求进入
↓
① RateLimiter(限流)─── 超过阈值 → 直接拒绝,保护下游
↓
② Feign 调用 ─────────── 发请求到下游服务
↓
③ Retry(重试)─────── 瞬时故障 → 重试 N 次,重试耗尽 → 降级返回默认值
↓
④ CircuitBreaker(熔断)─ 持续失败 → 断路 OPEN → 快速降级
1. 重试(Retry)
为什么要重试?
网络抖动、服务瞬时重启等场景下,重试可以自愈瞬时故障,无需人工介入。
幂等性是重试的前提
| 接口类型 | 是否幂等 | 能否重试 | 示例 |
|---|---|---|---|
| 查询(GET) | ✅ 幂等 | ✅ 安全重试 | 查询用户资料 |
| 删除(DELETE) | ✅ 幂等 | ✅ 安全重试 | 删除帖子 |
| 新增(POST) | ❌ 非幂等 | ⚠️ 可能重复 | 创建订单、扣款 |
| 修改(PUT) | ⚠️ 视场景 | ⚠️ 谨慎 | 更新库存 |
建议:非幂等接口如需重试,通过唯一请求 ID(幂等键)保证重复操作不会产生副作用。
三种重试方案对比
| 维度 | Feign Retryer | Spring Retry | Resilience4j Retry |
|---|---|---|---|
| 粒度 | 客户端级 | 方法级 | 方法级 |
| 配置方式 | Java Bean | 注解属性 | YAML 配置 |
| 与熔断器集成 | ❌ 独立 | ❌ 需手动配合 | ✅ 原生集成 |
| 指标监控 | ❌ 无 | ❌ 无 | ✅ Micrometer 指标 |
| fallback | ❌ 无 | @Recover |
fallbackMethod |
| 退避策略 | 固定间隔 | @Backoff(指数) |
YAML 配置(指数/随机) |
| 异常过滤 | ❌ 无 | noRetryFor |
exclude-exceptions |
Feign Retryer(客户端级)
Feign 自带重试器,默认 NEVER_RETRY(不重试):
1
2
3
4
5
6
// Feign 默认行为:永不重试(源码 FeignClientsConfiguration#feignRetryer)
@Bean
@ConditionalOnMissingBean
public Retryer feignRetryer() {
return Retryer.NEVER_RETRY;
}
如果开启,会作用于 整个 @FeignClient 的所有方法,无法区分幂等与非幂等接口,不推荐使用。
Spring Retry(方法级)
通过 @Retryable 注解实现方法级重试:
1
2
3
4
5
6
7
8
9
10
11
12
@Retryable(
value = Exception.class, // 对哪些异常重试
maxAttempts = 3, // 最多重试 3 次
backoff = @Backoff(delay = 200, multiplier = 2), // 指数退避 200ms → 400ms → 800ms
noRetryFor = {BusinessException.class} // 业务异常不重试
)
public List<ProfileVO> listByUserIds(List<String> userIds) { ... }
@Recover // 重试耗尽后降级
public List<ProfileVO> recover(Exception e, List<String> userIds) {
return null;
}
Resilience4j Retry(方法级,本项目采用)
通过 @Retry 注解 + YAML 配置实现,与熔断器、限流器同框架,统一管理:
1
2
3
4
5
6
7
@Retry(name = "user-profile-retry", fallbackMethod = "retryFallback")
public List<ProfileVO> listByUserIds(List<String> userIds) { ... }
// fallbackMethod 可以是 private(比 Spring Retry 的 @Recover 更灵活)
private List<ProfileVO> retryFallback(List<String> userIds, Exception e) {
return null;
}
1
2
3
4
5
6
7
8
9
10
11
12
resilience4j:
retry:
configs:
default:
max-attempts: 3 # 最多重试 3 次
wait-duration: 200ms # 初始等待
wait-duration-increasing-factor: 2 # 指数退避倍数
exclude-exceptions: # 不重试的异常
- com.catfun.common.core.exception.BusinessException
instances:
user-profile-retry:
base-config: default
本项目选择 Resilience4j Retry 的原因
- 与熔断器同框架:统一配置、统一监控,无需引入额外依赖
- YAML 配置:修改重试策略不用改代码,适合运维
- Micrometer 指标:重试次数、成功率自动暴露到 Prometheus
2. 降级(Fallback)
什么是降级?
调用失败时返回兜底数据,保证核心功能可用。例如帖子列表查不到用户昵称,就显示空昵称,而不是整个页面报错。
三种降级方式
| 方式 | 框架 | 触发条件 | 本项目用法 |
|---|---|---|---|
fallbackMethod |
Resilience4j | 重试耗尽 / 限流拒绝 | 返回 null |
FallbackFactory |
OpenFeign | Feign 调用异常 / 熔断 OPEN | 返回空列表 |
mock |
Dubbo | 调用异常 | 返回默认值 |
Resilience4j fallbackMethod(业务层降级)
1
2
3
4
5
6
7
@Retry(name = "user-profile-retry", fallbackMethod = "retryFallback")
public List<ProfileVO> listByUserIds(List<String> userIds) { ... }
private List<ProfileVO> retryFallback(List<String> userIds, Exception e) {
log.warn("重试耗尽,降级返回 null: {}", e.getMessage());
return null;
}
OpenFeign FallbackFactory(Feign 层降级)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Component
public class UserProfileFeignClientFallbackFactory implements FallbackFactory<UserProfileFeignClient> {
@Override
public UserProfileFeignClient create(Throwable cause) {
// 熔断器 OPEN 时降级(快速失败,不重试)
if (cause instanceof CallNotPermittedException) {
return new UserProfileFeignClient() {
@Override
public Result<List<ProfileVO>> listByUserIds(List<String> userIds) {
return Result.ok(Collections.emptyList());
}
};
}
// 其他异常向上抛出,交给 Retry 重试
throw new RuntimeException("Feign 调用失败,等待重试", cause);
}
}
降级策略的关键设计
FallbackFactory 只在熔断 OPEN 时降级,其他异常向上抛给重试层。
如果所有异常都降级,@Retry 会认为调用”成功”了(返回了空列表),从而跳过重试。正确顺序:
1
2
瞬时故障 → Retry 重试 3 次 → 重试耗尽 → fallbackMethod 降级
持续失败 → 熔断 OPEN → FallbackFactory 快速降级
3. 熔断(Circuit Breaker)
什么是熔断?
当下游服务持续异常时,快速失败不再发请求,防止故障扩散(雪崩)。相当于电路中的保险丝。
三种状态
1
2
3
4
5
6
7
CLOSED(正常)──失败率达阈值──> OPEN(熔断)
↓ 等待 N 秒
HALF_OPEN(半开,放行少量探测请求)
↓ 探测成功
CLOSED(恢复)
↓ 探测失败
OPEN(再次熔断)
Resilience4j CircuitBreaker 配置
1
2
3
4
5
6
7
8
9
resilience4j:
circuitbreaker:
configs:
default:
sliding-window-size: 100 # 统计窗口(最近 100 次)
minimum-number-of-calls: 20 # 至少 20 次才统计
failure-rate-threshold: 50 # 失败率 > 50% 触发熔断
wait-duration-in-open-state: 10s # 熔断持续 10s
permitted-number-of-calls-in-half-open-state: 10 # 半开时放行 10 次探测
熔断器在 Feign 层(不在业务层)
| 方案 | 位置 | 实例名 | 特点 |
|---|---|---|---|
| Feign 自动熔断(本项目) | Feign 层 | 自动生成(类名+方法名+返回类型) |
与业务层 Retry 分离 |
@CircuitBreaker 注解 |
业务层 | 可自定义 | 与 @Retry 在同一方法,重试会算入熔断统计 |
本项目选择 Feign 自动熔断,原因:与业务层 @Retry 职责分离,互不干扰。
熔断 vs 降级
| 维度 | 熔断 | 降级 |
|---|---|---|
| 目的 | 保护下游(不发请求) | 保护上游(返回兜底数据) |
| 触发 | 失败率达阈值 | 调用失败 |
| 状态 | 有状态(OPEN/CLOSED) | 无状态 |
| 关系 | 熔断 OPEN 后触发降级 | 降级是熔断的结果 |
4. 限流(RateLimiter)
什么是限流?
控制请求速率,防止下游服务被压垮。相当于地铁早高峰的限流进站。
Resilience4j RateLimiter 配置
1
2
3
4
5
6
7
resilience4j:
ratelimiter:
configs:
default:
limit-for-period: 50 # 每周期允许 50 次
limit-refresh-period: 1s # 每秒刷新(即 50 QPS)
timeout-duration: 0 # 超限不等待,直接拒绝
限流 vs 熔断
| 维度 | 限流 | 熔断 |
|---|---|---|
| 目的 | 主动保护(控制流量) | 被动保护(故障后断路) |
| 触发 | 请求速率超限 | 失败率超限 |
| 状态 | 有周期(每秒刷新) | 有状态(OPEN/CLOSED) |
5. 监控(Metrics)
本项目通过 common-monitor 模块统一采集指标:
| 指标 | 含义 | 访问路径 |
|---|---|---|
feign.call |
Feign 调用耗时 | /actuator/metrics/feign.call |
dubbo.call |
Dubbo 调用耗时 | /actuator/metrics/dubbo.call |
resilience4j.circuitbreaker.state |
熔断器状态 | /actuator/metrics/resilience4j.circuitbreaker.state |
resilience4j.retry.calls |
重试次数 | /actuator/metrics/resilience4j.retry.calls |
resilience4j.ratelimiter.available.permissions |
剩余可用许可 | /actuator/metrics/resilience4j.ratelimiter.available.permissions |
查看百分位耗时(P50/P95/P99)
1
2
3
4
5
# Feign 调用 P95 耗时
curl -s "http://localhost:48083/actuator/metrics/feign.call?tag=percentile:0.95"
# 熔断器状态(0=CLOSED, 1=OPEN, 2=HALF_OPEN)
curl -s "http://localhost:48083/actuator/circuitbreakers"
四层防护对应指标
| 防护层 | 指标 | 含义 |
|---|---|---|
| 限流 | resilience4j.ratelimiter.* |
限流次数、剩余许可 |
| 重试 | resilience4j.retry.calls |
重试成功/失败次数 |
| 熔断 | resilience4j.circuitbreaker.* |
状态、调用次数、失败率 |
| 调用 | feign.call / dubbo.call |
调用耗时、次数 |
六、Feign 连接池瓶颈(压测实战排查)
1. 问题背景
压测时 RT 随并发飙升:并发 2 时 p50=29ms,并发 40 时 p50=355ms。排查发现根因不在业务逻辑,而在 Feign 的 HTTP 客户端连接池。
2. 根因
| 链路 | 说明 |
|---|---|
| 下单链路 | 每单有 2 次 Feign 调用(查商品详情 + TCC 冻结库存),都打到 merchant 服务 |
| 默认客户端 | 项目未引 feign-httpclient,Feign 走 JDK HttpURLConnection |
| 致命限制 | JDK keep-alive 连接池默认上限 5 个连接/每主机 |
| 结果 | 并发超过 5 时,多余请求排队等连接释放 → RT 随并发线性上涨 |
类比:一个窗口只有 5 个接线员,来了 40 个电话,35 个排队等着,每个电话的响应时间自然被拉长。
3. 最终采用方案B(治本,已实施)
引入 feign-hc5(feign.hc5.ApacheHttp5Client)获得真正可配置的连接池,trade 服务的 application.yml:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
spring:
cloud:
openfeign:
httpclient:
hc5:
enabled: true # 启用 Apache HttpClient5
max-connections: 200 # 总连接数上限
max-connections-per-route: 100 # 单主机路由连接数上限(下单链路只打 merchant,按单主机算)
connection-timeout: 2000 # 建立连接超时(毫秒)
client:
config:
default:
connect-timeout: 5000 # 连接超时(毫秒)
read-timeout: 10000 # 读取超时(毫秒)
注意:openfeign 5.x 的开关是
spring.cloud.openfeign.httpclient.hc5.enabled(matchIfMissing=true),不是旧版的spring.cloud.openfeign.httpclient.enabled。
4. 判断是否生效
启用后可通过监控指标验证排队是否消失:
1
2
# Feign 调用耗时(P95 应在合理范围,不再随并发线性上涨)
curl -s "http://localhost:48086/actuator/metrics/feign.call?tag=percentile:0.95"