一、背景

项目中存在业务需求,在服务启动的时候调用远程服务获取数据初始化。
已知前提

  • 数据来源通过 Feign远程调用获取。
  • 初始化逻辑实现类实现了 ApplicationRunner接口
    实现ApplicationRunner接口,保证初始化数据时,spring 容器启动完成,包括远程 FeignClient已经实例化,保证API能够正常发起访问。
  • Feign 开启了 hystrix 熔断访问

相同代码在测试及本地环境访问时正常执行数据初始化逻辑运行正常,不过部署到正式环境时数据初始化异常,Feign 调用失败直接进入到 fallback 中。
排查被调用服务 http trace ,发现服务请求正常传递并且正常响应200,可以推断数据初始化失败是 Feign 超时导致的服务熔断。

二、Feign 调用超时分析

2.1、Feign + Hystrix + Ribbon 发起一个RPC调用

项目使用 Spring Cloud 全家桶,使用 Feign 作为远程调用、Ribbon 做的客户端负载均衡、Hystrix 做的服务熔断(采用线程隔离)。

在项目启动后发起的第一次 Feign 调用都发生了什么:

  • Hystrix 通过 Future 包装请求,同时超时时长默认为 1000 ms

Hystrix 线程隔离使用的多线程编程模块 Future发起线程执行,通过设定 Future超时时长实现的服务熔断。
默认情况下,Hystrix 超时时长为1000msHystrixCommandProperties#**_default_executionTimeoutInMilliseconds=1000_**)。

  • Future 执行线程逻辑:Ribbon 进行负载均衡处理。
  • Ribbon 负载均衡得到对应服务器信息,通过 Feign 拼接 Http 请求,发起服务调用。

  • 2.1.1、Hystrix 做了些什么?

    在线程隔离模式下,所有的请求都会经由 HystrixCommand 再一次封装,然后通过 Future 发起新的线程请求,同时指定超时时间(默认:1000ms)。

2.1.2、Ribbon 做了些什么

《Feign Ribbon Support》

Ribbon 实现的功能是:客户端的负载均衡。
Feign 组合 Ribbon 后,提供专门的类 LoadBalancereignClient来完成远程RPC拼接和调用。
同时 LoadBalancerFeignClient还提供了客户端负载均衡。

2.1.3、Feign 做了些什么?

Feign 的功能在引入 RIbbon 模块,提供新的类 LoadBalancerFeignClient该类提供客户端负载均衡的同时也负责 Feign 原先的功能:拼接 RPC 调用,发起 RPC 调用

2.2、导致超时的可能因素及解决方案

2.2.1、Hystrix 超时时间不够长(延长超时时长)

多数情况下该原因不成立,Hystrix 默认超时时间为:1000ms,已经满足大多数 RPC 调用需求。
不过也存在例外,比如网络等因素就可能导致请求响应超时,导致请求被熔断。
大多数情况下出现熔断可以容忍,不过在特殊情况下,比如:服务启动时的数据初始化场景下,请求被熔断将导致业务异常。

所以针对该情况,可以适当性的增加 Hystrix 的超时时长,配置如下:

  1. ## 线程隔离超时时长默认:5s(全局配置)
  2. hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds= 5000

在某些特殊场景下如果需要延长 Hystrix 超时时长,可以针对个别接口进行配置,配置如下:

也可以直接在方法上追加 @HystrixCommand 注解(未验证)

  1. ## 局部设置超时:
  2. hystrix.command.<HystrixCommandKey>.execution.isolation.thread.timeoutInMilliseconds: 1000
  3. ## 例如:hystrix.command 后对应 FeignClient 接口方法全路径名称
  4. hystrix.command.xxxFeignClient#xxxMethod(String,Integer).execution.isolation.thread.timeoutInMilliseconds=10000

2.2.2、LoadBalancerFeignClient 加载 FeignLoadBalancer 为懒加载(变更为饥饿加载)

LoadBalancerFeignClient 实现客户端负载均衡以及发起Feign RPC 调用的大体方法时序如下:
LoadBalancerFeignClient#execute -> LoadBalancerFeignClient#lbClient -> FeignLoadBalancer#executeWithLoadBalancer

在整个流程中发起执行的对象为 FeignLoadBalancer,而 FeignLoadBalancer 的获取默认情况下是懒加载的,即第一次发起 Feign RPC 调用时才会实例化该对象,相关源码如下:

image.png
上述代码讲述了,在获取 FeignLoadBalancer实例对象时

  • 尝试从缓存中获取实例FeignLoadBalancer,如果存在直接返回
  • 如果缓存中不存在,则进行实例化操作然后放入缓存中

FeignLoadBalancer懒加载在一定程度上使得第一次 Feign 调用耗时更长,加上网络因素以及请求响应可能是的第一次 Feign 调用超过 1s,从而导致 Hystrix 熔断(默认情况为1s)。

针对该情况,可以通过配置,在项目启动时就把对应的 FeignLoadBalancer对象实例化放入到内存中,从而减少第一次 Feign 调用中的请求时长损耗,配置如下(官方参考配置

  1. ## 开启接加载
  2. ribbon.eager-load.enabled = true
  3. ## 配置需要加载的 FeignLoadBalancer(对应 @FeignClient(value=xxx) 中的 value 值)
  4. ribbon.eager-load.clients = xxxclient1xxxclient2

2.2.3、Feign RPC 超时

本案例背景下的超时并不是因为远程服务调用超时而导致的问题,不过在分析过程中得出结论,在某些场景下 Feign RPC 超时同样会导致熔断信息。

拼接请求发起 RPC 调用代码,RibbonLoadBalancingHttpClient#execute
image.png

服务在发起远程服务调用会指定对应连接超时,读超时,可以通过配置延长这两个值从而延长 Fiegn 请求等待。

不过上述值的配置不宜过长,超时太长,并且并发过高会导致连接池耗尽,服务阻塞等待。