Http调用篇 1. 超时 对于 HTTP 调用,虽然应用层走的是 HTTP 协议,但网络层面始终是 TCP/IP 协议。TCP/IP 是面向连接的协议,在传输数据之前需要建立连接。几乎所有的网络框架都会提供这么两个超时参数。
1.1 连接超时 连接超时参数( ConnectTimeout): 让用户配置建连阶段的最长等待时间
误区 :
连接超时配置得特别长,比如 60 秒。
一般而言,TCP三次握手的时间非常的短,如果很久没有建立连接,很可能是网络或防火墙的问题。如果几秒连接不上,可能永远也连接不上。所以连接超时时间设置特别长意义不大,1~5s即可。
排查连接超时问题,却没理清连的是哪里。
一般而言,服务端会有很多个节点,如果通过客户端负载均衡,那么是直接与服务端建立连接,如果服务端是通过Nginx的反向代理来负载均衡,那么是与Nginx建立连接。
直接连接服务端(排查服务端的问题)
连接Nginx(排查Nginx的问题)
1.2 读取超时 读取超时参数(ReadTimeout): 用来控制从 Socket 上读取数据的最长等待时间。
误区 :
出现了读取超时,服务端的执行就会中断。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 @RestController @RequestMapping("clientreadtimeout") @Slf4j public class ClientReadTimeoutController { private String getResponse (String url, int connectTimeout, int readTimeout) throws IOException { return Request.Get("http://localhost:8080/clientreadtimeout" + url) .connectTimeout(connectTimeout) .socketTimeout(readTimeout) .execute() .returnContent() .asString(); } @GetMapping("client") public String client () throws IOException { log.info("client1 called" ); return getResponse("/server?timeout=5000" , 1000 , 2000 ); } @GetMapping("server") public void server (@RequestParam("timeout") int timeout) throws InterruptedException { log.info("server called" ); TimeUnit.MILLISECONDS.sleep(timeout); log.info("Done" ); } }
调用 client 接口后,从日志中可以看到,客户端 2 秒后出现了 SocketTimeoutException,原因是读取超时,服务端却丝毫没受影响在 3 秒后执行完成。
类似 Tomcat 的 Web 服务器都是把服务端请求提交到线程池处理的,只要服务端收到了请求,网络层面的超时和断开便不会影响服务端的执行。因此,出现读取超时不能随意假设服务端的处理情况,需要根据业务状态考虑如何进行后续处理。
认为读取超时只是 Socket 网络层面的概念,是数据传输的最长耗时,故将其配置得非常短,比如 100 毫秒。
确切地说,读取超时指的是,向 Socket 写入数据后,我们等到 Socket 返回数据的超时时间,其中包含的时间或者说绝大部分的时间,是服务端处理业务逻辑的时间。
认为超时时间越长任务接口成功率就越高,将读取超时参数配置得太长。
HTTP请求一般是需要获得结果,属于同步调用。当服务端处理时间过长,客户端的线程(Tomcat线程)一直处于等待状态,当出现大量超时时,并发情况下可能会创建大量的线程,最终导致程序崩溃。我们应该设置一个较短的读取超时时间,以防止被下游服务拖慢,通常不会设置超过 30 秒的读取超时。
对定时任务或异步任务来说,读取超时配置得长些问题不大
1.3 Feign 和 Ribbon 配合使用,你知道怎么配置超时吗? 为Feign 配置超时参数比较复杂,为 Feign 配置超时参数的复杂之处在于,Feign 自己有两个超时参数,它使用的负载均衡组件 Ribbon 本身还有相关配置。
结论: 结论一:Feign和Ribbon都不设置,默认情况下取Ribbon的读取超时 1 秒,如此短的读取超时算是坑点一。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 public class RibbonClientConfiguration { public static final int DEFAULT_CONNECT_TIMEOUT = 1000 ; 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 的读取超时,就必须同时配置连接超时,才能生效。(==我看源码的时候已经修复了==) 1 2 3 4 if (config.getConnectTimeout() != null && config.getReadTimeout() != null) { builder.options(new Request.Options(config.getConnectTimeout(), config.getReadTimeout())); }
==Feign创建Request时,如果获取连接超时时间或者读取超时时间未配置会取默认值。==
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 class FeignClientFactoryBean implements FactoryBean <Object>, InitializingBean, ApplicationContextAware { private int readTimeoutMillis = new Request .Options().readTimeoutMillis(); private int connectTimeoutMillis = new Request .Options().connectTimeoutMillis(); protected void configureUsingProperties (FeignClientProperties.FeignClientConfiguration config, Feign.Builder builder) { connectTimeoutMillis = config.getConnectTimeout() != null ? config.getConnectTimeout() : connectTimeoutMillis; readTimeoutMillis = config.getReadTimeout() != null ? config.getReadTimeout() : readTimeoutMillis; builder.options(new Request .Options(connectTimeoutMillis, TimeUnit.MILLISECONDS, readTimeoutMillis, TimeUnit.MILLISECONDS, true )); } }
结论三:单独的超时可以覆盖全局超时,这符合预期。 对单独的 Feign Client 设置超时时间,可以把 default 替换为 Client 的 name:
1 2 3 4 feign.client.config.default.readTimeout=3000 feign.client.config.default.connectTimeout=3000 feign.client.config.clientsdk.readTimeout=2000 feign.client.config.clientsdk.connectTimeout=2000
结论四:除了可以配置 Feign,也可以配置 Ribbon 组件的参数来修改两个超时时间。 ==注意==:首字母需大写
1 2 ribbon.ReadTimeout=4000 ribbon.ConnectTimeout=4000
结论五:同时配置 Feign 和 Ribbon 的超时,以 Feign 为准。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 feign.client.config.default .readTimeout=3000 feign.client.config.default .connectTimeout=3000 ribbon.ReadTimeout=4000 ribbon.ConnectTimeout=4000 执行耗时:3006ms 错误:Read timed out executing POST http: plaintext public class LoadBalancerFeignClient implements Client { 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; } }
2. 重试 2.1 Ribbon 会自动重试请求 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 public static final int DEFAULT_MAX_AUTO_RETRIES_NEXT_SERVER = 1 ;public static final int DEFAULT_MAX_AUTO_RETRIES = 0 ;public static final Boolean DEFAULT_OK_TO_RETRY_ON_ALL_OPERATIONS = Boolean.FALSE; 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) { return nextServerCount <= lbContext.getRetryHandler().getMaxRetriesOnNextServer() && canRetry(context); }
3. 并发 defaultMaxPerRoute=2,也就是同一个主机 / 域名的最大并发请求数为 2
4. 项目