HTTP调用:你考虑到超时、重试、并发了吗?
进行 HTTP 调用本质上是通过 HTTP 协议进行一次网络请求,需要考虑以下三点
- 框架设置的默认超时是否合理;
- 考虑到网络的不稳定,超时后的请求重试是一个不错的选择,但需要考虑服务端接口的幂等性设计是否允许我们重试;
- 需要考虑框架是否会像浏览器那样限制并发连接数,以免在服务并发很大的情况下,HTTP 调用的并发数限制成为瓶颈。
配置连接超时和读取超时参数的学问
连接超时参数 ConnectTimeout,让用户配置建连阶段的最长等待时间;
读取超时参数 ReadTimeout,用来控制从 Socket 上读取数据的最长等待时间。
连接超时参数和连接超时的误区有这么两个:
连接超时配置得特别长,比如 60 秒。三次握手这些是毫秒级的,如果几秒连接不上,那么基本就连接不上了,可以看看是不是防火墙的问题。
排查连接超时问题,却没理清连的是哪里。通常我们的服务会有多个节点,如果别的客户端通过客户端负载均衡技术来连接服务端,那么客户端和服务端会直接建立连接,此时出现连接超时大概率是服务端的问题;而如果服务端通过类似 Nginx 的反向代理来负载均衡,客户端连接的其实是 Nginx,而不是服务端,此时出现连接超时应该排查 Nginx。
读取超时参数和读取超时误区:
第一个误区:认为出现了读取超时,服务端的执行就会中断。类似 Tomcat 的 Web 服务器都是把服务端请求提交到线程池处理的,只要服务端收到了请求,网络层面的超时和断开便不会影响服务端的执行
第二个误区:认为读取超时只是 Socket 网络层面的概念,是数据传输的最长耗时,故将其配置得非常短。比如 100 毫秒。
其实,发生了读取超时,网络层面无法区分是服务端没有把数据返回给客户端,还是数据在网络上耗时较久或丢包。
读取超时是服务处理超时,指的是,向 Socket 写入数据后,我们等到 Socket 返回数据的超时时间,其中包含的时间或者说绝大部分的时间,是服务端处理业务逻辑的时间。
第三个误区:认为超时时间越长任务接口成功率就越高,将读取超时参数配置得太长。
同步调用。如果超时时间很长,在等待服务端返回数据的同时,客户端线程(通常是 Tomcat 线程)也在等待,当下游服务出现大量超时的时候,程序可能也会受到拖累创建大量线程,最终崩溃。面向用户响应的请求或是微服务短平快的同步接口调用,并发量一般较大,我们应该设置一个较短的读取超时时间,以防止被下游服务拖慢,通常不会设置超过 30 秒的读取超时。
Feign 和 Ribbon 配合使用,你知道怎么配置超时吗?
- 默认情况下 Feign 的读取超时是 1 秒,如此短的读取超时算是坑点一。
如果要修改 Feign 客户端默认的两个全局超时时间,你可以设置feign.client.config.default.readTimeout 和feign.client.config.default.connectTimeout 参数:
feign.client.config.default.readTimeout=3000
feign.client.config.default.connectTimeout=3000
- 如果要配置 Feign 的读取超时,就必须同时配置连接超时,才能生效。
- 单独的超时可以覆盖全局超时
- 除了可以配置 Feign,也可以配置 Ribbon 组件的参数来修改两个超时时间。这里的坑点三是,参数首字母要大写,和 Feign 的配置不同
ribbon.ReadTimeout=4000
ribbon.ConnectTimeout=4000
- 同时配置 Feign 和 Ribbon 的超时,以 Feign 为准
Ribbon 会自动重试请求
翻看 Ribbon 的源码可以发现,MaxAutoRetriesNextServer 参数默认为 1,也就是 Get请求在某个服务端节点出现问题(比如读取超时)时,Ribbon 会自动重试一次
解决方法:
- 把发短信接口从 Get 改为 Post。这里的一个误区是,Get 请求的参数包含在 Url QueryString中,会受浏览器长度限制,所以一些同学会选择使用 JSON 以 Post 提交大参数,使用Get 提交小参数。
- 将 MaxAutoRetriesNextServer 参数配置为 0,禁用服务调用失败后在下一个服务端节点的自动重试。在配置文件中添加一行即可:ribbon.MaxAutoRetriesNextServer=0
并发限制了爬虫的抓取能力
案例:爬虫的效率很低。按理还说,爬一次需要1秒,那么我并发10个线程爬,也是耗时1秒。
查看 PoolingHttpClientConnectionManager 源码,可以注意到有两个重要参数:
defaultMaxPerRoute=2,也就是同一个主机 / 域名的最大并发请求数为 2。我们的爬虫需要 10 个并发,显然是默认值太小限制了爬虫的效率。
maxTotal=20,也就是所有主机整体最大并发为 20,这也是 HttpClient 整体的并发度。举一个例子,使用同一
个 HttpClient 访问 10 个域名,defaultMaxPerRoute 设置为 10,为确保每一个域名都能达到 10 并发,需要把 maxTotal 设置为 100。
声明一个新的 HttpClient 放开相关限制,设置maxPerRoute 为 50、maxTotal 为 100。
httpClient2 = HttpClients.custom().setMaxConnPerRoute(10).setMaxConnTotal(20).build();
为什么很少见到写入超时,客户端发送数据到服务端,首先接力连接(TCP),然后写入TCP缓冲区,TCP缓冲区根据时间窗口,发送数据到服务端,因此写入操作可以任务是自己本地的操作,本地操作是不需要什么超时时间的,如果真的有什么异常,那也是连接(TCP)不上,或者超时的问题,连接超时和读取超时就能覆盖这种场景
Spring Boot 带来了【约定大于配置】的说法,但是,本文告诉我们,越是约定大于配置,越是要对那些“默认配置”心里有数才行。