微服务中连接、读取、重试的超时问题
概念:
- 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); }
-