您现在的位置是:首页 >技术教程 >04 | HTTP调用:你考虑到超时、重试、并发了吗?网站首页技术教程

04 | HTTP调用:你考虑到超时、重试、并发了吗?

流水_0 2025-03-16 00:01:02
简介04 | HTTP调用:你考虑到超时、重试、并发了吗?

与执行本地方法不同,进行 HTTP 调用本质上是通过 HTTP 协议进行一次网络请求。网络请求必然有超时的可能性,因此我们必须考虑到这三点:

  • 首先,框架设置的默认超时是否合理;
  • 其次,考虑到网络的不稳定,超时后的请求重试是一个不错的选择,但需要考虑服务端接口的幂等性设计是否允许我们重试;
  • 最后,需要考虑框架是否会像浏览器那样限制并发连接数,以免在服务并发很大的情况下,HTTP 调用的并发数限制成为瓶颈。

配置连接超时和读取超时参数的学问

对于 HTTP 调用,虽然应用层走的是 HTTP 协议,但网络层面始终是 TCP/IP 协议。TCP/IP 是面向连接的协议,在传输数据之前需要建立连接。几乎所有的网络框架都会提供这么两个超时参数:

  • 连接超时参数 ConnectTimeout,让用户配置建连阶段的最长等待时间;
  • 读取超时参数 ReadTimeout,用来控制从 Socket 上读取数据的最长等待时间。

这两个参数看似是网络层偏底层的配置参数,不足以引起开发同学的重视。但,正确理解和配置这两个参数,对业务应用特别重要,毕竟超时不是单方面的事情,需要客户端和服务端对超时有一致的估计,协同配合方能平衡吞吐量和错误率。

连接超时参数和连接超时的误区有这么两个:

  • 连接超时配置得特别长,比如 60 秒。一般来说,TCP 三次握手建立连接需要的时间非常短,通常在毫秒级最多到秒级,不可能需要十几秒甚至几十秒。如果很久都无法建连,很可能是网络或防火墙配置的问题。这种情况下,如果几秒连接不上,那么可能永远也连接不上。因此,设置特别长的连接超时意义不大,将其配置得短一些(比如 1~5 秒)即可。如果是纯内网调用的话,这个参数可以设置得更短,在下游服务离线无法连接的时候,可以快速失败。
  • 排查连接超时问题,却没理清连的是哪里。通常情况下,我们的服务会有多个节点,如果别的客户端通过客户端负载均衡技术来连接服务端,那么客户端和服务端会直接建立连接,此时出现连接超时大概率是服务端的问题;而如果服务端通过类似 Nginx 的反向代理来负载均衡,客户端连接的其实是 Nginx,而不是服务端,此时出现连接超时应该排查 Nginx。

读取超时参数和读取超时则会有更多的误区,我将其归纳为如下三个。

  • 第一个误区:认为出现了读取超时,服务端的执行就会中断。
  • 第二个误区:认为读取超时只是 Socket 网络层面的概念,是数据传输的最长耗时,故将其配置得非常短,比如 100 毫秒。其实,发生了读取超时,网络层面无法区分是服务端没有把数据返回给客户端,还是数据在网络上耗时较久或丢包。但,因为 TCP 是先建立连接后传输数据,对于网络情况不是特别糟糕的服务调用,通常可以认为出现连接超时是网络问题或服务不在线,而出现读取超时是服务处理超时。确切地说,读取超时指的是,向 Socket 写入数据后,我们等到 Socket 返回数据的超时时间,其中包含的时间或者说绝大部分的时间,是服务端处理业务逻辑的时间。
  • 第三个误区:认为超时时间越长任务接口成功率就越高,将读取超时参数配置得太长。进行 HTTP 请求一般是需要获得结果的,属于同步调用。如果超时时间很长,在等待服务端返回数据的同时,客户端线程(通常是 Tomcat 线程)也在等待,当下游服务出现大量超时的时候,程序可能也会受到拖累创建大量线程,最终崩溃。对定时任务或异步任务来说,读取超时配置得长些问题不大。但面向用户响应的请求或是微服务短平快的同步接口调用,并发量一般较大,我们应该设置一个较短的读取超时时间,以防止被下游服务拖慢,通常不会设置超过 30 秒的读取超时。你可能会说,如果把读取超时设置为 2 秒,服务端接口需要 3 秒,岂不是永远都拿不到执行结果了?的确是这样,因此设置读取超时一定要根据实际情况,过长可能会让下游抖动影响到自己,过短又可能影响成功率。甚至,有些时候我们还要根据下游服务的 SLA,为不同的服务端接口设置不同的客户端读取超时。

Feign 和 Ribbon 配合使用,你知道怎么配置超时吗?

  • 结论一,默认情况下 Feign 的读取超时是 1 秒,如此短的读取超时算是坑点一。
  • 结论二,也是坑点二,如果要配置 Feign 的读取超时,就必须同时配置连接超时,才能生效。
  • 结论三,单独的超时可以覆盖全局超时,这符合预期,不算坑:
  • 结论四,除了可以配置 Feign,也可以配置 Ribbon 组件的参数来修改两个超时时间。这里的坑点三是,参数首字母要大写,和 Feign 的配置不同。
  • 结论五,同时配置 Feign 和 Ribbon 的超时,以 Feign 为准。

Ribbon重试请求

  • Ribbon 会默认自动重试一次。
  • 对于重试,因为 HTTP 协议认为 Get 请求是数据查询操作,是无状态的,又考虑到网络出现丢包是比较常见的事情,有些 HTTP 客户端或代理服务器会自动重试 Get/Head 请求。如果你的接口设计不支持幂等,需要关闭自动重试。但,更好的解决方案是,遵从 HTTP 协议的建议来使用合适的 HTTP 方法。

并发限制:

  • 除了超时和重试的坑,进行 HTTP 请求调用还有一个常见的问题是,并发数的限制导致程序的处理能力上不去。
  • 包括 HttpClient 在内的 HTTP 客户端以及浏览器,都会限制客户端调用的最大并发数。如果你的客户端有比较大的请求调用并发,比如做爬虫,或是扮演类似代理的角色,又或者是程序本身并发较高,如此小的默认值很容易成为吞吐量的瓶颈,需要及时调整。

思考与讨论

我们强调了要注意连接超时和读取超时参数的配置,大多数的 HTTP 客户端也都有这两个参数。有读就有写,但为什么我们很少看到“写入超时”的概念呢?

  • 客户端发送数据到服务端,首先建立连接(TCP),然后写入TCP缓冲区,TCP缓冲区根据时间窗口,发送数据到服务端,因此写入操作可以任务是自己本地的操作,本地操作是不需要什么超时时间的,如果真的有什么异常,那也是连接(TCP)不上,或者超时的问题,连接超时和读取超时就能覆盖这种场景。

除了 Ribbon 的 AutoRetriesNextServer 重试机制,Nginx 也有类似的重试功能。你了解 Nginx 相关的配置吗?

  • proxy_next_upstream:
  • 语法: proxy_next_upstream [error|timeout|invalid_header|http_500|http_503|http_404|off]
  • 默认值: proxy_next_upstream error timeout 即 error timeout会自动重试
  • 可以修改默认值,在去掉error和timeout,这样在发生错误和超时时,不会重试 proxy_next_upstream_tries 这个参数决定重试的次数,0表示关闭该参数
风语者!平时喜欢研究各种技术,目前在从事后端开发工作,热爱生活、热爱工作。