【Spring Cloud】FeignClient 首次請求花費大量時間/超時的問題

問題:

Spring Cloud項目啓動後,首次使用 FeignClient 請求每每會消耗大量時間,並有必定機率所以致使請求超時。java

基本就是兩個問題:git

  1. FeignClient 首次請求耗時較長;
  2. FeignClient 首次請求失敗。

探索

仔細觀察日誌,會發現本質上是由於FeignClient 的初始化花費了大量時間。github

2019-01-28  16:19:46.074    INFO  3740  ---  [nio-9790-exec-2]  s.c.a.AnnotationConfigApplicationContext  :  Refreshing  SpringClientFactory-ms-dyh-manufacturer:  startup  date  [Mon  Jan  28  16:19:46  CST  2019];  parent:  org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext@15b986cd
2019-01-28  16:19:46.411    INFO  3740  ---  [nio-9790-exec-2]  f.a.AutowiredAnnotationBeanPostProcessor  :  JSR-330  'javax.inject.Inject'  annotation  found  and  supported  for  autowiring
2019-01-28  16:19:46.671    INFO  3740  ---  [nio-9790-exec-2]  c.netflix.config.ChainedDynamicProperty    :  Flipping  property:  ms-dyh-manufacturer.ribbon.ActiveConnectionsLimit  to  use  NEXT  property:  niws.loadbalancer.availabilityFilteringRule.activeConnectionsLimit  =  2147483647
2019-01-28  16:19:46.715    INFO  3740  ---  [nio-9790-exec-2]  c.n.u.concurrent.ShutdownEnabledTimer        :  Shutdown  hook  installed  for:  NFLoadBalancer-PingTimer-ms-dyh-manufacturer
2019-01-28  16:19:46.776    INFO  3740  ---  [nio-9790-exec-2]  c.netflix.loadbalancer.BaseLoadBalancer    :  Client:  ms-dyh-manufacturer  instantiated  a  LoadBalancer:  DynamicServerListLoadBalancer:{NFLoadBalancer:name=ms-dyh-manufacturer,current  list  of  Servers=[],Load  balancer  stats=Zone  stats:  {},Server  stats:  []}ServerList:null
2019-01-28  16:19:46.783    INFO  3740  ---  [nio-9790-exec-2]  c.n.l.DynamicServerListLoadBalancer            :  Using  serverListUpdater  PollingServerListUpdater
2019-01-28  16:19:46.810    INFO  3740  ---  [nio-9790-exec-2]  c.netflix.config.ChainedDynamicProperty    :  Flipping  property:  ms-dyh-manufacturer.ribbon.ActiveConnectionsLimit  to  use  NEXT  property:  niws.loadbalancer.availabilityFilteringRule.activeConnectionsLimit  =  2147483647
2019-01-28  16:19:46.811    INFO  3740  ---  [nio-9790-exec-2]  c.n.l.DynamicServerListLoadBalancer            :  DynamicServerListLoadBalancer  for  client  ms-dyh-manufacturer  initialized:  DynamicServerListLoadBalancer:{NFLoadBalancer:name=ms-dyh-manufacturer,current  list  of  Servers=[192.168.0.114:8360],Load  balancer  stats=Zone  stats:  {unknown=[Zone:unknown;	Instance  count:1;	Active  connections  count:  0;	Circuit  breaker  tripped  count:  0;	Active  connections  per  server:  0.0;]
},Server  stats:  [[Server:192.168.0.114:8360;	Zone:UNKNOWN;	Total  Requests:0;	Successive  connection  failure:0;	Total  blackout  seconds:0;	Last  connection  made:Thu  Jan  01  08:00:00  CST  1970;	First  connection  made:  Thu  Jan  01  08:00:00  CST  1970;	Active  Connections:0;	total  failure  count  in  last  (1000)  msecs:0;	average  resp  time:0.0;	90  percentile  resp  time:0.0;	95  percentile  resp  time:0.0;	min  resp  time:0.0;	max  resp  time:0.0;	stddev  resp  time:0.0]
]}ServerList:ConsulServerList{serviceId='ms-dyh-manufacturer',  tag=null}

事實上在15年的時候已經有人提出這個問題 issue,當前這個issue還處於open狀態。
在這裏插入圖片描述
能夠發現,這哥們判斷的緣由跟咱們判斷的一致:儘管feign client是在啓動時被建立,但真正的初始化倒是在首次使用feign client的時候進行的。web

解決

1. (不建議)將 Hystrix 的超時時間調高

### Hystrix 配置
hystrix:
  command:
    default:
      execution:
        isolation:
          thread:
            timeoutInMilliseconds: 5000

理論上這是一個治標的辦法,這樣處理可以解決超時的問題,但沒法解決首次花費時間長的問題。同時由於須要將熔斷器的超時時間設置得更長,等價於在必定程度上限制了熔斷器的適用範圍。
因此可用這個方法,但不推薦這個方法。spring

2. (傻子)禁用 Hystrix 的超時時間

這個方法簡直就是傻子才選的,不描述了。app

3. 模擬請求進行warm up

基本思路:在spring容器初始化後,找到全部實現了FeignClient 的bean,主動發起任意請求,該請求會致使feign client的真正初始化。ide

step1. 對feign client的接口添加方法svg

@GetMapping("/actuator/health")
String heartbeat();

step2. 添加ApplicationListener在spring context加載完後,找到全部的feign client,並經過反射執行一次heart beat,此時便會取巧地觸發feign client的初始化。ui

@Component
public class EarlyInitFeignClientOnContextRefresh implements
        ApplicationListener<ContextRefreshedEvent> {
    Logger logger = LoggerFactory.getLogger(EarlyInitFeignClientOnContextRefresh.class);

    @Autowired()
    @Qualifier("cachingLBClientFactory")
    CachingSpringLoadBalancerFactory factory;

    @Override
    public void onApplicationEvent(ContextRefreshedEvent event) {
        ApplicationContext applicationContext = event.getApplicationContext();
        Map<String, Object> beans = applicationContext.getBeansWithAnnotation(FeignClient.class);
        for (Map.Entry<String, Object> entry :
                beans.entrySet()) {
            Class<?> clazz = entry.getValue().getClass();
            try {
                Method method = null;
                method = clazz.getMethod("heartbeat");
                method.invoke(entry.getValue());
                logger.warn("init feign client:  " + clazz.getName());
            } catch (NoSuchMethodException e) {
                logger.warn("init feign client fail: no method of heartbeat in " + clazz.getName());
            } catch (IllegalAccessException e) {
                logger.warn("init feign client fail: IllegalAccessException of " + clazz.getName());
            } catch (InvocationTargetException e) {
                logger.warn("init feign client fail: InvocationTargetException of " + clazz.getName());
            } catch (Exception e){
                logger.error(e.getMessage());
            }
        }

        logger.info("init feign client done!");
    }
}

可是這種方法肯存在必定的風險,feign的contributor有提到他們之因此用lazy creation是由於不這麼作的話在某些特定場景下會存在問題。
在這裏插入圖片描述.net

擴展思考

  1. FeignClient的初始化過程;
  2. FeignClient初始化各階段的消耗時長,進而具體哪一步耗時最長(是不是註冊和發現);
  3. 當FeignClient調用的服務不在線時,可否保證方法三仍舊有效;
  4. 切入到FeignClient真實建立過程的初始化,而非經過使用client發起請求模擬地達到這個目的。