nber1994



lnmp报错too-big-header错误探究

January 8, 2018

之前遇到的一个偶发的错误,探讨下到底是怎么肥四

1.背景

遇到nginx报出了如下的error,导致服务为502 bad gateway, 而且某些请求稳定复现,但是其他请求缺没有问题,正常返回,正常情况下,php的warning并不会导致流程的中断

2018/01/08 15:50:20 [error] 15477#0: *2941700922 upstream sent too big header while reading response header from upstream, client: xx.xx.xxx, server: test.com, request: "POST /test/test HTTP/1.1", upstream: "fastcgi://127.0.0.1:xxxx", host: "xxxxxx", referrer: "xxxxxx"

一时之间不知道原因,唯一有异常的是warninig日志较多,推测可能是php-fpm会把warning日志加到response header头里,导致头部过大报错

php代码中存在类似与以下的错误片段:

<?php
    $a = [
        'a' => '123',
        'b' => '123',
        'c' => '123',
        'd' => '123',
    ];

    foreach ($a['nber1994'] as $item) {
        /**
            do something...
        */    
    }

此时会报warning的错误,类似于

php message: php Warning:  Invalid argument supplied for foreach() in XXXXX.php on line 206
php message: php Warning:  Invalid argument supplied for foreach() in XXXXX.php on line 206
php message: php Warning:  Invalid argument supplied for foreach() in XXXXX.php on line 206
php message: php Warning:  Invalid argument supplied for foreach() in XXXXX.php on line 206
php message: php Warning:  Invalid argument supplied for foreach() in XXXXX.php on line 206
php message: php Warning:  Invalid argument supplied for foreach() in XXXXX.php on line 206
php message: php Warning:  Invalid argument supplied for foreach() in XXXXX.php on line 206
php message: php Warning:  Invalid argument supplied for foreach() in XXXXX.php on line 206
php message: php Warning:  Invalid argument supplied for foreach() in XXXXX.php on line 206
php message: php Warning:  Invalid argument supplied for foreach() in XXXXX.php on line 206
php message: php Warning:  Invalid argument supplied for foreach() in XXXXX.php on line 206
php message: php Warning:  Invalid argument supplied for foreach() in XXXXX.php on line 206
php message: php Warning:  Invalid argument supplied for foreach() in XXXXX.php on line 206

大量的warning如果会被加到header头里的话,会触发nginx的相关配置(如果有的话),导致报错,看起来解释的通 但是是否真的是因为header头过大导致的呢,探究一下

2.简单的分析一次请求的日志行为

该情况是线上问题,无法抓取到问题发生时,php-fpm与nginx之间到底发生了什么,所以准备在自己的开发环境复现, 不过首先需要能够对nginx与php-fpm进程间通信的数据进行抓取

2.1 抓取php-fpm与nginx之间的通信数据

2.1.1 php监听端口通信方式的监听

速度较unix socket较慢,但是支持跨机器调用,我们可以通过tcpdump进行抓取

tcpdump -X -i any  port 9999 > /tmp/tcpdump.log

ps:开放php端口对外提供fastcgi服务的方法

1.设置防火墙,开放你的端口
2.php-fpm配置增加:
listen = 公网IP:9085
listen.allowed_clients = 192.168.10.66 #设置允许连接到 FastCGI 的服务器 IPV4 地址。如果允许所有那么把这条注释掉即可

2.1.2 以unix socket方式通信数据的抓取

该方法速度较快,性能高,可以通过监听socket通道得到数据

cd /path/to/php.sock
mv php.sock php.sock.orig
socat -t100 -x -v UNIX-LISTEN:php.sock,mode=777,reuseaddr,fork UNIX-CONNECT:php.sock.orig
将socket通道的内容转发一份到另一个socket,进行监听

2.1.3 抓取到的数据大概的样子

抓到的数据

2.2 简单分析

让我们观察下日志, 首先可以看到tcp的建立链接数据,可以顺便直观了解下tcp的三次握手:P

