布景

最近事务方反应线上 Nginx 常常会打出一些『古怪』的 access 日志,古怪之处在于这些日志的 request_time 值总是正好 upstream_response_time 的值大5秒,所以我就帮他们检查了一下导致这个问题的原因,本文记载一下终究查询的定论以及进程。

定论

首要给出发作该问题的原因,这样不愿意看细节的同学看完这段就能够完毕阅读了。该问题是由 Nginx 的推延关闭(lingering close)衔接导致的。Nginx 为了能够滑润关闭衔接,选用了推延关闭,它的工作办法如下:Nginx 在给客户端发送完终究一个数据包后会首要关闭 TCP 衔接的写端(TCP 是全双工协议,任何一端都即可读也可写),表明效劳端不会再向客户端发送任何数据,可是不会当即关闭 TCP 衔接的读端,而是等候一个超时,在超时抵达后假如客户端还没有数据发来,Nginx 才会关闭TCP的读端,然后关闭整个衔接,然后再输出日志。另一方面,Nginx 是在关闭衔接后才输出日志,所以在输出日志之前呼应早就发送给了用户,因而对事务几乎没有影响。可是这也会导致 requset_time 值变得不精确,使其失掉核算意义,敞开 Keep-Alive 能够部分解决这一问题。

问题追寻

首要咱们先来了解一下 request_time 与 upstream_response_time 这两个值在 Nginx 中是怎样界说的,它们的意义在 Nginx 手册中描绘如下:

从上面的界说能够看到, request_time 的值包含了接纳用户恳求数据、处理恳求以及给用户发送呼应这三部分的耗时,而 upstream_response_time 仅仅 Nginx 和上游效劳交互的时刻,在咱们这儿便是PHP 处理恳求的时刻。那么由于网络原因,request_time 大于乃至远大于upstream_response_time 都是很正常的,可是总是大5秒就很古怪了。

Nginx 装备导致的么?

由于两者总是相差5秒,很简略让人想到或许是Nginx的装备文件中的某个参数导致了该问题,经过检查装备文件的确发现了一个可疑的装备项目:

1 fastcgi_connect_timeout5

这个装备表明将 Nginx 与 PHP-FPM 之间的衔接超时设置为5秒,那么导致该问题的一个或许的原因便是当 Nginx 第一次测验与 PHP-FPM 树立衔接超时了,第2次测验才连上,这样就会正好多出了一个5秒的衔接超时时刻。可是进一步检查日志发现,PHP 的恳求处理日志早在 Nginx 日志之前5秒就打出来了,而且假如 Nginx 衔接 PHP 超时是会输出 error 日志的,可是线上的 error 日志里边并没有衔接超时的记载,所以这个原因很快被否决了。

Nagle 算法惹的祸?

已然装备文件中没有显式的装备会导致该问题,那么就有或许是 Nginx 的默许装备导致的,因而我查找了一下源代码中与5有关的内容,希望能发现一些蛛丝马迹,成果发现了一段如下的注释:

