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~800ms | 3~10ms | 首个用户感知明显 |
| 启动预热 | 首次调用即快 | 不变 | 启动多花几百毫秒 |
| 连接池 | 减少握手 | 明显降低 | 多一个依赖 |
| 饿加载实例列表 | 减少首次拉取 | 不变 | 启动时依赖注册中心可用 |
5. 常见追问
追问一:为什么第二次就快了?
客户端的 Feign 实例、RequestTemplate、Encoder 都已被缓存,实例列表在本地缓存里(默认 35s),TCP 连接被连接池复用,JIT 也完成了热点代码编译。
追问二:首次慢会影响到线上吗? 会。发版滚动更新时,新实例刚注册就接到流量,首批请求会慢;如果上游超时设置得紧,还会触发超时重试,放大问题。所以预热 + 就绪探针是标配。
追问三:lazy-attributes-resolution 是什么?
Spring Cloud OpenFeign 的属性解析开关。设为 false(默认)时在启动阶段解析 @FeignClient 的属性,避免首次调用才解析;设为 true 则延迟到调用时。它与「连接建立」是两回事,别混为一谈。
追问四:Feign 首次调用会重复拉取实例列表吗?
同一个服务名只会有一个 ServiceInstanceListSupplier,首次拉取后进入缓存。多个 Feign 客户端指向同一服务时共享。
追问五:预热接口会不会影响业务?
用一个只读的 ping/health 端点即可,不要调用有写操作或重资源的接口,否则预热本身就成了故障源。
小结
首次慢不是 bug,是惰性初始化 + 冷连接 + 冷 JIT 的叠加。解决思路就三条:提前触发初始化(预热/饿加载)、复用连接(连接池)、让流量晚一点进来(就绪探针)。三条都做,首次调用的毛刺基本可以消失。