Feign 第一次调用为什么会很慢?怎么解决?

#Java#面试八股

Feign 第一次调用为什么会很慢?怎么解决?

结论先行:Feign 的第一次调用要一次性完成大量惰性初始化:创建 Feign 子容器、解析 @FeignClient 接口与 Contract、装配 Encoder/Decoder、初始化负载均衡客户端并首次拉取实例列表、建立 TCP 连接(HTTPS 还要握手),再加上类加载与 JIT 尚未预热。第二次开始这些开销都不再发生,于是表现为「首次几百毫秒,之后几毫秒」。

1. 首次调用的耗时构成

第一次 stockClient.get(1L)
 ├─ ① Feign 子容器初始化(NamedContextFactory)       ~100ms+
 ├─ ② 解析接口:Contract → MethodMetadata → RequestTemplate
 ├─ ③ 装配 Encoder/Decoder/Logger/Retryer
 ├─ ④ 负载均衡客户端首次 getInstances()(拉注册中心)
 ├─ ⑤ 建立 HTTP 连接:DNS + TCP 三次握手(+ TLS 握手)
 └─ ⑥ 类加载、JIT 编译、Jackson 首次序列化(反射元数据)

其中 ①④⑤ 是大头,⑥ 在冷启动的容器里也很可观。

2. 为什么是「惰性」的

FeignClientFactoryBean.getTarget() 在第一次调用时才真正构建客户端。更早的版本里,@FeignClient 的 ApplicationContext 是懒创建的;Spring Cloud OpenFeign 后来提供了 lazy-attributes-resolution 配置,默认已改为饿加载属性解析,但连接与实例列表仍然是首次调用才建立。

3. 解决办法

(1)启动预热:最直接有效

@Component
public class FeignWarmUpRunner implements ApplicationRunner {

    private final StockClient stockClient;

    public FeignWarmUpRunner(StockClient stockClient) {
        this.stockClient = stockClient;
    }

    @Override
    public void run(ApplicationArguments args) {
        try {
            // 用一个无副作用的健康/探测接口触发初始化
            stockClient.ping();
        } catch (Exception e) {
            // 预热失败不阻断启动,记录日志即可
            log.warn("feign warm up failed: {}", e.getMessage());
        }
    }
}

(2)开启负载均衡的饿加载

# Ribbon 时代(老项目)
ribbon:
  eager-load:
    enabled: true
    clients: stock-service,user-service
# Spring Cloud LoadBalancer(新项目,按需选择)
spring:
  cloud:
    loadbalancer:
      cache:
        ttl: 10s

(3)换掉默认的 HttpURLConnection,用连接池

spring:
  cloud:
    openfeign:
      httpclient:
        hc5:
          enabled: true          # Apache HttpClient 5,带连接池
        ok-http:
          enabled: false
      client:
        config:
          default:
            connect-timeout: 1000
            read-timeout: 3000

JDK 自带的 HttpURLConnection 没有连接池,keep-alive 行为也不可控;换成 HttpClient 5 或 OkHttp 后,第二次调用直接复用连接,省掉握手。

(4)启动顺序与健康检查

服务刚启动就接流量时,Feign 客户端、数据库连接池、JIT 全都没热。K8s 里配置 readinessProbe 的 initialDelaySeconds,或在就绪前先跑一轮自检,是最省事的做法。

4. 效果对比

措施首次耗时后续耗时代价
不做处理300~800ms3~10ms首个用户感知明显
启动预热首次调用即快不变启动多花几百毫秒
连接池减少握手明显降低多一个依赖
饿加载实例列表减少首次拉取不变启动时依赖注册中心可用

5. 常见追问

追问一:为什么第二次就快了? 客户端的 Feign 实例、RequestTemplate、Encoder 都已被缓存,实例列表在本地缓存里(默认 35s),TCP 连接被连接池复用,JIT 也完成了热点代码编译。

追问二:首次慢会影响到线上吗? 会。发版滚动更新时,新实例刚注册就接到流量,首批请求会慢;如果上游超时设置得紧,还会触发超时重试,放大问题。所以预热 + 就绪探针是标配。

追问三:lazy-attributes-resolution 是什么? Spring Cloud OpenFeign 的属性解析开关。设为 false(默认)时在启动阶段解析 @FeignClient 的属性,避免首次调用才解析;设为 true 则延迟到调用时。它与「连接建立」是两回事,别混为一谈。

追问四:Feign 首次调用会重复拉取实例列表吗? 同一个服务名只会有一个 ServiceInstanceListSupplier,首次拉取后进入缓存。多个 Feign 客户端指向同一服务时共享。

追问五:预热接口会不会影响业务? 用一个只读的 ping/health 端点即可,不要调用有写操作或重资源的接口,否则预热本身就成了故障源。

小结

首次慢不是 bug,是惰性初始化 + 冷连接 + 冷 JIT 的叠加。解决思路就三条:提前触发初始化(预热/饿加载)、复用连接(连接池)、让流量晚一点进来(就绪探针)。三条都做,首次调用的毛刺基本可以消失。

笔记加载中…