目录
背景
管理后台上有个批量退款的功能,每次操作退两百笔退款订单,大概会执行4分钟的时间。发现相同的参数和请求路径,每隔两分钟进行重试,总共重试了四次,不像是人为的原因。
分析
1、谷歌浏览器超过2分钟没有收到服务方的请求,就会进行断开连接。这时就猜测考虑是不是业务处理逻辑太久了,导致浏览器长时间没有接受到后台的返回结果,自动重复提交了记录。
3、之前是在管理项目上出现的问题,利用arthas注入程序执行时睡眠10分钟的代码,复现了。
重新发起请求原因
If an HTTP/1.1 client sends a request which includes a request body, but which does not
include an Expect request-header field with the "100-continue" expectation, and if the
client is not directly connected to an HTTP/1.1 origin server, and if the client sees the
connection close before receiving any status from the server, the client SHOULD retry the request.
大致意思就是说,如果发送一个请求到服务器端,该请求有请求体,但是请求头里面不包含“ 100-continue ”这种东西,并且客户端没有直接连接到原始的 HTTP/1.1 服务器,此时,如果客户端在接收到服务器发送的 HTTP 状态之前发现服务器主动关掉连接,那么客户端应该重试请求。
那看起来好像就是服务器端主动关掉了连接,导致浏览器重新发送请求了。
真正的原因
网上案例
如果服务利用nginx做反向代理, nginx配置有超时控制,超过60s时,就中断了请求,然后谷歌浏览器超过两分钟就重新发起了请求了。
send_timeout的默认值为60s, 配置区域:http server location。
说明:设置响应传输到客户端的超时时间。仅在两个连续的写操作之间设置超时,而不是为整个
响应的传输。如果客户端在此时间内未收到任何内容,则会关闭连接。
可以查看nginx配置的超时时间,判断是否有如下配置:
server {
send_timeout 60s; // 服务端向客户端传输数据的超时时间
}
这样看就完全满足上述重发请求的条件了:
- 请求头里面不包含“ 100-continue ”
- 客户端没有连接到原始HTTP服务器(Nginx做了反向代理)
- Nginx超过60s主动关掉了连接
管理项目案例分析
前端Ajax超时配置分析
浏览器通过Ajax访问时的网络连接/读/写超时时间。
默认情况下未配置,即未启用超时。指定超时值以毫秒为单位设置请求超时,这将覆盖全局设置。
它应该基于每个浏览器是否有XMLHttpRequest对象的超时处理。Timeout unsigned long请求在自动终止之前可以花费的毫秒数。值为0(这是默认值)表示没有超时。
官网说明
timeout
Type: Number
Set a timeout (in milliseconds) for the request. This will override any global timeout set
with $.ajaxSetup(). The timeout period starts at the point the$.ajaxcall is made; if
several other requests are in progress and the browser has no connections available, it is
possible for a request to time out before it can be sent. In jQuery 1.4.x and below, the
XMLHttpRequest object will be in an invalid state if the request times out; accessing any
object members may throw an exception. In Firefox 3.0+ only, script and JSONP requests
cannot be cancelled by a timeout; the script will run even if it arrives after the timeout period.
浏览器的默认超时时间配置分析
Chrome 浏览器的默认值是5分钟,如下图:
相关浏览器代码地址如下:https://source.chromium.org/chromium/chromium/src/+/main:net/socket/client_socket_pool.cc;l=25
管理服务的nginx超时配置分析
nginx没有配置proxy_connect_timeout、proxy_read_timeout、proxy_send_timeout相关的参数
网络连接/读/写超时设置相关说明如下:
proxy_connect_timeout time:与后端/上游服务器建立连接的超时时间,默认为60s,此时间不超过75s。
proxy_read_timeout time:设置从后端/上游服务器读取响应的超时时间,默认为60s,此超时时间指的是两次成功读操作间隔时间,而不是读取整个响应体的超时时间。如果在此超时时间内上,游服务器没有发送任何响应,则Nginx关闭此连接。
proxy_send_timeout time:设置往后端/上游服务器发送请求的超时时间,默认为60s,此超时时间指的是两次成功写操作间隔时间,而不是发送整个请求的超时时间。如果在此超时时间内上,游服务器没有接收任何响应,则Nginx关闭此连接。
进行测试
让sre帮忙将管理平台的nginx配置改成600秒,如下:
proxy_connect_timeout 600;
proxy_read_timeout 600;
proxy_send_timeout 600;
测试结果如下:
没改nginx配置前 | 配置nginx配置 | |
服务侧代码改成为10分钟后返回结果 | 每两分钟重试一次 浏览器两分钟后显示请求结果为504状态 | 日志显示 1、每十分钟返回处理结果 2、每六分钟重试一次,重试多次 浏览器显示无法访问此网站 |
服务侧代码改成为5分钟后返回结果 | 每两分钟重试一次 浏览器两分钟后显示请求结果为504状态 | 日志显示 1、每五分钟返回处理结果 2、每六分钟重试一次,只重试两次 浏览器显示无法访问此网站 |
服务侧代码改成为4分钟后返回结果 | 每两分钟重试一次 浏览器两分钟后显示请求结果为504状态 | 日志显示 1、四分钟返回处理结果 2、服务端不再有重试的日志 浏览器显示成功接收到请求结果 |
总结
浏览器发起请求,当服务端的响应时间需要很久时,需要考虑好请求链接各环节的超时配置。
本次请求问题,谷歌浏览器默认超时配置是5分钟,管理平台的nginx超时配置有问题,导致nginx提前主动关闭了链接,进而浏览器主动发起了重试请求。
本次修改方案:因为退款200笔订单耗时在4分钟左右,所以本次只改了管理项目的nginx配置,如下:
server {
...
proxy_connect_timeout 600;
proxy_read_timeout 600;
proxy_send_timeout 600;
}
浏览器超时5分钟,为什么重试是2分钟自动重试?
猜测:nginx中包含多种不同的IO处理函数。假设nginx事件处理使用的epoll模型,为事件设置了一个超时定时器,从而能够处理事件超时的情况,一般都会循环调用epoll_wait监听所有fd,处理发生的读写事件。在第一次读取时没有超时,然后执行第二次读取时超时了,就标记该事件过期了,然后nginx就主动调用了断开连接。这样两次执行的耗时也刚好是两分钟。
注意:依旧要小心响应时间超过5分钟的请求,因为谷歌浏览器默认超时配置是5分钟,服务端的响应时间超过5分钟时,浏览器会发起重试。