前言
nginx用的时候很开心,出现问题时却很要命。这次出现的问题虽然能有很多方式去解决,但我仍旧希望能够用一个问题将知识进行串联,深挖其根因。
问题现象
浏览器上传文件,文件大小是四百多M,等待一段时间后,上传失败。显示报错如下图:
问题排查
信息收集
遇到这种问题上面出现了两个错误,一个是网络错误,一个返回的是413。413这个错误信息提示很明显,Request Entity Too Large。凭多年老司机经验,查看nginx配置,发现配置的client_max_body_size为300m。将其配置改为client_max_body_size为2000m。再次上传文件,测试。还是报错,但是这次报错只有net::ERROR_CONNECTION_ABORTED。
再次凭借老司机的经验,怀疑文件过大,引起的超时,至于在哪里超时,我们要看看这个具体情况。我通过复现,再配合整个链路日志排查。我整体梳理出了的信息情况如下图。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-h6xl82Hv-1626016425573)(https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/a1433d347c734357a2c0585e242847dc~tplv-k3u1fbpfcp-watermark.image)]
可是实际现象明明是net::ERROR_CONNECTION_ABORTED,复现却返回的是504。这到底是怎么回事?
问题分析
从整个日志请求的返回来看,slb日志显示504,而nginx的日志显示返回码是499。基本上可以断定情况是nginx侧的响应时间过长,slb主动断开了连接。
504:Gateway Timeout 网关超时
499:Client Closed Request 客户端主动断开连接
由于SLB不是我们管理,需要向云厂商确认proxy_read_timeout的情况,经过确认,其配置为60s。这样与我们的nginx的499,请求时间是60s的现象符合。
请求超时时间 (proxy_read_timeout)
用户可配置,默认值60秒。这个timeout针对后端,即SLB和RS之间的HTTP请求和响应。这个时间是两次从后端收到数据的时间差,不是整个的接收时间。如下两种都是超时的情况:
• SLB将HTTP请求转发给后端,后端60秒没有回复任何HTTP响应。
• SLB将HTTP请求转发给后端,后端回复HTTP响应的部分数据,随后的60秒都没有收到HTTP响应剩下的部分。
可以理解成后端在回复HTTP响应完成前,不能idle超过60秒。
在超时时间内后端服务器一直没有响应,负载均衡做为代理将放弃等待,给客户端返回HTTP 504错误码。
示例配置如下:
proxy_read_timeout 60;
总结下,问题发生是由于slb的proxy_read_timeout为60s,而上传大文件到SLB,SLB转发到后端nginx的过程中,超过60s没有回复任何HTTP响应,SLB就主动断开了连接。文件最终上传失败。
而出现net::ERROR_CONNECTION_ABORTED,说明在上传文件的过程中,当http服务器没有读全客户端的http请求体时就会中断链接,导致这种错误的主要原因是上传的文件太大,服务器不能继续读取请求而过早中断链接。这里初步怀疑是浏览器的版本会导致http请求超时时间不同。用户发生了net::ERROR_CONNECTION_ABORTED是由于其文件还未上传完成就发生了请求超时。
这里还是有个问题,为什么slb这里的请求时间是将近500s呢?
基于这个案例我们继续进行分析,我们需要弄清楚转发请求是怎么实现的。弄清楚之前,我们可以先了解下nginx是怎么处理一个请求的。
nginx如何处理请求
http的请求处理分为如下阶段
- 初始化HTTP Request(读取来自客户端的数据,生成HTTP Request对象,该对象含有该请求所有的信息)
- 处理请求头
- 处理请求体
- 如果有的话,调用与此请求(URL或者Location)关联的handler
- 依次调用个phase handler进行处理
具体过程
而在 phase handler通常将经过如下几个阶段:
NGX_HTTP_POST_READ_PHASE: 读取请求内容阶段
NGX_HTTP_SERVER_REWRITE_PHASE: Server请求地址重写阶段
NGX_HTTP_FIND_CONFIG_PHASE: 配置查找阶段
NGX_HTTP_REWRITE_PHASE: Location请求地址重写阶段
NGX_HTTP_POST_REWRITE_PHASE: 请求地址重写提交阶段
NGX_HTTP_PREACCESS_PHASE: 访问权限检查准备阶段
NGX_HTTP_ACCESS_PHASE: 访问权限检查阶段
NGX_HTTP_POST_ACCESS_PHASE: 访问权限检查提交阶段
NGX_HTTP_TRY_FILES_PHASE: 配置项 try_files 处理阶段
NGX_HTTP_CONTENT_PHASE: 内容产生阶段
NGX_HTTP_LOG_PHASE: 日志模块处理阶段
划重点NGX_HTTP_CONTENT_PHASE及内容产生阶段request将交给一个合适的content handler去处理,而这里我们使用的就是proxy_pass
处理请求体转发过程
nginx自身处理请求体
经过上面的整个请求的阶段划分,我们再重点分析下文件上传的时候,nginx是如何转发这个请求的。这里重点看的是下proxy_pass的handler,这个处于处理请求体的阶段,会调用的ngx_http_read_client_request_body()函数,来对请求体进行读取。**值得注意的是这些模块会把客户端的请求体完整的读取后才开始往后端转发数据。**这里也解释了为什么slb的请求时间是将近500s,而nginx的请求时间才60s。由于文件先要上传到slb上,再将数据进行转发,在这个过程中发生了proxy_read_timeout的情况。另外补充一个知识点:
由于内存的限制,ngx_http_read_client_request_body()接口读取的请求体会部分或者全部写入一个临时文件中,根据请求体的大小以及相关的指令配置,请求体可能完整放置在一块连续内存中,也可能分别放置在两块不同内存中,还可能全部存在一个临时文件中,最后还可能一部分在内存,剩余部分在临时文件中。
ngx_http_proxy_handler的源码情况,其中会使用ngx_http_read_client_request_body,ngx_http_upstream_init作为回调函数,当读取完请求体后,在执行ngx_http_upstream_init()进行请求的转发。
static ngx_int_t
ngx_http_proxy_handler(ngx_http_request_t *r)
{
...
rc = ngx_http_read_client_request_body(r, ngx_http_upstream_init);
if (rc >= NGX_HTTP_SPECIAL_RESPONSE) {
return rc;
}
return NGX_DONE;
}
upstream处理请求体
当使用upstream模块时,我们可以了解下upstream的机制是怎样的,从而弄清楚整个请求链路上的状态码是如何形成的。
-
建立upstream,ngx_http_upstream_init
-
删除超时定时器
-
建立到上游请求
-
挂接一些处理函数,包含第6步中要用到的请求结束后的upstream的清理函数
-
-
创建上游的链接,ngx_http_upstream_connect
- 建立socket、connetion,发起tcp建连请求,使用epoll发送请求,挂接upstream的handler,包括第4、5步中处理上游应答的处理函数
-
发送到上游的请求,ngx_http_upstream_send_request
-
处理上游的响应头,process_header 解析请求头,ngx_http_upstream_process_headers处理请求头
-
处理上游的响应体,ngx_http_upstream_process_body_in_memory
-
结束upstream 请求,ngx_http_upstream_cleanup
upstream请求处理请求阶段中,SLB在upstream模块中会设置超时定时器,超过60s,发起了断开连接,SLB的断开连接,同时会到时nginx也断开连接。这样就解释了各个状态码的情况了。
另外在这个阶段,nginx会默认定义三个超时时间:
- proxy_connect_timeout,默认75s,该指令设置与upstream server的连接超时时间。
- proxy_send_timeout,默认60s,这个指定设置了发送请求给upstream服务器的超时时间。超时设置不是为了整个发送期间,而是在两次write操作期间。如果超时后,upstream没有收到新的数据,nginx会关闭连接
- proxy_read_timeout,默认60s,该指令设置与代理服务器的读超时时间。它决定了nginx会等待多长时间来获得请求的响应。这个时间不是获得整个response的时间,而是两次reading操作的时间
解决方式
根据当个问题来看,我提供了如下几个思路,大家可以根据自身情况来选择。
- 压缩文件再上传或者提高本地带宽,争取在60s内完成上传
- 配置slb,将proxy_read_timeout的时间增大(一般无法更改)
- 使用分片上传的方式,这里需要前后端进行代码改造。参考:https://www.cnblogs.com/tugenhua0707/p/11246860.html
问题总结
经过上述分析我们明白了如下几点:
- client_max_body_size可以配置nginx接受请求体的大小。
- 504和499的错误描述。
- proxy_read_timeout可以配置读取后端服务超时时间。
- nginx的请求是由多个阶段组成的,实际处理请求体是在NGX_HTTP_CONTENT_PHASE发生。
- slb的请求时间和nginx的请求时间的不一致是由于slb是将请求体获取完成后,再向后端服务进行转发。
结束语
文章中必然会有一些不严谨的地方,还希望大家包涵,大家吸取精华(如果有的话),去其糟粕。如果大家感兴趣可以关我的公众号:gungunxi。我的微信号:lcomedy2021
参考文档
接受包的原理:https://blog.csdn.net/ApeLife/article/details/73866197
配置参数详解:https://www.cnblogs.com/lemon-flm/p/8352194.html
nginx的实现原理:https://cloud.tencent.com/developer/article/1447290
https://tengine.taobao.org/book/chapter_12.html
源码分析:https://www.kancloud.cn/digest/understandingnginx/202605