一、故障
基本架构如图所示,客户端发起 http 请求给 nginx,nginx 转发请求给网关,网关再转发请求到后端微服务。
故障现象是,每隔十几分钟或者几个小时不等,客户端就会得到一个或者连续多个请求超时错误。查看 nginx 日志,对应请求返回 499;查看网关日志,没有收到对应的请求。
从日志分析,问题应该处在 nginx 或者 spring-cloud-gateway 上。
nginx 版本:1.14.2,spring-cloud 版本:Greenwich.RC2。
nginx 主要配置如下:
[root@wh-hlwzxtest1 conf]# cat nginx.conf
worker_processes 8;
events {
use epoll;
worker_connections 10240;
}
http {
include mime.types;
default_type application/octet-stream;
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
#gzip on;
upstream dbg2 {
server 10.201.0.27:8888;
keepalive 100;
}
server {
listen 80;
server_name localhost;
charset utf-8;
location /dbg2/ {
proxy_pass http://dbg2/;
proxy_http_version 1.1;
proxy_set_header Connection "";
}
}
}
为了提高性能,nginx 发送给网关的请求为 http 1.1,可以复用 tcp 连接。
二、排查
1、查看 tcp 连接
[[email protected] logs]# ss -n | grep 10.201.0.27:8888
tcp ESTAB 0 0 10.197.0.38:36674 10.201.0.27:8888
tcp ESTAB 0 0 10.197.0.38:40106 10.201.0.27:8888
[[email protected] opt]# ss -n | grep 10.197.0.38
tcp ESTAB 0 0 ::ffff:10.201.0.27:8888 ::ffff:10.197.0.38:40106
tcp ESTAB 0 0 ::ffff:10.201.0.27:8888 ::ffff:10.197.0.38:39266
可以看到 nginx 和网关之间建立的 socket 连接为 (10.201.0.27:8888,10.197.0.38:40106),另外的 2 条记录就很可疑了。猜测原因是:一端异常关闭了 tcp 连接却没有通知对端,或者通知了对端但对端没有收到。
2、抓包分析
先看下 nginx 的抓包数据:
序号 8403:转发 http 请求给网关;
序号 8404:在 RTT 时间内没有收到 ack 包,重发报文;
序号 8505:RTT 约等于 0.2s,tcp 重传;
序号 8506:0.4s 没收到 ack 包,tcp 重传;
序号 8507:0.8s 没收到 ack 包,tcp 重传;
序号 8509:1.6s 没收到 ack 包,tcp 重传;
...
序号8439:28.1s(128RTT)没收到 ack 包,tcp 重传。
序号 8408:请求设置了超时时间为 3s,因此发送 FIN 包。
再看下网关的抓包数据:
序号 1372:17:24:31 收到了 nginx 发过来的 ack 确认包,对应 nginx 抓包图中的序号 1348(nginx 那台服务器时间快了差不多 1 分 30 秒);
序号 4221:2 小时后,发送 tcp keep-alive 心跳报文,(从 nginx 抓包图中也可以看出这 2 小时之内该 tcp 连接空闲);
序号 4253:75s 后再次发送 tcp keep-alive 心跳;
序号 4275:75s 后再次发送心跳;
连续 9 次;
序号 4489:发送 RST 包,通过对端重置连接。
2 小时,75s, 9 次,系统默认设置。
[root@eureka2 opt]# cat /proc/sys/net/ipv4/tcp_keepalive_time
7200
[root@eureka2 opt]# cat /proc/sys/net/ipv4/tcp_keepalive_intvl
75
[root@eureka2 opt]# cat /proc/sys/net/ipv4/tcp_keepalive_probes
9
具体这几个参数的作用,参考文章:为什么基于TCP的应用需要心跳包
3、分析
通过以上抓包分析,基本确认了问题出在 nginx 上。19:25 时,网关发送 tcp keep-alive 心跳包给 nginx 那台服务器,此时那台服务器上保留着该 tcp 连接,却没有回应;22:20 时,nginx 发送 http 请求给网关,而网关已经关闭该 tcp 连接,因此没有应答。
三、解决
1、proxy_send_timeout
nginx 中与 upstream 相关的超时配置主要有如下参数,参考:Nginx的超时timeout配置详解
proxy_connect_timeout:nginx 与 upstream server 的连接超时时间;proxy_read_timeout:nginx 接收 upstream server 数据超时, 默认 60s, 如果连续的 60s 内没有收到 1 个字节, 连接关闭;
proxy_send_timeout:nginx 发送数据至 upstream server 超时, 默认 60s, 如果连续的 60s 内没有发送 1 个字节, 连接关闭。
这几个参数,都是针对 http 协议层面的。比如 proxy_send_timeout = 60s,并不是指如果 60s 没有发送 http 请求,就关闭连接;而是指发送 http 请求后,在两次 write 操作期间,如果超过 60s,就关闭连接。所以这几个参数,显然不是我们需要的。
2、upstream 模块的 keepalive_timeout 参数
查看官网文档,Module ngx_http_upstream_module,
Syntax: keepalive_timeout timeout;
Default:
keepalive_timeout 60s;
Context: upstream
This directive appeared in version 1.15.3.
Sets a timeout during which an idle keepalive connection to an upstream server will stay open.
设置 tcp 连接空闲时间超过 60s 后关闭,这正是我们需要的。
为了使用该参数,升级 nginx 版本到 1.15.8,配置文件如下:
http {
upstream dbg2 {
server 10.201.0.27:8888;
keepalive 100;
keepalive_requests 30000;
keepalive_timeout 300s;
}
...
}
设置 tcp 连接上跑了 30000 个 http 请求或者空闲 300s,那么就关闭连接。
之后继续测试,没有发现丢包。
序号 938:空闲 5 分钟后,nginx 主动发起 FIN 报文,关闭连接。