1 2 3 4 5 6 7 8 Therefore we use the TCP_NOynyhPUSH option (similar to Linux's TCP_CORK) 张志鹏柳岩to postpone the sending - it not only sends a header and the first part of the file in one packet, but also sends the file pages in the full packets. But until FreeBSD 4.5 turning TCP_NOPUSH off does not flush a pending data that less than MSS, so that data may be sent with 5 second delay. So we do not use TCP_NOPUSH on FreeBSD prior to 4.5, although it can be used for non-keepalive HTTP connections.

上面注释的大约意思是,在较老的 FreeBSD 的操作系统上,就算关闭了 TCP_NOPUSH 参数,假如一个包小于 MSS,仍然有或许会被推延5秒发送。TCP_NOPUSH 参数是用来操控 TCP 的 Nagle 算法的,该算法的详细内容能够查阅网上材料,其中心思维是将多个接连的小包累积成一个大包,然后一次性发送,这能够提高网宿州,nginx的推延关闭,心爱头像络的运用率。Nginx 中还有一个装备项也与 Nagle 算法相关,那便是 TCP_NODELAY,它的意义与 TCP_NOPUSH 正好相反,表明关闭 TCP 的 Nagle 化,也便是内核收到数据后不论巨细直接发送。这两个装备看似互斥,可是在实践运用中,咱们却将它们都翻开,由于 Nginx 能够经过合作运用这两个装备来最大功率的运用网络。合作办法如下:首要依据 TCP_NOPUSH 敞开 Nagle 潘玮楷算法,将数据累积到缓冲区中,当需求发送的数据都累积完结可是还没有到达 MSS 时,当即依据TCP_NODELAY 关闭 Nagel 算法,此刻内核会一次性将缓冲区中的数据宣布。总结为一句话便是:累积满意量的数据(NOPUSH)然后当即宣布(NODELAY)。

咱们线上的Linux内核版本是2.6.32,比较老了,所以咱们猜测会不会也存在上面所说的这个问题,这时组内其他同学检查 Nginx 装备文件,发现 sendfile,TCP_NOPUSH 以及 TCP_NODELAY 这三个开关都翻开了,可是 Keep-Alive 却没有翻开,而 Nginx 手册中清晰写到只要在敞开 sendfile 的情况下 TCP_NOPUSH 才会收效,以及敞开 Keep-Alive 的前提下 TCP_NODELAY 才会翻开。换句话说,咱们线上只敞开了 TCP_NOPUSH,却没有敞开 TCP_NODELAY,这就有或许导致包的推延发送。因而咱们联系了运维相关的同学,将 Keep-Alive 翻开,也便是让 TC刘克俭简历P_NODELAY收效,然后调查日志,发现相差5秒的反常日志真的消失了。这时咱们都认为问题的原因找到了。

真的是Nagle算法惹的祸么?

尽管敞开 Keep-Alive 使 TCP_NODELAY 收效后,反常日志消失了,可是我心里仍然有几个疑问,总觉得这不是导致问题的根本原因:

在几个猜测都不对后,觉得仍是应该调试一下 Nginx 代码才干发现问题。由于忧虑直接 gdb 调试或许会导致 Nginx 的功能下降,以至于不能触发能够打出古怪日志的条件,因而我想到了一个简略的变通办法:只要能获取核算 request_time 之前的一切函数调用栈,那么也就能够大致知道时刻花在哪了。依据这个思路我修改了一下 Nginx 源代码,在获取时刻的当地有意加了一个对内存的不合法拜访:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 ngx_http_log_request_time(ngx_http_request_t *r, u_char *buf, ngx_http_log_op_t *op) { ngx_time_t *tp; ngx_msec_int_t ms; tp = ngx_timeofday(); ms = (ngx_msec_int_t) ((tp->sec - r->start_sec) * 1000 + (tp->msec - r->start_msec)); ms = ngx_max(ms, 0); //假如呼应时刻是5s,就触发下面的内存拜访过错,然后发作一个core。 if (ms == 5000) { *(char *)(0) = 'N'; } return ngx_sprintf(buf, "%T.%03M", (time_t) ms / 1000, ms % 1000); }

选用修改后的代码,在至尊毒后王爷喂不饱短衔接的情况下,Nginx只要在关恩佩罗耶林闭与客户端的衔接后才会开端输出日志,而不是给客户端发送完数据后就打日志。那么这个关闭衔接的进程的耗时就很有或许是request_time比 upstream_response_time多出来的时刻。咱们接下来再来详细经过源代码看一下 Nginx关闭衔接的进程,首要的代码如白灵和兆海下:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 ngx_http_finalize_connection(ngx_http_request_t *r) { if (r->reading_body) { r->keepalive = 0; r->lingering_close = 1; } //假如敞开了长衔接且长衔接未超时,那么走长衔接处理相关的代码 if (!ngx_terminate && !ngx_exiting && r->keepalive && clcf->keepalive_timeout > 0) { ngx_http_set_keepalive(r); return; } //不再需求keepalive,即衔接需求关闭,而且翻开了linge兲孖ring close,就经过lingering close的办法来关闭衔接,也便是推延关闭 if (clcf->lingering_close == NGX_HTTP_LINGERING_ALWAYS || (clcf->lingering_close =余干舒世珍= NGX_HTTP_LINGERING_ON && (r->lingering_close || r->header_in->pos < r->header_in->last || r->connection->read->ready))) { ngx_http_set_lingering_close(r); return; } ngx_http_close_request(r, 0); }

留意上面并不是 ngx_http_finalize_connection 函数的悉数,我仅仅贴出了与问题相关的代码。能够看到 Nginx 在不需求保护长衔接且敞开了 lingering close 的时,会调用 ngx_http_set_lingering_close 来设置终究的关闭函数。单词 lingering 是推延的意思,那么 lingering close 自然是推延关闭的意思。了解 socket 编程的同学应该知道 socket 有一个选项叫 SO_LINGER,假如对一个套接字敞开了该选项,那么在调用 close 或许 shutdown 关闭套接字时会一向堵塞到将缓冲区里的音讯红尘情歌吉他谱都发送完毕才干回来。敞开该选项的首要效果是为了滑润关闭套接字,使效劳具有更好的兼容性,更详细的内容咱们能够网上查阅材料。前面提到假如直接在套接字上设置 SO_LINGER 特点,那么在关闭时或许会引起堵塞,可是咱们又知道 Nginx 里的套接字都设置了非堵塞特点,这会导致未界说的行为,别的假如完全由操作系统来进行推延关闭,或许并不能满意 Nginx 的宿州,nginx的推延关闭,心爱头像需求,所以 Nginx 没有运用这种办法,而是自己完成了推延关闭。首要看下 ngx_http_set_lingering_close 函数,它是用来对一个恳求设置推延关闭办法的:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 ngx_http_set_lingering_close(ngx_http_request_t *r) { ngx_event_t *rev, *wev; ngx_connection_t *c; ngx_http_core_loc_conf_t *clcf; c = r->connection; clcf =mk妹 ngx_http_get_module_loc_conf(r, ngx_http_core_module); rev = c->read; //获取衔接的读事情 //设置读事情触发时的处理函数,也便是延时关闭衔接函数 rev->handler = ngx_http_lingering_close_handler; //lingering_time用来操控总的推延超时时刻,比方在第一个lingering_timeout后,收到了数据,那么接下来还会再进行 //推延关闭,然后再等候lingering_timeout,如此重复,可是总的时刻不能超越lingering_time r->lingering_time = ngx_time() + (time_t) (clcf->lingering_time / 1000); //向事情循环中加入超时事情,超时时刻是lingering_timeout, //也便是说在lingering_timeout时刻后,ngx_http_lingering_close_handler会被调用 ngx_add_timer(rev, clcf->lingering_timeout); if (ngx_handle_read_event(rev, 0) != NGX_OK) { ngx_http_close_request(r, 0); return; } wev = c->write; wev->handler = ngx_http_empty_handler; if (wev->active && (ngx_event_f宿州,nginx的推延关闭,心爱头像lags漯河赵耀嵩 & NGX_USE_LEVEL_EVENT)) { if (ngx_del_event(wev, NGX_WRITE_EVENT, 0) != NGX_OK) { ngx_http_close_request(r, 0); return; } } //关闭套接字的写端,也便是说只要读是推延关闭的 if (ngx_shutdown_socket(c->fd, NGX_WRITE_SHUTDOWN) == -1) { ngx_connection_error(c, ngx_socket_errno, ngx_shutdown_socket_n " failed"); ngx_http_close_request(r, 0); return; } if (rev->ready) { ngx_http_lingering_close_handler(rev); } }

ngx_http_set_lingering_close 函数便是用过来设置推延关闭函数的,要害的部分现已加了注释。能够看到 Nginx 首要经过 吴英杰简历lingering_time 和 lingering_timeout 这两个参数来操控推延关闭的时刻,lingering_time 表明总的推延时刻,lingering_timeout 表明单次推延时刻。上面的这段代码会向 Nginx 的事情循环注册一个超时时刻,超时的时刻距离是 lingering_timeout,超时事情的处理函数是 ngx_http_lingering_close_handler,便是说一旦推延时刻到了,该函数就会被调用,它的首要内容如下:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 宿州,nginx的推延关闭,心爱头像46 47 48 49 50 51 52 53 54 55 56 ngx宿州,nginx的推延关闭,心爱头像_http_lingering_close_handler(ngx_event_t *rev) { ssize_t n; ngx_msec_t timer; ngx_connection_t *c; ngx_http_request_t *r; ngx_http_core_loc_conf_t *clcf; u_char buffer[NGX_HTTP_LINGERING_BUFFER_SIZE]; c = rev->data; r = c->data; ngx_log_debug0(NGX_LOG_DEBUG_HTTP, c->log, 0, "http lingering close handler"); if (rev->timedout) { ngx_http_close_request(r, 0); return; } //核算剩下的悉数可用超时时刻 timer = (ngx_msec_t) r->lingering_time - (ngx_msec_t) ngx_time(); //总推延等候昆山杰齿口腔医院时刻现已超越lingering_time了,那么不论怎样样都直接关闭衔接 if ((ngx_msec_int_t) timer <= 0) { ngx_http_close_request(r, 0); return; } do { n = c->recv(c, buffer, NGX_HTTP_LINGERING_BUFFER_SIZE); ngx_log_debug1(NGX_LOG_DEBUG_HTTP, c->log, 0, "lingering read: %z", n); //推延时刻到了,且套接字发作了过错,或许对方关闭了套接字,那么将整个衔接关闭 if (n == NGX_ERROR || n == 0) { ngx_http_close_request(r, 0); return; } } while 宿州,nginx的推延关闭,心爱头像(rev->ready); if (ngx_handle_read_event(rev, 0) != NGX_OK) { ngx_http_close_request(r, 0);单亲公主相亲记 return; } clcf = ngx_http_get_module_loc_conf(r, ngx_http_core_module); timer *= 1000; if (timer > clcf->lingering_timeout) { timer = clcf->lingering_timeout; } //运转到这儿,阐明超时时刻浅说五十种禅定阴魔内客户端发来了数据且还有超时时刻可用,那么来再次注册推延关闭事情,开端下一次的推延关闭等候。 ngx_add_timer(rev, timer); }

上面便是当推延关闭事情超时后 Nginx 的处理进程,首要核算总的推延超时时刻还剩下多少,假如没有了,直接断开衔接,这能够避免『等候-接纳部分数据-等候-接纳部分数据』的无限死循环。接下来 Nginx 测验读取套接字,假如读犯错或许对方关闭了衔接或许仍然没有数据读到,那么 Nginx就将衔接关闭,不然再次注册推延超时事情,开端下一次的推延关闭。依据上面的剖析能够看到,在 Nginx 发送完数据包并进入推延关闭衔接流程后,假如客户端在 lingering_timeout 时刻内没有进行任何操作,那么就会关闭与客户端的衔接然后输出日志,这就会导致导致拜访日志滞后 lingering_timeout 才输出。咱们线上并没有对该参数进行装备,那么会选用默许值,正好是5秒,与实践情况符合。别的假如运用长衔接,Nignx 在恳求完毕后不需求关闭衔接而直接输出日志,那么就不会有这个问题,这也就解说了为什么敞开 Keep-Alive 后问题消失。

复现

知道了问题的原因复现就很简略了,只要在 Nginx 中设置 lingering_timeout 的值,然后调查日志中输出的时刻差是不是发作相应的改动即可。比方将该值设置为7,会发现时刻差为5的日志就消失了,而都变成了时刻差为7的日志:

宿州,nginx的推延关闭,心爱头像
1 2 3 4 5 6 7 [shibing@localhost sbin]$ tail -f ../logs/access.log | grep "request_time=7" 172.17.176.138 - - [17/Nov/2016:18:53:15 +0800] "GET /index.php HTTP/1.1" 200 3450 "-" "-" "-" request_time=7.001 upstream_time=0.000 header_time=0.000 172.17.176.138 - - [17/Nov/2016:18:53:15 +0800] "GET /index.php HTTP/1.1" 200 3450 "-" "-" "-" request_time=7.000 upstream_time=0.000 header_time=0.000 172.17.176.138 - - [17/Nov/2016:18:53:15 +0800] "GET /index.php HTTP/1.1" 200 3450 "-" "-" "-" request_time=7.001 upstream_time=0.001 header_time=0.001 172.17.176.138 - - [17/Nov/2016:18:53:15 +0800] "GET calando/index.php HTTP/1.1" 200 3450 "-" "-" "-" request_time=7.000 upstream_time=0.000 header_time=0.000 172.17.176.138 - - [17/Nov/2016:18:53:15 +0温勇智800] "GET /index.php HTTP/1.1" 200 3450 "-" "-" "-" request_time=7.000 upstream_time=0.000 header_time=0.000 172.17.176.138 - - [17/Nov/2016:18:53:15 +0800] "GET /index.php HTTP/1.1" 200 3450 "-" "-" "-" request_time=7.000 upstream_time=0.000 header_time=0.000

转自:http://sh文昭谈古论今ibing.github.io/2016/11/18/nginx%E7%9A%84%E5%BB%B6%E8%BF%9F%E5%85%B3%E9%97%AD-lingering-close/

客户端 操作系统
声明:该文观念仅代表作者自己,搜狐号系信息发布渠道,搜狐仅供给信息存储空间效劳。