HTTP 超时处理
1 超时,网络终究是靠不住的
HTTP 调用既然是网络请求,就可能超时,超时错误分两种,connect timeout 和 read timeout,前者可能是网络问题,或者服务端连接池不够用了。后者是连接已经建立了,但是服务端太忙了,不能及时处理完你的请求。
因此在实际开发中都需要注意考虑这些超时的处理措施。
框架设置的默认超时时间是否合理?
过短,请求还未处理完成,你有些急不可耐了呀!
过长请求早已超出正常响应时间而挂了!
网络不稳定性,超时后可以通过定时任务请求重试这时,就要注意考虑服务端接口幂等性设计,即是否允许重试?
框架是否会像浏览器那样限制并发连接数,以免在高并发下,HTTP 调用的并发数成为瓶颈!
1.1 HTTP 调用框架技术选型
Spring Cloud 全家桶或 Dubbo 使用 Feign 进行声明式服务调用或 Dubbo 自己的一套服务调用流程。
只使用 Spring BootHTTP 客户端,如 Apache HttpClient
1.2 连接超时配置 && 读取超时参数
虽然应用层是 HTTP 协议,但网络层始终是 TCP/IP 协议。TCP/IP 是面向连接的协议,在传输数据之前需先建立连接。网络框架都会提供如下超时相关参数:
连接超时参数 ConnectTimeout 可自定义配置的建立连接最长等待时间
读取超时参数 ReadTimeout 控制从 Socket 上读取数据的最长等待时间。
1.3 常见的写 bug 姿势
连接超时配置过长
比如 60s。TCP 三次握手正常建立连接所需时间很短,在 ms 级最多到 s 级,不可能需要十几、几十秒,多半是网络或防火墙配置问题。这时如果几秒还连不上,那么可能永远也连不上。所以设置特别长的连接超时无意义,1~5 秒即可。如果是纯内网调用,还可以设更短,在下游服务无法连接时,快速失败
无脑排查连接超时问题
服务一般会有多个节点,若别的客户端通过负载均衡连接服务端,那么客户端和服务端会直接建立连接,此时出现连接超时大概率是服务端问题而若服务端通过 Nginx 反向代理来负载均衡,客户端连接的其实是 Nginx,而非服务端,此时出现连接超时应排查 Nginx
读取超时参数和读取超时“坑点”
只要读取超时,服务端程序的正常执行就一定中断了?
案例
client 接口内部通过HttpClient
调用服务端接口 server,客户端读取超时 2 秒,服务端接口执行耗时 5 秒。
调用 client 接口后,查看日志:
客户端 2s 后出现
SocketTimeoutException
,即读取超时服务端却泰然地在 3s 后执行完成
Tomcat Web 服务器是把服务端请求提交到线程池处理,只要服务端收到请求,网络层面的超时和断开便不会影响服务端的执行。因此,出现读取超时不能随意假设服务端的处理情况,需要根据业务状态考虑如何进行后续处理。
读取超时只是 Socket 网络层面概念,是数据传输的最长耗时,故将其配置很短
比如 100ms。
发生读取超时,网络层面无法区分如下原因:
服务端没有把数据返回给客户端
数据在网络上耗时较久或丢包
但 TCP 是连接建立完成后才传输数据,对于网络情况不是特差的服务调用,可认为:
连接超时网络问题或服务不在线
读取超时服务处理超时。读取超时意味着向 Socket 写入数据后,我们等到 Socket 返回数据的超时时间,其中包含的时间或者说绝大部分时间,是服务端处理业务逻辑的时间
超时时间越长,任务接口成功率越高,便将读取超时参数配置过长
HTTP 请求一般需要获得结果,属同步调用。若超时时间很长,在等待 Server 返回数据同时,Client 线程(通常为 Tomcat 线程)也在等待,当下游服务出现大量超时,程序可能也会受到拖累创建大量线程,最终崩溃。
对定时任务或异步任务,读取超时配置较长问题不大
但面向用户响应的请求或是微服务平台的同步接口调用,并发量一般较大,应该设置一个较短的读取超时时间,以防止被下游服务拖慢,通常不会设置读取超时超过 30s。
评论可能会有人问了,若把读取超时设为 2s,而服务端接口需 3s,不就永远拿不到执行结果?的确,因此设置读取超时要结合实际情况:
过长可能会让下游抖动影响到自己
过短又可能影响成功率。甚至,有些时候我们还要根据下游服务的 SLA,为不同的服务端接口设置不同的客户端读取超时。
1.4 最佳实践
连接超时代表建立 TCP 连接的时间,读取超时代表了等待远端返回数据的时间,也包括远端程序处理的时间。在解决连接超时问题时,我们要搞清楚连的是谁;在遇到读取超时问题的时候,我们要综合考虑下游服务的服务标准和自己的服务标准,设置合适的读取超时时间。此外,在使用诸如 Spring Cloud Feign 等框架时务必确认,连接和读取超时参数的配置是否正确生效。
2 Feign&&Ribbon
2.1 如何配置超时
为 Feign 配置超时参数的难点在于,Feign 自身有两个超时参数,它使用的负载均衡组件 Ribbon 本身还有相关配置。这些配置的优先级是啥呢?
2.2 案例
测试服务端超时,假设服务端接口,只休眠 10min
Feign 调用该接口:
通过 Feign Client 进行接口调用
在配置文件仅指定服务端地址的情况下:
得到如下输出:
Feign 默认读取超时是 1 秒,如此短的读取超时算是“坑”。
分析源码
自定义配置 Feign 客户端的两个全局超时时间
可以设置如下参数:
修改配置后重试,得到如下日志:
3 秒读取超时生效。注意:这里有一个大坑,如果希望只修改读取超时,可能会只配置这么一行:
测试会发现,这样配置无法生效。
要配置 Feign 读取超时,必须同时配置连接超时
查看FeignClientFactoryBean
源码
只有同时设置
ConnectTimeout
、ReadTimeout
,Request.Options 才会被覆盖
想针对单独的 Feign Client 设置超时时间,可以把 default 替换为 Client 的 name:
单独的超时可覆盖全局超时
除了可以配置 Feign,也可配置 Ribbon 组件的参数以修改两个超时时间
参数首字母要大写,和 Feign 的配置不同。
可以通过日志证明参数生效:
同时配置 Feign 和 Ribbon 的参数
谁会生效?
最终生效的是 Feign 的超时:
同时配置 Feign 和 Ribbon 的超时,以 Feign 为准
在LoadBalancerFeignClient
源码如果Request.Options
不是默认值,就会创建一个FeignOptionsClientConfig
代替原来 Ribbon 的DefaultClientConfigImpl
,导致 Ribbon 的配置被 Feign 覆盖:
但若这么配置,最终生效的还是 Ribbon 的超时(4 秒),难点 Ribbon 又反覆盖了 Feign?不,这还是因为坑点二,单独配置 Feign 的读取超时无法生效:
版权声明: 本文为 InfoQ 作者【JavaEdge】的原创文章。
原文链接:【http://xie.infoq.cn/article/8d8a396f80df277a108a28f77】。
本文遵守【CC-BY 4.0】协议,转载请保留原文出处及本版权声明。
评论