nginx 报500 Internal Server Error

原文:https://blog.csdn.net/starbucks_star/article/details/78427033

 

 

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


nginx 报500 Internal Server Error_第1张图片 

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


nginx 报500 Internal Server Error_第2张图片 

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非零:
 

[cpp] view plain copy

  1. 110     preread = r->header_in->last - r->header_in->pos;  
  2. 111  
  3. 112     if (preread) {  
  4. …  
  5. 166         rb->rest = r->headers_in.content_length_n - preread;  
  6. 167  
  7. 168         if (rb->rest <= (off_t) (b->end - b->last)) {  
  8. 169  
  9. 170             /* the whole request body may be placed in r->header_in */  
  10. 171  
  11. 172             rb->to_write = rb->bufs;  
  12. 173  
  13. 174             r->read_event_handler = ngx_http_read_client_request_body_handler;  
  14. 175  
  15. 176             return ngx_http_do_read_client_request_body(r);  
  16. 177         }  



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

IE的请求会跑到这里:

[cpp] view plain copy

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



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

[cpp] view plain copy

  1. 187     size = clcf->client_body_buffer_size;  
  2. 188     size += size >> 2;  
  3. 189  
  4. 190     if (rb->rest < size) {  
  5. 191         size = (ssize_t) rb->rest;  
  6. 192  
  7. 193         if (r->request_body_in_single_buf) {  
  8. 194             size += preread;  
  9. 195         }  
  10. 196  
  11. 197     } else {  
  12. 198         size = clcf->client_body_buffer_size;  
  13. 199  
  14. 200         /* disable copying buffer for r->request_body_in_single_buf */  
  15. 201         b = NULL;  
  16. 202     }  
  17. 203  
  18. 204     rb->buf = ngx_create_temp_buf(r->pool, size);  
  19. 205     if (rb->buf == NULL) {  
  20. 206         return NGX_HTTP_INTERNAL_SERVER_ERROR;  
  21. 207     }  
  22. …  
  23. 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设置得比较大

你可能感兴趣的:(Nginx)