nginx upstream读写超时问题定位
前几天在生产环境发现了一个问题,存在负载的情况下,当upstream服务器reload时(upstream也是一个nginx服务),下面都用nginx代指连接过程中的client,upstream代指连接过程中的server。nginx会报一些error日志,主要的日志有如下三种:
[error] 46478#0: *24814949 upstream timed out (110: Connection timed out) while reading response header from upstream
[error] 46478#0: *24814949 connect() failed (110: Connection timed out) while connecting to upstream
[error] 127715#0: *2830183 no live upstreams while connecting to upstream
nginx配置如下:
upstream backend {
server x.x.x.x weight=1;
server x.x.x.x weight=1;
keepalive 256;
}
http {
default_type application/octet-stream;
keepalive_timeout 300s;
client_header_timeout 5m;
send_timeout 1m;
proxy_request_buffering off;
proxy_buffering off;
proxy_buffer_size 16k;
proxy_headers_hash_max_size 40000;
proxy_headers_hash_bucket_size 128;
proxy_connect_timeout 5s;
subrequest_output_buffer_size 128k;
......
Upstream读超时
即第一种报错,通过 debug
日志以及在nginx中搜索打印错误信息的地方,发现在报错处在 ngx_http_upstream_next
函数:
static void
ngx_http_upstream_next(ngx_http_request_t *r, ngx_http_upstream_t *u,
ngx_uint_t ft_type)
{
ngx_msec_t timeout;
ngx_uint_t status, state;
...
if (ft_type == NGX_HTTP_UPSTREAM_FT_TIMEOUT) {
ngx_log_error(NGX_LOG_ERR, r->connection->log, NGX_ETIMEDOUT,
"upstream timed out");
}
...
}
在此处打断点,用 gdb
打印下调用栈发现调用此函数的地方在 ngx_http_upstream_process_header()
这是nginx处理上游响应头的地方。
static void
ngx_http_upstream_process_header(ngx_http_request_t *r, ngx_http_upstream_t *u)
{
ssize_t n;
ngx_int_t rc;
ngx_connection_t *c;
c = u->peer.connection;
ngx_log_debug0(NGX_LOG_DEBUG_HTTP, c->log, 0,
"http upstream process header");
c->log->action = "reading response header from upstream";
if (c->read->timedout) {
ngx_http_upstream_next(r, u, NGX_HTTP_UPSTREAM_FT_TIMEOUT);
return;
}
c->read->timedout
为 1
代表着读超时,其流程如下:nginx调用 ngx_http_upstream_init_request
进行upstream初始化,在其中会调用 ngx_http_upstream_connect
对上游服务器进行连接,连接成功后会调用 ngx_http_upstream_send_request
发送请求到上游服务器,如果发送成功,会将读事件添加到定时器中,在一定时间内进行读超时事件处理,定时器的超时时间就是 proxy_read_timeout
static void
ngx_http_upstream_send_request(ngx_http_request_t *r, ngx_http_upstream_t *u,
ngx_uint_t do_write)
{
ngx_int_t rc;
ngx_connection_t *c;
c = u->peer.connection;
...
rc = ngx_http_upstream_send_request_body(r, u, do_write);
...
if (!u->request_body_sent) {
u->request_body_sent = 1;
if (u->header_sent) {
return;
}
//这里gdb打印出来的read_timeout值为1s
ngx_add_timer(c->read, u->conf->read_timeout);
...
}
上面定时器的设置的回调处理c->read
是在 ngx_http_upstream_connect
函数中设置的,并且调用 ngx_event_connect_peer
进行实际的连接操作:
void
ngx_http_upstream_connect(ngx_http_request_t *r, ngx_http_upstream_t *u)
{
ngx_int_t rc;
ngx_connection_t *c;
...
rc = ngx_event_connect_peer(&u->peer);
...
//这里设置了连接读、写事件的回调函数
c->write->handler = ngx_http_upstream_handler;
c->read->handler = ngx_http_upstream_handler;
u->write_event_handler = ngx_http_upstream_send_request_handler;
u->read_event_handler = ngx_http_upstream_process_header;
...
}
nginx在 ngx_event_connect_peer
中会将建立连接的 socket
通过 ngx_epoll_add_connection
加入到 epoll
监听中,并注册 EPOLLIN|EPOLLOUT|EPOLLET|EPOLLRDHUP
监听事件:
ngx_int_t
ngx_event_connect_peer(ngx_peer_connection_t *pc)
{
...
//这里的的函数为:ngx_epoll_add_connection
if (ngx_add_conn) {
if (ngx_add_conn(c) == NGX_ERROR) {
goto failed;
}
}
...
}
nginx使用 ngx_event_expire_timers
检查定时器中的所有事件,这就意味着,当u->conf->read_timeout
时间后,nginx会在 ngx_event_expire_timers
中将 c->read
的 timeout
置为1,并调用 c->read->handler
进行处理,即调用 ngx_http_upstream_handler
:
static void
ngx_http_upstream_handler(ngx_event_t *ev)
{
ngx_connection_t *c;
ngx_http_request_t *r;
ngx_http_upstream_t *u;
...
//如果是写事件,则调用ngx_http_upstream_send_request_handler
if (ev->write) {
u->write_event_handler(r, u);
} else {
//如果是读事件,调用ngx_http_upstream_process_header
u->read_event_handler(r, u);
}
...
}
所以,经过以上流程,读事件触发时,会调用 ngx_http_upstream_process_header
处理响应头,当发现已经超时了 epoll
监听中还没有对应的读事件到达,则就报一开始的 upstream timed out
错误。在上面 ngx_http_upstream_send_request
中打印的 u->conf->read_timeout
为1s,这个值有些低,而在nginx.conf中没找到显式的 proxy_read_timeout
定义,根据nginx文档,这个值默认为60s,有些奇怪:
# http://nginx.org/en/docs/http/ngx_http_proxy_module.html#proxy_read_timeout
**Syntax**: proxy_read_timeout time;
**Default**: proxy_read_timeout 60s;
**Context**: http, server, location
最后仔细查找,在nginx.conf include的某个配置文件中找到了 proxy_read_timeout
定义值为1s
Upstream连接超时
第二种报错是在 ngx_http_upstream_test_connect
中报的:
static ngx_int_t
ngx_http_upstream_test_connect(ngx_connection_t *c)
{
int err;
socklen_t len;
...
/*
* BSDs and Linux return 0 and set a pending error in err
* Solaris returns -1 and sets errno
*/
if (getsockopt(c->fd, SOL_SOCKET, SO_ERROR, (void *) &err, &len)
== -1)
{
err = ngx_socket_errno;
}
**//这里打印发现err为110**
if (err) {
c->log->action = "connecting to upstream";
(void) ngx_connection_error(c, err, "connect() failed");
return NGX_ERROR;
}
}
经过 gdb
打印调用栈发现其是在 ngx_http_upstream_send_request
中调用的,是在向上游服务器发送请求:
static void
ngx_http_upstream_send_request(ngx_http_request_t *r, ngx_http_upstream_t *u,
ngx_uint_t do_write)
{
ngx_int_t rc;
ngx_connection_t *c;
c = u->peer.connection;
...
if (!u->request_sent && ngx_http_upstream_test_connect(c) != NGX_OK) {
ngx_http_upstream_next(r, u, NGX_HTTP_UPSTREAM_FT_ERROR);
return;
}
...
}
这里的过程是这样的:在upstream完成和上游的连接后(在 ngx_http_upstream_connect
中调用完 ngx_event_connect_peer
),会将写事件添加到定时器中,超时时间就是nginx.conf中 proxy_connect_timeout
配置项:
void
ngx_http_upstream_connect(ngx_http_request_t *r, ngx_http_upstream_t *u)
{
...
rc = ngx_event_connect_peer(&u->peer);
...
if (rc == NGX_AGAIN) {
ngx_add_timer(c->write, u->conf->connect_timeout);
return;
}
...
}
这里值得一提的是,对于非阻塞的 connect()
系统调用,返回 -1
并不必然代表连接失败,主要看设置的 errno
,如果为 EINPROGRESS
,则表示连接还在进行中,才有后续添加写超时定时器的操作。
ngx_int_t
ngx_event_connect_peer(ngx_peer_connection_t *pc)
{
...
if (rc == -1) {
err = ngx_socket_errno;
if (err != NGX_EINPROGRESS
...
return NGX_DECLINED
}
}
//errno为EINPROGRESS
if (ngx_add_conn) {
if (rc == -1) {
/* NGX_EINPROGRESS */
return NGX_AGAIN;
}
...
}
...
}
我们回到定位问题的主线,上面在 ngx_http_upstream_test_connect()
中打印 socket
的 errno
为 110
,我们在 /usr/include/asm-generic/errno.h
中我们可以看到完整的 errno
定义:
...
#define ETIMEDOUT 110 /* Connection timed out */
#define ECONNREFUSED 111 /* Connection refused */
...
#define EINPROGRESS 115 /* Operation now in progress */
...
这里可以看到 110
代表着连接超时,我这里以为是nginx配置中的 proxy_connect_timeout
时间配置的太短了,导致触发了写超时事件,于是调大了这个值,但是发现没有用,依旧会报同样的错误。这里就需要审视一下, getsockopt(c->fd, SOL_SOCKET, SO_ERROR, (void *) &err, &len)
这个操作到底是在获取什么错误。看下 connect
的 man page
这里说了什么:
EINPROGRESS
The socket is nonblocking and the connection cannot be completed immediately.
It is possible to select(2) or poll(2) for completion by selecting the socket
for writing. After select(2) indicates writability, use getsockopt(2) to read
the SO_ERROR option at level SOL_SOCKET to determine whether connect() completed successfully
(SO_ERROR is zero) or unsuccessfully (SO_ERROR is one of the usual error codes listed
here, explaining the reason for the failure).
这里的意思就是,对于非阻塞的 connect
操作,在可写事件触发后,还需要调用 getsockopt
检查原本的 socket
有没有出现错误,这一点决定是连接是否建立成功。这里的关键点是,这个连接操作是由操作系统内核完成的,不在nginx参数调控范围内,所以调大 proxy_connect_timeout
对此无效。
于是想到复现问题并且 tcpdump
抓包看下握手期间发生了什么:
找到一个报错的连接,握手过程如下
-
nginx向upstream发送一个 SYN
,并且等待upstream的ACK
回复(就是调用connect()
) -
upstream收到了nginx的 SYN
,但是没有回复 -
1s后,nginx又向upstream发送了一个 SYN
,等待回复,这个时候已经触发了tcp的超时重传 -
upstream收到了重传的 SYN
,但是依旧没有回复
连接在此就断开了,nginx没有继续尝试重连,然后打印出来 connect() failed
错误日志
这里有两个问题
-
upstream在reload期间,无法建立新的连接,导致连接阻塞 -
nginx等待1s后,重传 SYN
包进行尝试连接,但只尝试了一次
对于第一个问题,在upstream reload时,立即在nginx上访问upstream服务并记录时间,发现upstream reload成功到能够建立新的连接,大概是6秒左右:
date; curl x.x.x.x; date;
之所以在 reload期间不能建立新的连接,是因为nginx reload的过程是建立新的worker进程,这个时候老的worker进程不再接受新连接,等新的worker建立成功后才能建立新的连接,当处理完老的连接后,老的woker也就退出了。这里upstream reload时间过长,也是一个需要解决的问题,但是是另一类问题了。
对于第二个问题,重传次数其实是由内核参数 net.ipv4.tcp_syn_retries
规定的,查询下nginx服务器上这个参数的配置: sysctl -a | grep net.ipv4.tcp_syn_retries
,发现值为1,这和抓包看到的现象是一致的。这个值需要改大一些,改成多少合适呢,需要做下实验,找一个访问不通的ip,比如 www.google.com
的服务器
# tcp_syn_retries为1时是3s左右
$ date; curl 185.45.7.189; date
Wed Apr 13 13:44:40 CST 2022
curl: (7) Failed connect to 185.45.7.189:80; Connection timed out
Wed Apr 13 13:44:43 CST 2022
# tcp_syn_retries为2时是8s左右
$ date; curl 185.45.7.189; date
Wed Apr 13 13:45:47 CST 2022
curl: (7) Failed connect to 185.45.7.189:80; Connection timed out
Wed Apr 13 13:45:55 CST 2022
# tcp_syn_retries为3时是15s左右
date; curl 185.45.7.189; date
Wed Apr 13 13:46:54 CST 2022
curl: (7) Failed connect to 185.45.7.189:80; Connection timed out
Wed Apr 13 13:47:09 CST 2022
......
实验的结果表示, tcp_syn_retries
设置成2或3都可以,于是把这个值改成2试试: sysctl net.ipv4.tcp_syn_retries=2
,修改后测试发现依旧会出现连接超时的问题,需要注意,这里还需要把 proxy_connect_timeout
调大,这样才能避免,在触发内核重传阈值之前触发nginx的写超时事件。
no live upstreams
这个错误是在连接upstream的时候报的:
void
ngx_http_upstream_connect(ngx_http_request_t *r, ngx_http_upstream_t *u)
{
...
rc = ngx_event_connect_peer(&u->peer);
...
if (rc == NGX_BUSY) {
ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, "no live upstreams");
ngx_http_upstream_next(r, u, NGX_HTTP_UPSTREAM_FT_NOLIVE);
return;
}
}
可以看到,当 rc
为 NGX_BUSY
时,会报这个错误, NGX_BUSY
实际上是在 ngx_peer_connection_t *pc
尝试选择一个upstream时,发现无有效的upstream。这里调用 pc->get()
,这个 get
是一个函数指针,其指向的值是在 ngx_http_upstream_init_round_robin_peer
中设置的,而后者是在初始化upstream时,在 ngx_http_upstream_init_request
中调用的。
ngx_int_t
ngx_event_connect_peer(ngx_peer_connection_t *pc)
{
...
//这里的get方法是ngx_http_upstream_get_round_robin_peer,即轮询
rc = pc->get(pc, pc->data);
if (rc != NGX_OK) {
return rc;
}
...
}
ngx_int_t
ngx_http_upstream_init_round_robin_peer(ngx_http_request_t *r,
ngx_http_upstream_srv_conf_t *us)
{
ngx_uint_t n;
ngx_http_upstream_rr_peer_data_t *rrp;
...
r->upstream->peer.get = ngx_http_upstream_get_round_robin_peer;
r->upstream->peer.free = ngx_http_upstream_free_round_robin_peer;
r->upstream->peer.tries = ngx_http_upstream_tries(rrp->peers);
...
}
ngx_int_t
ngx_http_upstream_get_round_robin_peer(ngx_peer_connection_t *pc, void *data)
{
...
peer = ngx_http_upstream_get_peer(rrp);
if (peer == NULL) {
goto failed;
}
...
failed:
...
return NGX_BUSY;
}
nginx负载均衡的具体逻辑这里我们不深究了,总之原因和上面第二种错误相关,两个upstream会几乎同时reload,这个时候nginx发现无有效可用upstream,就会报 no live upstream
那个错误。
总结
-
upstream timed out while reading...
这种错误的直接原因是proxy_read_timeout
配置设置太小 -
connect() failed (110: Connection timed out) while connecting to upstream
这种错误的直接原因是内核参数net.ipv4.tcp_syn_retries
太小、没能在proxy_connect_timeout
时间内建立起有效连接 -
no live upstream
这种错误的直接原因和第二种相关,是负载均衡发现无可用upstream导致的 -
上面所有错误的更近一层的原因是upstream reload时间过长,导致upstream太长时间不能建立新连接