文章

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 动态代理 实现:

  1. Spring 启动时扫描 @FeignClient 注解的接口
  2. 为每个接口生成动态代理对象
  3. 调用方法时,代理对象解析注解(如 @GetMapping),拼接参数生成 HTTP 请求
  4. 通过集成的 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 的原因

  1. 与熔断器同框架:统一配置、统一监控,无需引入额外依赖
  2. YAML 配置:修改重试策略不用改代码,适合运维
  3. 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-hc5feign.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.enabledmatchIfMissing=true),不是旧版的 spring.cloud.openfeign.httpclient.enabled

4. 判断是否生效

启用后可通过监控指标验证排队是否消失:

1
2
# Feign 调用耗时(P95 应在合理范围,不再随并发线性上涨)
curl -s "http://localhost:48086/actuator/metrics/feign.call?tag=percentile:0.95"
本文由作者按照 CC BY 4.0 进行授权