nginx升级版本导致的CLOSE_WAIT异常

背景:

在 nginx上添加 http模块(C++),添加的 http模块调用配置文件中配置好的动态库程序(C++),动态库程序实现业务代码。最近需要用到 nginx比较高版本的镜像流量的功能,但是我们线上 nginx版本比较低,所以需要进行 nginx版本升级。

 

问题:

升级 nginx版本后(1.8 -> 1.16),上游请求 nginx服务会随机超时,在 nginx服务机器上查看网络连接状态,有很多的 CLOSE_WAIT状态。

 

排查:

第一阶段:

最开始怀疑新的 nginx版本对某些配置默认值做了修改,导致老的配置加上新的 nginx版本导致的问题,于是百度 nginx CLOSE_WAIT各种文章,尝试后均无效果。

 

第二阶段:

直接在线上机器抓包,查看超时产生 CLOSE_WAIT的原因,发现是上游服务器在长连接请求上发送了 FIN,nginx服务器只回复了 ACK,没有继续回复 FIN导致的。但是为什么没有回复呢?

看抓包是上游服务器发送了请求,nginx还没有响应上游服务器,上游服务器就发送了 FIN。猜测是不是 nginx的响应还在内核的缓冲区中没有返回给上游服务器,所以先回复了 ACK,等待内核缓冲区数据发送给上游服务器后再返回 FIN。于是各种百度内核缓冲区和 nginx tcp连接相关的配置,各种尝试后仍无结果。

 

nginx升级版本导致的CLOSE_WAIT异常_第1张图片

 

第三阶段:

打算开启 nginx的 debug,看看日志中有没有什么线索。但是肯定不能线上机器开 debug,于是就想在开发环境复现。于是查看之前抓的包,发现如果一直请求 nginx服务,大概率在某个时刻就会出现上面的情况。于是拿一个线上请求,写了个简单的 tcp发送工具,把 http数据发送给新版 nginx。发现确实连续发送10~20个包左右就会出现上面的现象。

 

这尝试的期间,还发现了如果上游的请求在一个 tcp包中无法完全传输时大概率发生此问题,也就是说 nginx有时需要 recv两次才能获取完整的 http请求数据。并且发现 tcp发送工具设置很长的超时时间(比如2s),nginx也没有响应,这个时候 tcp发送工具调用 close就会复现上面的场景。于是可以判断是这时 nginx已经不处理这个连接上的请求了。

为了进一步验证,如果请求数据足够小可以在一个 tcp包中发送的话,是不会出现上面的现象的。于是简单判断是 nginx一次无法收集齐 http请求中 Content-Length的字节数,后面即使数据到达了,nginx也无动于衷,导致客户端超时关闭连接,nginx仍无动于衷,所以产生了 CLOSE_WAIT状态。

 

有了上面的信息,直接在 tcp发送工具中 http头中的 Content-Length字段写上一个比包体大的数据,发送给 nginx,也就是无论如何永远都无法完整的得到请求数据,这样发送后发现必现上面的问题。

有了必现的请求方式,就编译了 nginx的 debug版本并打开 debug日志,从日志中找一些线索。发现如果第一次无法成功 recv的话,后面的数据或者 close状态被 nginx接收到后,nginx处理时只会显示 http reading blocked(ngx_http_block_reading函数的打印),为什么这里的 read event的处理函数被设置为 ngx_http_block_reading函数了呢?

2019/08/16 10:32:05 [debug] 14202#0: *1 http run request: "/tet/testtest/tet?"
2019/08/16 10:32:05 [debug] 14202#0: *1 http reading blocked

 

第四阶段:

于是查看代码中设置 read event handler的地方( 即 r->read_event_handler = ngx_http_block_reading; ),什么时候设置了 ngx_http_block_reading函数,确实找到了几处,但是由于我对 nginx源码并不是特别熟悉,所以看不太懂。于是找到了最近的 1.14版本和 1.12版本分别进行测试,发现在 1.12版本中上面的情况是可以正常处理的,但是在 1.14版本就不可以正常处理了。所以我就对比了 1.12版本和 1.14版本 nginx在 http处理相关阶段和 ngx_http_block_reading函数相关的代码做了哪些改动?最终确认到了一个地方,就是函数 ngx_http_finalize_request中:

void ngx_http_finalize_request(ngx_http_request_t *r, ngx_int_t rc) {
    c = r->connection;
    if (rc == NGX_DONE) {
        // ...
        return;
    }

    if (rc == NGX_DECLINED) {
        // ...
        return;
    }

    if (rc == NGX_ERROR || rc == NGX_HTTP_REQUEST_TIME_OUT || rc == NGX_HTTP_CLIENT_CLOSED_REQUEST || c->error) {
        // ...
        return;
    }

    // ...
    r->done = 1;
    r->read_event_handler = ngx_http_block_reading;          // 这里就是新版本添加的设置 read event handler的地方
    r->write_event_handler = ngx_http_request_empty_handler;

    // ...
    ngx_http_finalize_connection(r);
}

 

发现在 1.14版本之后就添加了设置 recv event的处理函数为 ngx_http_block_reading的地方,没有看懂为什么要添加这一行。于是我就把这行代码注释掉重新编译 nginx。发现 1.14版本可以常工作了。于是确定是这里的问题,但是又想了一下,nginx稳定版本不可能有如此猖狂的 bug啊,我的应用场景很普遍啊,如果有这个问题,肯定早就修复了。况且不熟悉上下文的情况下修改 nginx源码,不能保证后续是否稳定。

 

 

第五阶段:

因为我们添加的 http模块类似 nginx的 subrequest,就是简单封装了一下,使用了我们自己的协议。于是我在想,如果不用我们的 http模块,使用 nginx的 proxy module,然后使用 tcp发送工具测试一下,发现 nginx处理正常,没有问题。这就奇怪了,同样是封装的 upstream功能,我们的 http模块就是简单的增加了一层协议,为什么就出现了这么大的问题。

到这里,确定是我们的 http模块和高版本的 nginx兼容有问题了。于是分别查看使用 nginx的 proxy module处理的 debug日志和我们的 http模块处理的 debug日志,发现在调用函数ngx_http_finalize_request的时候的参数不一致,proxy module传入参数是 -4(NGX_DONE),所以提前返回了,不会走到设置 recv event的处理函数的地方;而我们的 http模块的参数是 -2(NGX_AGAIN),就到了设置 recv event的处理函数的地方。所以为什么我们的参数不一样呢?

于是查看 nginx的 proxy模块的 proxy_pass处理函数和我们自己的 http模块在读取 http请求时处理函数的相关代码,同样是 recv返回的数据不够 http头中 Content-Length的指定数量,nginx proxy模块在收到 NGX_AGAIN消息后,调用函数 ngx_http_finalize_request的传参时 NGX_DONE,但我们 http模块传的就是 NGX_AGAIN,于是修改了下,把我们 http模块在 recv数据不足时的 NGX_AGAIN改为 NGX_DONE,重新编译测试,发现 nginx功能正常。后续有时间在研究下 NGX_AGAIN和 NGX_DONE在不同时刻的不同意义。

 

下面是正常情况下,客户端发送完请求立马发送 FIN的抓包处理以及 nginx debug日志:

nginx debug日志:

epoll_wait() reported that client prematurely closed connection, 
so upstream connection is closed too while sending request to upstream ...

 

你可能感兴趣的:(C++,nginx)