nginx处理firefox的POST请求可能遇到的问题:小文件可以提交,大文件不能提交

发布于 2015-02-04  133 次阅读


转自 http://blog.csdn.net/yui/article/details/6457922

这个问题是其他同事反映过来的,应该说比较罕见,需要同时满足三种条件才能发生。为了保持神秘,原因暂时不提,不过背景得交待一下。该案例的大概架构就是部署两个nginx服务器,nginx1作为普通的web server,nginx2作为反向代理部署在nginx1的后端。出于测试目的,取消了临时文件所在目录client_body_temp的访问权限,此为条件一 

关于client_body_temp目录的作用,简单说就是如果客户端POST一个比较大的文件,长度超过了nginx缓冲区的大小,需要把这个文件的部分或者全部内容暂存到client_body_temp目录下的临时文件。

引起我们注意的是nginx的一个配置指令,client_body_buffer_size,如果把它设置为比较大的数值,例如256k,那么,无论使用firefox还是IE浏览器,来提交任意小于256k的图片,都很正常。如果注释该指令,使用默认的client_body_buffer_size设置,也就是操作系统页面大小的两倍,8k或者16k(此为条件二 ),问题就出现了。

无论使用firefox4.0还是IE8.0,提交一个比较大,200k左右的图片,都返回500 Internal Server Error错误。这其实也没有问题,200k大于当前的client_body_buffer_size(8k或者16k),提交的内容需要写入临时文件,前面取消了目录访问权限导致出错。

但是,如果提交一个比较小,30k左右的图片,firefox和IE的返回结果却有所不同。IE仍然返回500错误,这很好理解,30k仍然大于当前的client_body_buffer_size(8k或者16k),出错是正常的,不出错才奇怪。然而,firefox就是神奇地返回了经过resize server处理后的页面!

这太奇怪了,难道firefox发送的数据与IE发送的有所不同?使用tcpdump抓包发现,的确是有很大的不一样。

IE发送的数据包截图如下,建立连接时三次握手清晰可见,第10行是IE向nginx1发送http头消息,第24行是nginx1发送应答,然后再发送Content-Disposition和Content-Type两行header和body。Body与header的主干部分是分开不同的包发送的。

 

而firefox发送的数据包截图如下,三次握手不再赘述,第13行是firefox向nginx1发送http头消息,奇怪的是除了发送头消息,还附带了部分body,见第33行,第35行才是nginx1对该头消息的应答。也就是说,firefox把一部分body塞到header包里。

 

Firefox的这种行为使同事注意到nginx里设置的另一个缓冲区大小:client_header_buffer_size。原来此前他们设置了该值为128k,此为条件三 

综合以上现象,就有了初步推断:对于IE的请求,nginx把body只放到body的缓冲区处理,所以不受header的缓冲区大小的影响,而对于firefox的请求,nginx可能会把body放到header的缓冲区处理,所以,分别设置header、body缓冲区为128k、8k/16k的时候,POST 30k的图片能够成功而POST 200k的图片返回500错误,分别设置header、body缓冲区为128k、256k的时候,POST 200k的图片也能成功。

如何验证这个结论呢?究竟什么情况下body数据会放到header缓冲区处理呢?

“有问题,看日志”是一个好习惯。但是默认情况下,nginx记录的日志比较简单,不能满足要求,需要这样打开调试日志功能:

编译nginx,configure时使用--with-debug打开调试信息,然后make && make install
编辑nginx.conf,在server的error_log指令的文件名后面加上debug:
        error_log     logs/8085_error.log debug ;

使用firefox POST 30k左右的图片时,截取到的部分日志如下:
70 2011/05/26 09:53:28 [debug] 23622#0: *33 http client request body preread 2398
71 2011/05/26 09:53:28 [debug] 23622#0: *33 http read client request body
72 2011/05/26 09:53:28 [debug] 23622#0: *33 recv: fd:3 5840 of 28645
73 2011/05/26 09:53:28 [debug] 23622#0: *33 http client request body recv 5840
74 2011/05/26 09:53:28 [debug] 23622#0: *33 http client request body rest 22805

按这几行的关键字,可以搜索到对应的函数为src/http/ngx_http_request_body.c 的ngx_http_read_client_request_body 和ngx_http_do_read_client_request_body。前者会把预读的body(在源代码里,作者把被塞进http头消息包里的那部分body称为preread,即预读的body)暂存到header缓冲区,而只要这块header缓冲区足够大,足以容纳剩下的body的时候,会调用后者把它们也一起读进来。于是,上面30k左右的图片就能穿透过去,看上去不可思议的事情就这样发生了。

简单地分析一下相关源码。

在这里开始正式读取body。IE的请求,preread为零,而Firefox的请求,preread非零:

    110     preread = r->header_in->last - r->header_in->pos;
    111
    112     if (preread) {
    …
    166         rb->rest = r->headers_in.content_length_n - preread;
    167
    168         if (rb->rest <= (off_t) (b->end - b->last)) {
    169
    170             /* the whole request body may be placed in r->header_in */
    171
    172             rb->to_write = rb->bufs;
    173
    174             r->read_event_handler = ngx_http_read_client_request_body_handler;
    175
    176             return ngx_http_do_read_client_request_body(r);
    177         }

在168行有个条件判断,rb->rest是未读body的剩余长度,b->end – b->last就是空余的缓冲区大小。当header缓冲区不够大时,显然是不会跑到上面176行那里去的,而是会掉到下面,重新根据client_body_buffer_size的大小分配缓冲区处理。于是,200k那么大的图片就被挡住了。而30k左右的图片,在设置了比较大的client_header_buffer_size的时候是可以过去的。

IE的请求会跑到这里:

    181     } else {
    182         b = NULL;
    183         rb->rest = r->headers_in.content_length_n;
    184         next = &rb->bufs;
    185     }
    186

前面处理不完的都跑到这里,用了两次client_body_buffer_size,意思是剩余的内容不超过缓冲区大小的1.25倍,一次读完(1.25可能是经验值吧),否则,按缓冲区大小读取。

    187     size = clcf->client_body_buffer_size;
    188     size += size >> 2;
    189
    190     if (rb->rest < size) {
    191         size = (ssize_t) rb->rest;
    192
    193         if (r->request_body_in_single_buf) {
    194             size += preread;
    195         }
    196
    197     } else {
    198         size = clcf->client_body_buffer_size;
    199
    200         /* disable copying buffer for r->request_body_in_single_buf */
    201         b = NULL;
    202     }
    203
    204     rb->buf = ngx_create_temp_buf(r->pool, size);
    205     if (rb->buf == NULL) {
    206         return NGX_HTTP_INTERNAL_SERVER_ERROR;
    207     }
    …
    236     return ngx_http_do_read_client_request_body(r);

其实这样的处理流程也是无可厚非的,遇到body比较小,刚好header缓冲区又能够放得下,不用白不用,是不是?

最后,整理一下出现这个问题需要的条件。值得注意的是,目前各种版本的nginx都有这个现象(0.7.68、0.8.54、1.0.2都有) 
1) client_body_temp设置为不可访问,使得没有权限写临时文件
2) client_body_buffer_size使用默认设置,8k或者16k
3) client_header_buffer_size设置得比较大