12:09:25.581210 IP 127.0.0.1.58984 > 127.0.0.1.9999: Flags [S], seq 3550696989, win 65495, options [mss 65495,sackOK,TS val 1133088550 ecr 0,nop,wscale 7], length 0
        0x0000:  4500 003c d93b 4000 4006 637e 7f00 0001  E..<.;@.@.c~....
        0x0010:  7f00 0001 e668 270f d3a3 561d 0000 0000  .....h'...V.....
        0x0020:  a002 ffd7 4619 0000 0204 ffd7 0402 080a  ....F...........
        0x0030:  4389 8f26 0000 0000 0103 0307            C..&........
12:09:25.581249 IP 127.0.0.1.9999 > 127.0.0.1.58984: Flags [S.], seq 457907882, ack 3550696990, win 65483, options [mss 65495,sackOK,TS val 1133088550 ecr 1133088550,nop,wscale 7], length 0
        0x0000:  4500 003c 0000 4000 4006 3cba 7f00 0001  E..<..@.@.<.....
        0x0010:  7f00 0001 270f e668 1b4b 1eaa d3a3 561e  ....'..h.K....V.
        0x0020:  a012 ffcb 396f 0000 0204 ffd7 0402 080a  ....9o..........
        0x0030:  4389 8f26 4389 8f26 0103 0307            C..&C..&....
