微服务中连接、读取、重试的超时问题

概念:

  • HTTP调用,应用层走的HTTP协议,但网络层面始终是TCP/IP协议。TCP/IP是面向连接的协议,在传输数据之前需要建立连接。几乎所有网络框架都会提供两个超时参数。
  • :建立TCP连接的时间;确认需要明白连接的是谁。

连接超时:ConnectTomeout

  • 时间不易过长:让用户配置建连阶段的最长等待时间,一般来说,TCP三次握手建立连接需要的时间非常短,通常在毫秒级最多至秒级,因此,设置(1~9秒)即可。如果是纯内内网用的话,这个参数可以更短,在下游服务离线无法连接的时候,可以快速失败。
  • 排查服务端/Nginx:通常情况下,客户端负载均衡来连服务端,会直接建立连接,此时出现连接超时大概率是服务端的问题;而如果服务端通过类似Nginx反向代理来负载均衡,客户端连接的其实是Nginx,而不是服务端,此时连接超时排查Nginx。

读取超时:ReadTimeout

  • 等待远端返回数据的时间,包括远端程序处理的时间;解决需要考虑下游服务的服务标准和自己的服务标准,设置合适的读取超时时间。
  • 重试:因为HTTP协议中GET请求表示数据查询操作,无状态,并且考虑到常见的网络丢包现象,HTTP客户端或代理服务器会自动重试GET/HEAD请求。
  • 注意:
    • 读取超时时,只要服务端收到请求,网络层面的超时和断开不会影响服务端的执行。因此,出现读取超时不能随意假设服务端的处理情况,需要根据业务状态考虑如何进行后续处理。
    • 读取超时是数据传输的最长耗时+服务端处理业务逻辑的时间。
    • 读取超时设置根据实际情况,对于定时任务和异步任务,超时配置长些问题不大。微服务中短平快的同步接口调用,并发较大,应设置一个较短的读取超时时间,防止被下游服务拖慢,通常不会设置超过30秒。过长会让下游抖动影响到自己,过短可能影响成功率。

超时参数调整:

  • ①连接超时与读取超时在微服务中同时配置才有效
  • ②如果接口设计不支持冥等,需要关闭自动重试,更好的解决方案是遵从HTTP协议使用合适的HTTP方法
  • ③包括HttpClient在内的HTTP客户端及浏览器,都会限制客户端调用的最大并发数。所以在客户端有比较大的请求调用并发,需要调正默认值,防止达到吞吐量的瓶颈。

SpringCloud之Feign超时的配置:

  • Feign有两个超时参数,它使用的负载均衡组件Ribbon也有配置,需要注意以下几点
  • 一、Feign的读取超时默认1秒
    • //RibbonClientConfiguration实现-创建出来默认设置1s
      /**
       * Ribbon client default connect timeout.
       */
      public static final int DEFAULT_CONNECT_TIMEOUT = 1000;
      
      /**
       * Ribbon client default read timeout.
       */
      public static final int DEFAULT_READ_TIMEOUT = 1000;
      
      @Bean
      @ConditionalOnMissingBean
      public IClientConfig ribbonClientConfig() {
         DefaultClientConfigImpl config = new DefaultClientConfigImpl();
         config.loadProperties(this.name);
         config.set(CommonClientConfigKey.ConnectTimeout, DEFAULT_CONNECT_TIMEOUT);
         config.set(CommonClientConfigKey.ReadTimeout, DEFAULT_READ_TIMEOUT);
         config.set(CommonClientConfigKey.GZipPayload, DEFAULT_GZIP_PAYLOAD);
         return config;
      }
      //修改默认配置
      
      feign.client.config.default.readTimeout=3000
      feign.client.config.default.connectTimeout=3000

       

  • 二、读取超时和连接超时需要同时配置才能生效
    • //FeignClientFactoryBean
      if (config.getConnectTimeout() != null && config.getReadTimeout() != null) {
         builder.options(new Request.Options(config.getConnectTimeout(),
               config.getReadTimeout()));
      }
      //针对单独的Feign Client设置超时时间,把default替换为Client的name
      
      feign.client.config.default.readTimeout=3000
      feign.client.config.default.connectTimeout=3000
      feign.client.config.clientsdk.readTimeout=2000
      feign.client.config.clientsdk.connectTimeout=2000

       

  • 三、单独的超时可以覆盖全局超时
  • 四、通过配置Ribbon组件的参数修改两个超时时间
    • ribbon.ReadTimeout=4000
      ribbon.ConnectTimeout=4000

       

  • 五、同时配置Feign和Ribbon,以Feign为准
    • //LoadBalancerFeignClient
      //options不是默认值,创建FieignOptionsClientConfig,Ribbon的配置被Feign覆盖
      
      IClientConfig getClientConfig(Request.Options options, String clientName) {
         IClientConfig requestConfig;
         if (options == DEFAULT_OPTIONS) {
            requestConfig = this.clientFactory.getClientConfig(clientName);
         }
         else {
            requestConfig = new FeignOptionsClientConfig(options);
         }
         return requestConfig;
      }

       

 Ribbon自动重试导致的短信重复发送问题:

  • 出现场景:用户服务调用短信服务,代码中没有重试逻辑。
  • 还原逻辑:一次Get请求发送短信接口,客户端调用用户服务,用户服务通过feign调用短信服务(Feign内部有一个Ribbon组件负责客户端负载均衡,通过配置文件设置其调用的服务端为两个节点,可以验证ribbon的一次重试)
  • 解决:
    • 一、把发送短信接口从Get改为Post,有状态的API接口不应该定义为Get,根据HTTP协议的规范,Get请求用于数据查询,选择Get还是Post的依据,应该是API的行为,而不是参数大小。
    • 二、将MaxAutoRetiresNextServer参数配置为0,禁用服务调用失败后在下一个服务端节点自动重试。
    • ribbon.MaxAutoRetriesNextServer=0
  • 源码:
    •   
      // DefaultClientConfigImpl
      public static final int DEFAULT_MAX_AUTO_RETRIES_NEXT_SERVER = 1;//由此可知Get请求在某个服务端节点出现问题(如读取超时)时,Ribbon会自动重试一次
      public static final int DEFAULT_MAX_AUTO_RETRIES = 0;
      
      // RibbonLoadBalancedRetryPolicy
      public boolean canRetry(LoadBalancedRetryContext context) {
         HttpMethod method = context.getRequest().getMethod();
         return HttpMethod.GET == method || lbContext.isOkToRetryOnAllOperations();
      }
      
      @Override
      public boolean canRetrySameServer(LoadBalancedRetryContext context) {
         return sameServerCount < lbContext.getRetryHandler().getMaxRetriesOnSameServer()
               && canRetry(context);
      }
      
      @Override
      public boolean canRetryNextServer(LoadBalancedRetryContext context) {
         // this will be called after a failure occurs and we increment the counter
         // so we check that the count is less than or equals to too make sure
         // we try the next server the right number of times
         return nextServerCount <= lbContext.getRetryHandler().getMaxRetriesOnNextServer()
               && canRetry(context);
      }

       

posted @ 2021-12-06 11:30  白玉神驹  阅读(1066)  评论(0编辑  收藏  举报