这个问题是其他同事反映过来的,应该说比较罕见,需要同时满足三种条件才能发生。为了保持神秘,原因暂时不提,不过背景得交待一下。该案例的大概架构就是部署两个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设置得比较大