12:09:25.581261 IP 127.0.0.1.58984 > 127.0.0.1.9999: Flags [.], ack 1, win 512, options [nop,nop,TS val 1133088550 ecr 1133088550], length 0
        0x0000:  4500 0034 d93c 4000 4006 6385 7f00 0001  E..4.<@.@.c.....
        0x0010:  7f00 0001 e668 270f d3a3 561e 1b4b 1eab  .....h'...V..K..
        0x0020:  8010 0200 602b 0000 0101 080a 4389 8f26  ....`+......C..&
        0x0030:  4389 8f26                                C..&

2.2.1 一次请求的nginx-error日志

nginx-error

2.2.2 一次请求的tcpdump的日志

tcpdump-log

2.2.3 简单结论

观察到的现象

得到的初步结论

对比nginx日志与tcpdump的日志的量,我们能看到:

1.php-fpm确实会将warning与error传递给nginx

2.不管fastcgi_buffer_size设置了多少,其实是根据fastcgi_buffer_size的大小来一段一段的读取php-fpm的响应头,不管传了多少的header,都会按照fastcgi_buffer_size的大小,一段一段的读取,然后写入到nginx的日志

得到了上述结论,我们大致可以确定,并不是因为header头过大,或者说php-fpm传递给nginx的数据超过fastcgi_buffer_size的值才报出的502,那到底是为什么会报相关错误呢?这就需要探究下nginx内部了

3.nginx内部发生了什么

首先我们知道:

3.1 fastcgi_buffer_size

我们知道,fastcgi_buffer_size与proxy_buffer_size这两个参数会影响到响应头, 其中proxy_buffer_size影响的是nginx作为反向代理时的参数 对于nginx配置文件中的fastcgi_buffer_size,文档中是这么写的

文档

其中 the first part of the response received from the FastCGI server.,这个参数指定的是接受到fastcgi-server端的第一部分的响应(一般是response header),在lnmp的场景里,fastcgi-server就是php部分 其中这个first part的含义是这样子的,由于upstream是一个通用的组件,它不知道后端的协议,而对于http场景来说,由于http是需要header的,而后端的协议不一定有头,此时就需要我们通过解析后端的协议,然后来设置好发送给client的头,最终发送给client,通过上面的观察,我们发现php的错误信息也会包含其中

以下是所谓的正常情况下的the first part of the response的结构

header

3.2 nginx代码报错分析

源码中报错的位置如下

error

//nginx接收来自上游服务器的响应头部
static void ngx_http_upstream_process_header(ngx_http_request_t *r, ngx_http_upstream_t *u)
{
    //循环接收来自后端服务器的响应头部,并使用状态机解析
    for ( ;; )
    {
        n = c->recv(c, u->buffer.last, u->buffer.end - u->buffer.last);

        //表示还需要再次接收来自上游服务器的响应,重新注册读事件到epoll中
        if (n == NGX_AGAIN)
        {
            ngx_handle_read_event(c->read, 0);
            return;
        }

        //更新接收缓冲区
        u->buffer.last += n;

        //调用http模块的方法,解析响应头部
        //fastcgi为: ngx_http_fastcgi_process_header
        rc = u->process_header(r);

        //表示包头还没有完全接收到
        if (rc == NGX_AGAIN)
        {
            //已经读取到末尾
            if (u->buffer.last == u->buffer.end) {
                ngx_log_error(NGX_LOG_ERR, c->log, 0,
                        "upstream sent too big header");

                ngx_http_upstream_next(r, u,
                        NGX_HTTP_UPSTREAM_FT_INVALID_HEADER);
                return;
            }
            continue;
        }
    }
}

可以看到,当处理头部的会调函数process_header返回NGX_AGAIN,同时已经读取到了response的末尾,则会报错

3.2.1处理头部的回调函数process_header伪代码

//解析来自后端服务器发来的响应头部,从fastcgi格式转为nginx格式。
//采用两个状态机,一个解析fastcgi头部,一个解析fastcgi响应包头
static ngx_int_t ngx_http_fastcgi_process_header(ngx_http_request_t *r)
{
    //循环解析每一个组(fastcgi头部 + 多个http响应头部组成的数据)。因为每次从内核接收到的数据有可能包含多个组
    for ( ;; )
    {
        //解析fastcgi的头部
        if (f->state < ngx_http_fastcgi_st_data)
        {
            f->pos = u->buffer.pos;
            f->last = u->buffer.last;

            //使用状态解析fastcgi的头部
            rc = ngx_http_fastcgi_process_record(r, f);
            u->buffer.pos = f->pos;
            u->buffer.last = f->last;
        }

        //这个循环用于解析这个组内的每一个http响应头部。因此该组内的http响应包头可能由多个http响应头部组成。
        //而ngx_http_parse_header_line状态机每次只能解析一个http响应头部,因此需要循环解析
        for ( ;; )
        {
            //使用状态机解析来自后端服务器发来的http响应头部
            rc = ngx_http_parse_header_line(r, &u->buffer, 1);
            if (rc == NGX_OK)
            {
                //解析某个http响应头部成功后,保存到数组链表中
                /* a header line has been parsed successfully */
                h = ngx_list_push(&u->headers_in.headers);

                //保存http响应头部
                ngx_memcpy(h->key.data, r->header_name_start, h->key.len);
                //保存http响应头部的值
                ngx_memcpy(h->value.data, r->header_start, h->value.len);
                h->hash = r->header_hash;

                //如果是负责均衡模块支持的头部,则把常用头部的指针指向数组链表中相应位置
                hh = ngx_hash_find(&umcf->headers_in_hash, h->hash,
                                   h->lowcase_key, h->key.len);
                hh->handler(r, h, hh->offset);
            }

            //解析完成所有的响应头部,则保存响应状态码
            if (rc == NGX_HTTP_PARSE_HEADER_DONE)
            {
                if (u->headers_in.status)
                {
                    //后端服务器返回了状态码,则优先使用后端服务的响应状态码
                }
                else if (u->headers_in.location)
                {
                    //后端服务器返回了location,则需要给客户端返回302重定向
                }
                else
                {
                    //否则nginx构造200 0k返回给客户端
                    u->headers_in.status_n = 200;
                    ngx_str_set(&u->headers_in.status_line, "200 OK");
                }
            }
        }
    }
}

以上是流程代码,具体细节未展示 以上代码会对http与fastcgi的header分别进行解析

当两个状态机对fastcgi与http头解析如果出错的话(比如header头缺失),也会返回NGX_AGAIN,如果此时读取到php-fpm返回的header的尾部时,则会报错 那到底是否是这个原因呢?php-fpm传送了残缺的或者没有传递response header,才导致的报错?我们复现下观察下

4.尝试复现

如果可以复现出502,我们就能验证我们的猜想 不断的增大warning的数目,并监听端口与日志,截取502发生时,php-fpm到底传了什么给nginx 出问题时,是因为过多的warning写入到了fastcgi_buffer_size中,我们可以按着这个思路,进行强行复现

4.1 php脚本

我们可以尝试着一步步加大error的size,来观察发送502时的日志情况 以下是脚本:

<?php
for ($i = 0; $i<$_GET['iterations']; $i++)
        error_log(str_pad("a", $_GET['size'], "a"));
echo "got here\n";

4.2 日志监听

同时监听php绑定的端口,得到日志(此时,fastcgi_buffer_size 4K)

4.3 压测方法

使用循环对脚本进行请求,不断的增加单条日志的size与迭代次数 ,使其向日志里写入更多的字节,同时过滤返回的502以及报错时的迭代次数与每次日志的大小

bash~ for it in {30..200..3}; do for size in {100..250..3}; do echo "size=$size iterations=$it $(curl -sv "http://localhost/debug.php?size=$size&iterations=$it" 2>&1 | egrep '^< HTTP')"; done; done | grep 502 | head

size=121 iterations=30 < HTTP/1.1 502 Bad Gateway
size=109 iterations=33 < HTTP/1.1 502 Bad Gateway
size=241 iterations=48 < HTTP/1.1 502 Bad Gateway
size=226 iterations=51 < HTTP/1.1 502 Bad Gateway
size=190 iterations=60 < HTTP/1.1 502 Bad Gateway
size=223 iterations=69 < HTTP/1.1 502 Bad Gateway
size=127 iterations=87 < HTTP/1.1 502 Bad Gateway
size=199 iterations=96 < HTTP/1.1 502 Bad Gateway
size=151 iterations=99 < HTTP/1.1 502 Bad Gateway
size=106 iterations=102 < HTTP/1.1 502 Bad Gateway 

顺便说下,unix socket方式性能真的很高,这种unix socket通信的情况下,测试语句运行完了也没有报502 由上可知,我们复现了502,下面我们对日志进行紧张的分析

4.4 结果分析

报错的nginx日志

2018/01/09 11:39:01 [error] 4207#0: *2045 FastCGI sent in stderr: "php message: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
php message: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
php message: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
php message: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
php message: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
php message: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
php message: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
php message: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
php message: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
php message: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
php message: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
php message: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
php message: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
php message: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
php message: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
php message: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
php message: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
php message: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
php message: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
php message: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
php message: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
php message: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
php message: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
php message: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
php message: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
php message: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
php message: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
php message: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
php message: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
php message: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
2018/01/09 11:39:01 [error] 4207#0: *2045 upstream sent too big header while reading response header from upstream, client: 10.30.128.251, server: testtest, request: "GET /debug.php?size=121&iterations=30 HTTP/1.1", upstream: "fastcgi://127.0.0.1:9999", host: "test.com"

可以看到我们成功复现了too big header报错:P

正常的nginx-error日志

2018/01/09 11:39:02 [error] 4207#0: *2353 FastCGI sent in stderr: "php message: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
php message: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
php message: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
php message: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
php message: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
php message: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
php message: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
php message: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
php message: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
php message: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
php message: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
php message: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
php message: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
php message: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
php message: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
php message: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
php message: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
php message: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
php message: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
php message: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
php message: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
php message: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
php message: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
php message: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
php message: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
php message: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
php message: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
php message: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
php message: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
php message: aaaaaaaaaaaaa

紧张分析

上面的日志,一个是39:01,一个是39:02的,按照我们的测试脚本,时间越靠后,传递的日志量会越大(至少是之前的3倍),但是为什么数据量较小的39:01秒的日志触发了502,而数据量较大的39:02没有报错,而且,日志的量也是相同的,明显有截断,明显超出了4K,这也能验证我们的推测,其实fastcgi_buffer_size并不是一个检查值,超过这个值并不会报错。而更像是一个暂存的空间

进一步紧张分析

我们回去看下之前监控的tcpdump的日志,发现了一些有趣的现象

一次正常的日志

        0x0cf0:  6161 6161 6161 6161 6161 6161 6161 6161  aaaaaaaaaaaaaaaa
        0x0d00:  6161 6161 6161 6161 6161 6161 6161 6161  aaaaaaaaaaaaaaaa
        0x0d10:  6161 6161 6161 6161 6161 6161 6161 6161  aaaaaaaaaaaaaaaa
        0x0d20:  6161 6161 610a 5048 5020 6d65 7373 6167  aaaaa.php.messag
        0x0d30:  653a 2061 6161 6161 6161 6161 6161 6161  e:.aaaaaaaaaaaaa
        0x0d40:  6161 6161 6161 6161 6161 6161 6161 6161  aaaaaaaaaaaaaaaa
        0x0d50:  6161 6161 6161 6161 6161 6161 6161 6161  aaaaaaaaaaaaaaaa
        0x0d60:  6161 6161 6161 6161 6161 6161 6161 6161  aaaaaaaaaaaaaaaa
        0x0d70:  6161 6161 6161 6161 6161 6161 6161 6161  aaaaaaaaaaaaaaaa
        0x0d80:  6161 6161 6161 6161 6161 6161 6161 6161  aaaaaaaaaaaaaaaa
        0x0d90:  6161 6161 6161 610a 0000 0000 0106 0001  aaaaaaa.........
        0x0da0:  0024 0400 436f 6e74 656e 742d 7479 7065  .$..Content-type
        0x0db0:  3a20 7465 7874 2f68 746d 6c0d 0a0d 0a67  :.text/html....g
        0x0dc0:  6f74 2068 6572 650a 0000 0000 0103 0001  ot.here.........
        0x0dd0:  0008 0000 0000 0000 0061 6161            .........aaa
        12:09:25.585035 IP 127.0.0.1.9999 > 127.0.0.1.58984: Flags [F.], seq 3497, ack 721, win 523, options [nop,nop,TS val 1133088554 ecr 1133088551], length 0
        0x0000:  4500 0034 918f 4000 4006 ab32 7f00 0001  E..4..@.@..2....
        0x0010:  7f00 0001 270f e668 1b4b 2c53 d3a3 58ee  ....'..h.K,S..X.
        0x0020:  8011 020b 4fa2 0000 0101 080a 4389 8f2a  ....O.......C..*
        0x0030:  4389 8f27                                C..'

一次502的日志

        0x1f30:  6d65 7373 6167 653a 2061 6161 6161 6161  message:.aaaaaaa
        0x1f40:  6161 6161 6161 6161 6161 6161 6161 6161  aaaaaaaaaaaaaaaa
        0x1f50:  6161 6161 6161 6161 6161 6161 6161 6161  aaaaaaaaaaaaaaaa
        0x1f60:  6161 6161 6161 6161 6161 6161 6161 6161  aaaaaaaaaaaaaaaa
        0x1f70:  6161 6161 6161 6161 6161 6161 6161 6161  aaaaaaaaaaaaaaaa
        0x1f80:  6161 6161 6161 6161 6161 6161 6161 6161  aaaaaaaaaaaaaaaa
        0x1f90:  6161 6161 6161 6161 6161 6161 6161 6161  aaaaaaaaaaaaaaaa
        0x1fa0:  6161 6161 6161 6161 6161 6161 6161 6161  aaaaaaaaaaaaaaaa
        0x1fb0:  6161 6161 6161 6161 6161 6161 6161 6161  aaaaaaaaaaaaaaaa
        0x1fc0:  6161 6161 6161 6161 6161 6161 6161 6161  aaaaaaaaaaaaaaaa
        0x1fd0:  6161 6161 6161 6161 6161 6161 6161 6161  aaaaaaaaaaaaaaaa
        0x1fe0:  6161 6161 6161 6161 6161 6161 6161 6161  aaaaaaaaaaaaaaaa
        0x1ff0:  6161 6161 6161 6161 6161 6161 6161 6161  aaaaaaaaaaaaaaaa
        0x2000:  6161 6161 6161 6161 6161 6161 6161 6161  aaaaaaaaaaaaaaaa
        0x2010:  6161 6161 6161 6161 6161 6161 6161 6161  aaaaaaaaaaaaaaaa
        0x2020:  6161 6161 6161 6161 6161 6161 6161 6161  aaaaaaaaaaaaaaaa
        0x2030:  6161 610a                                aaa.
12:09:43.315839 IP 127.0.0.1.36564 > 127.0.0.1.9999: Flags [.], ack 40961, win 768, options [nop,nop,TS val 1133106285 ecr 1133106285], length 0
        0x0000:  4500 0034 c6d5 4000 4006 75ec 7f00 0001  E..4..@.@.u.....
        0x0010:  7f00 0001 8ed4 270f 363a 1573 0df7 5365  ......'.6:.s..Se
        0x0020:  8010 0300 e2df 0000 0101 080a 4389 d46d  ............C..m
        0x0030:  4389 d46d                                C..m

发现了吗,502报错的情况下,tcpdump抓到的包中,末尾缺少了http的response header,这正验证了我们之前的推测,php-fpm在返回body之前,并没有传递完整的response header给nginx,导致nginx报出来错误

4.5 暂时的结论

观察上面的日志,报错的最后的响应头不完整或者根本就没有,这个正好会进入到nginx中未解析到header头并且已到header尾部的情况,触发报错 所以,暂时现在可以推断的是,

php-fpm会在一定的情况下向nginx传送不完整的响应头数据,导致nginx解析fastcgi与http的header出错,导致报出502 这看起来不可思议,竟然是下游服务出了差错,而不是因为nginx内的fastcgi_buffer_size太小导致的错误,不过该问题并不是不可能发生: 哈哈

5. 探究下不同fastcgi_buffer_size下的502发生情况

5.1 fastcgi_buffer_size 1k

nginx日志

不会报502的错误日志 error-log

会报502的错误日志 error-log

tcpdump的日志

不会报502的错误日志 tcp-error

会报502的错误日志 tcp-error

测试结果

bash~ for it in {30..200..3}; do for size in {100..250..3}; do echo "size=$size iterations=$it $(curl -sv "http://test/debug.php?size=$size&iterations=$it" 2>&1 | egrep '^< HTTP')"; done; done | grep 502 | head
size=121 iterations=30 < HTTP/1.1 502 Bad Gateway
size=190 iterations=30 < HTTP/1.1 502 Bad Gateway
size=109 iterations=33 < HTTP/1.1 502 Bad Gateway
size=202 iterations=33 < HTTP/1.1 502 Bad Gateway
size=127 iterations=36 < HTTP/1.1 502 Bad Gateway
size=184 iterations=36 < HTTP/1.1 502 Bad Gateway
size=241 iterations=36 < HTTP/1.1 502 Bad Gateway
size=169 iterations=39 < HTTP/1.1 502 Bad Gateway
size=205 iterations=42 < HTTP/1.1 502 Bad Gateway
size=229 iterations=42 < HTTP/1.1 502 Bad Gateway

5.2 fastcgi_buffer_size 2k

nginx日志

不会报502的错误日志 error-log

会报502的错误日志 error-log

tcpdump的日志

不会报502的错误日志 tcp-error

会报502的错误日志 tcp-error

测试结果

bash~ for it in {30..200..3}; do for size in {100..250..3}; do echo "size=$size iterations=$it $(curl -sv "http://test/debug.php?size=$size&iterations=$it" 2>&1 | egrep '^< HTTP')"; done; done | grep 502 | head
size=121 iterations=30 < HTTP/1.1 502 Bad Gateway
size=190 iterations=30 < HTTP/1.1 502 Bad Gateway
size=109 iterations=33 < HTTP/1.1 502 Bad Gateway
size=229 iterations=42 < HTTP/1.1 502 Bad Gateway
size=241 iterations=48 < HTTP/1.1 502 Bad Gateway
size=106 iterations=51 < HTTP/1.1 502 Bad Gateway
size=226 iterations=51 < HTTP/1.1 502 Bad Gateway
size=175 iterations=54 < HTTP/1.1 502 Bad Gateway
size=190 iterations=60 < HTTP/1.1 502 Bad Gateway
size=148 iterations=63 < HTTP/1.1 502 Bad Gateway

5.3 fastcgi_buffer_size 4k

nginx日志

不会报502的错误日志 error-log

会报502的错误日志 error-log

tcpdump的日志

不会报502的错误日志 tcp-error

会报502的错误日志 tcp-error

测试结果

bash~ for it in {30..200..3}; do for size in {100..250..3}; do echo "size=$size iterations=$it $(curl -sv "http://test/debug.php?size=$size&iterations=$it" 2>&1 | egrep '^< HTTP')"; done; done | grep 502 | head
size=121 iterations=30 < HTTP/1.1 502 Bad Gateway
size=109 iterations=33 < HTTP/1.1 502 Bad Gateway
size=241 iterations=48 < HTTP/1.1 502 Bad Gateway
size=226 iterations=51 < HTTP/1.1 502 Bad Gateway
size=190 iterations=60 < HTTP/1.1 502 Bad Gateway
size=223 iterations=69 < HTTP/1.1 502 Bad Gateway
size=127 iterations=87 < HTTP/1.1 502 Bad Gateway
size=199 iterations=96 < HTTP/1.1 502 Bad Gateway
size=151 iterations=99 < HTTP/1.1 502 Bad Gateway
size=106 iterations=102 < HTTP/1.1 502 Bad Gateway

5.4 观察发现

由上1k,2k,4k的比对结果,我们可以得出以下结论

综合以上502报错时的size与iteration的值的乘积,我们可以发现,不同fastcgi_buffer_size下,其实发生502错误时的php-fpm传给nginx的数据的大小基本是一样的 提高fastcgi_buffer_size的大小,并不能减少too big header的发生风险 但是建议是设置成操作系统分页的大小

6. 结论

经过探究,我们最终可以得到:

1.php-fpm确实会将warning与error传递给nginx

2.不管fastcgi_buffer_size设置了多少,其实是根据fastcgi_buffer_size的大小来一段一段的读取php-fpm的响应头,不管传了多少的header,都会按照fastcgi_buffer_size的大小,一段一段的读取,然后写入到nginx的日志

3.php-fpm会在一定的情况下向nginx传送不完整的响应头数据,导致nginx解析fastcgi与http的header出错,导致报出502

4.提高fastcgi_buffer_size的大小,并不能减少too big header的发生风险

7. 疑问

经过以上的探究,我们可以看到以上的结论,但是仍有很多疑问

1.为什么php-fpm发送的数据会缺少header

2.有没有其它情况会导致too big header报错

3.线上问题是仅有特定的请求参数会稳定复现,其他的则不会复现,到底发生了什么?是我们探究的原因导致的吗?

以上的疑问待进一步的探究

8. 对疑问进一步分析

经过老司机的指点,对以上压测得出的数据进行进一步的分析, 上面我们探究了在不同fastcgi_buffer_size下的502出错情况

1K

bash~ for it in {30..200..3}; do for size in {100..250..3}; do echo "size=$size iterations=$it $(curl -sv "http://test/debug.php?size=$size&iterations=$it" 2>&1 | egrep '^< HTTP')"; done; done | grep 502 | head
size=121 iterations=30 < HTTP/1.1 502 Bad Gateway
size=190 iterations=30 < HTTP/1.1 502 Bad Gateway
size=109 iterations=33 < HTTP/1.1 502 Bad Gateway
size=202 iterations=33 < HTTP/1.1 502 Bad Gateway
size=127 iterations=36 < HTTP/1.1 502 Bad Gateway
size=184 iterations=36 < HTTP/1.1 502 Bad Gateway
size=241 iterations=36 < HTTP/1.1 502 Bad Gateway
size=169 iterations=39 < HTTP/1.1 502 Bad Gateway
size=205 iterations=42 < HTTP/1.1 502 Bad Gateway
size=229 iterations=42 < HTTP/1.1 502 Bad Gateway

2k

bash~ for it in {30..200..3}; do for size in {100..250..3}; do echo "size=$size iterations=$it $(curl -sv "http://test/debug.php?size=$size&iterations=$it" 2>&1 | egrep '^< HTTP')"; done; done | grep 502 | head
size=121 iterations=30 < HTTP/1.1 502 Bad Gateway
size=190 iterations=30 < HTTP/1.1 502 Bad Gateway
size=109 iterations=33 < HTTP/1.1 502 Bad Gateway
size=229 iterations=42 < HTTP/1.1 502 Bad Gateway
size=241 iterations=48 < HTTP/1.1 502 Bad Gateway
size=106 iterations=51 < HTTP/1.1 502 Bad Gateway
size=226 iterations=51 < HTTP/1.1 502 Bad Gateway
size=175 iterations=54 < HTTP/1.1 502 Bad Gateway
size=190 iterations=60 < HTTP/1.1 502 Bad Gateway
size=148 iterations=63 < HTTP/1.1 502 Bad Gateway

4k

bash~ for it in {30..200..3}; do for size in {100..250..3}; do echo "size=$size iterations=$it $(curl -sv "http://test/debug.php?size=$size&iterations=$it" 2>&1 | egrep '^< HTTP')"; done; done | grep 502 | head
size=121 iterations=30 < HTTP/1.1 502 Bad Gateway
size=109 iterations=33 < HTTP/1.1 502 Bad Gateway
size=241 iterations=48 < HTTP/1.1 502 Bad Gateway
size=226 iterations=51 < HTTP/1.1 502 Bad Gateway
size=190 iterations=60 < HTTP/1.1 502 Bad Gateway
size=223 iterations=69 < HTTP/1.1 502 Bad Gateway
size=127 iterations=87 < HTTP/1.1 502 Bad Gateway
size=199 iterations=96 < HTTP/1.1 502 Bad Gateway
size=151 iterations=99 < HTTP/1.1 502 Bad Gateway
size=106 iterations=102 < HTTP/1.1 502 Bad Gateway

8.1 观察

对比上述结果,有没有发现什么诡异? 为什么三次压测的结果,得到的502次数都是10次 为什么第一次报错都是在size=121,iteration=30的情况下发生的?

8.2 进一步分析结果

我们把每一次的size与iteration进行相乘,我们可以得到如下的一些结果:(添加上开头的php message: 这13个字符)

(121 + 13) * 30 = 4020
(109 + 13) * 33 = 4026
(241 + 13) * 48 = 12192
(226 + 13) * 51 = 12189
(190 + 13) * 60 = 12180
.....

我们知道1024是程序员日,那上面的数据有什么敏感的吗?

1024 * 4 = 4096,
4096 * 3 = 12288
....

甩锅

对比上面的数据,似乎是php的日志在某一个nk大小的时候(我的测试机器上很可能是4k), 会稳定复现这个问题呢? 这也很好的解释了为什么线上的某些请求会稳定复现这种问题,而其他请求,即使打了更多的php warning日志也不会触发502报警的现象

这回,我们可以把锅甩给php-fpm,我们可以不负责任的胡乱猜测一下,

是否php-fpm内部也存在类似的buffer(在我的测试机器上也许是4k),也许这段buffer是tcp发送消息的缓冲区,当发送的字节很接近4k时, 就会触发某些bug,导致某些尾部数据的丢失, 而当大小与4k的整数倍的数据相差比较大的数据,则不会丢失数据, emmm,也许是在重新分配buffer的时候出了问题,导致的数据丢失:P

参考

部分内容参考自以下内容,感谢以下作者对知识的分享 http://nginx.org/en/docs/http/ngx_http_fastcgi_module.html http://www.pagefault.info/?p=273 http://blog.csdn.net/ApeLife/article/details/78001508?ref=myrecommend http://blog.csdn.net/midion9/article/details/51384002 https://www.imooc.com/article/19278 https://stackoverflow.com/questions/23844761/upstream-sent-too-big-header-while-reading-response-header-from-upstream

以上探究仅是个人观点,可能存在错误,欢迎交流~ eamil:m18710895391@163.com