背景
部门进行统一收口鉴权接口,各个业务集群拿到cookie后,通过调用服务方的内网接口鉴权拿到用户的信息。但是上线后通过观察http请求异常日志,发现请求鉴权接口的时候会产生大量请求超时。
基于上述现象,作为业务开发同学在没有其他网络设备权限的前提下,进行问题的排查剖析。
线上报警
在沙箱环境切换了鉴权接口,并测试无问题后,进行了代码上线。但是在上线该鉴权接口改动后,日志爆出了大量http异常,如下所示
json
[
{
"serviceName": "http:\/\/xxauth.xxxcorp.com",
"errMsg": "Request timeout! the server hasn't responded over the timeout setting",
"cli": {
"errCode": 110,
"errMsg": "Connection timed out",
"connected": false,
"host": "",
"port": 0,
"ssl": false,
"setting": {
"timeout": 1
},
"requestMethod": "GET",
"requestHeaders": {
"authCookieName": "dxxxxxdffd",
"port": 8787
},
"createTime": 1692597687
}
}
]
由日志中根下的errMsg可以了解到是响应超时,服务端没有在设置的timeout时间范围内返回响应。
但是cli.errMsg提示的是连接超时,那么这个超时到底是发生在「建立连接」初始阶段,还是连接完成后的「传输数据」阶段呢。
这个提示让人感觉有点迷惑,那么先从客户端进行分析。
线索收集
整体流程主体如图所示,对每个环节进行逐一排查。
客户端
代码分析
代码中根据域名构建了一个连接池,每次请求前要从池中拿出一个链接,拿出前会检查链接是否可用,若不可用则重建创立一个链接。
默认使用的是http1.1协议,即长连接。
所以得知请求的链接一定是保证可用的,代码中暂时看来不存在问题。
tcpdump抓包
随机登录线上集群的一台报异常的机器后,执行tcpdump抓包命令进行抓包,抓取eth0网卡上 host为xxauth.xxxcorp.com的流量,并写入到auth文件当中。
bash
[work]$ sudo tcpdump -i eth0 -n host xxauth.xxxcorp.com -w auth.pcap
在机器上执行该命令后,另一边同时观察http的异常日志中,该ip地址是否产生了请求超时日志,待收集一定数量的超时日志之后,停止抓包。
csharp
[work]$ sz auth.pcap
wireshark报文分析
将抓包文件下载到本地,使用wireshark打开抓包文件,通过点击左下角的统计功能看一下请求的情况分布。

重点先关注黄色的warning,有47个链接重置报文,选择一个靠后相对完整的RST报文查看。
双击后报文列表中定位到该条报文的位置,然后右键->追踪流->TCP流追踪,过滤出来完整的记录后,可以看到三次握手是成功的,但是在请求了几次后产生了RST报文。

按照时间线(相对时间)依次阐述:
- No.202号报文:绿色对钩的位置客户端向服务端发起了GET请求
- No.212号报文:间隔41ms后,server先回复了ack
- No.269号报文:由于超过了设听的timeou时间1s,客户端主动向服务端发送了FIN报文
- No.276号报文:server对client恢复了ACK,表示收到了FIN报文。但它还有数据要传输,所以未回复FIN报文。
- No.296号报文:server向client进行了响应,但是由于已经超时client回复了RST报文
- No.298号报文:server向client回复了FIN报文,但是由于已经超时client回复了RST报文
发送RST报文属于非正常关闭连接的情况,而上图中的异常情况就是响应时长超过了设置的timeout时间。
引起关注的点是,客户端发送了GET请求以后,过了41ms左右仅仅收到接入层Nginx的ACK报文,并没有http的响应一起回来。再翻看抓包文件中,其他超时的GET请求和对应回复ACK的差值时间,基本都是40ms多一点。
观察这个流之前的一个事务时间基本都在10ms之内,正常情况不可能这么长时间就只回复一个ACK。难道是服务端处理耗时长 ,或者发生了网络拥堵吗?
但是这么有规律的时间间隔,一定是触发了某种规则。
这需要先问一个问题:为什么有时ACK会和响应报文一起回来,有时候会单独发包回来呢?
没错,其实就是TCP的「延迟确认机制」。
由于延迟确认机制,不会马上回复ACK,而是将它放入缓冲区中等待一段时间看有没有可以一同发送的报文,如果没有则会在达到特定「窗口时间」后发送出去。 而之前的响应都在窗口时间内写入了「缓冲区」,所以和ack报文一同发送了出去。这次由于在「窗口时间」内没有收到响应报文,所以将ACK报文单独发包返回了。
那么应该怎么验证猜测,这个窗口时间是多少呢?其实都定义在操作系统的TCP代码中了,我们先查看操作系统的版本号
shell
[work~]$ cat /proc/version
然后根据版本号查看对应的LINUX源码,搜索TCP_DELACK_MIN,就可以看到定义的最小等待窗口时间为HZ/25。HZ默认为1000,但是可以在编译内核的时候修改。
C
#define TCP_DELACK_MIN ((unsigned)(HZ/25)) /* minimal time to delay before sending an ACK */
关于HZ的定义,详细可以参考这篇文章
我理解就是定义1s内有多少个时钟中断。
通过下述命令来查看我们内核实际的HZ值,可以看到是1000,即1ms一个时钟中断。那么HZ/25=40ms,所以延迟机制确认的最小时间记为40ms,刚好和我们抓取的报文中的40ms对应上了。
ini
[work ~]$ zcat /proc/config.gz | grep CONFIG_HZ
CONFIG_HZ=1000
得出结论:前半段的网络传输没有问题。
客户端发送给接入层的包非常快就抵达了,接入层内核将ack报文写入发送缓冲区,然后等待可以一起返回的包,但是直至达到了40ms这个窗口时间也没等到,自己返回给了客户端。
小结
站在业务客户端的视角,到这里大概已经清楚问题了。客户端与接入层报文交互正常,问题应该存在于接入层与服务端之间。
中间环节
接入层日志
登录公司sre日志查询平台,根据「域名+499状态码」进行查询所有经过接入层的请求日志。
看到的确是有大量的499状态日志产生,响应时间也的确超出了客户端设置的1s。
后端服务状态码和响应字节数这两列为空,和运维的同学咨询后得知,499这种情况即使上游服务返回了,也不会记录返回的状态码和响应内容。
那么暂且就关注「请求时间」和「后端响应时间」这两项数据,官方文档的定义如下
请求时间 (request_time)
request processing time in seconds with a milliseconds resolution; time elapsed between the first bytes were read from the client and the log write after the last bytes were sent to the client
即,request_time =「nginx收到client对响应包的ack时间点」-「nginx接收到客户端的第一个http请求的tcp包的时间点」
包括:
- 将请求完整接受完成的时间
- 反向代理节点内部执行逻辑的时间
- upstream_response_time的时间
后端响应时间 (upstream_response_time)
keeps time spent on receiving the response from the upstream server; the time is kept in seconds with millisecond resolution. Times of several responses are separated by commas and colons like addresses in the $upstream_addr variable.
即,upstream_response_time =「收到上游完整响应的时间点」 - 「与上游开始建立连接时间点」
包括:
- 建立连接时间
- 上游处理时间
- 上游将完整报文发送到nginx时间
所以从这份日志能确认的是upstream_response_time时间确实过长,导致了整体的响应时间过长,至于是这三个阶段哪个耗时过长,还无法确定。
网络路径状况测试
在产生超时日志的client机器上,用mtr命令看下到对应的后端serverIp请求链路的网络稳定情况
bash
[work]$ mtr *.*.*.199
Host Loss% Snt Last Avg Best Wrst StDev
1. (waiting for reply)
2. *.*.*.153 1.0% 592 25.9 11.5 5.8 56.5 8.6
3. *.*.*.93 0.0% 592 9.6 10.1 6.6 36.1 2.4
4. *.*.*.245 0.0% 592 9.3 10.1 5.1 35.4 3.2
5. *.*.*.147 0.0% 592 10.5 12.6 9.3 19.0 2.1
6. *.*.*.77 0.0% 592 12.7 12.5 7.2 19.3 2.0
7. *.*.*.1 0.0% 592 14.6 12.3 6.5 18.7 2.0
8. (waiting for reply)
9. *.*.*.199 0.0% 592 16.1 14.9 9.5 23.8 2.4
通过结果可以看出来,虽然第二个节点上存在一定的丢包率,但是在最终的节点上丢包率是0,整体网络路径状况是稳定。
服务端
从异常日志中,找出几条请求超时的日志提供给服务开发同学
请求接口的时候存在authCookieName这样一个用户登录凭据,服务端同学可以根据这个标识对日志进行过滤。
json
{
"requestHeaders": {
"authCookieName": "dxxxxxdffd",
"port": 8787
}
}
tomcat日志
然后通过时间戳查看Tomcat日志查询,code都是「200」并且都处理时间都没有超时,响应时间大概在几ms~几十ms不等。
bash
127.0.0.1 - [17:27:18 +0800] 'GET /zufang/auth/getLoginBrokerIdByCookie' 0.002 200 81 '' 'okhttp/3.11.0' 'client ip'
127.0.0.1 - [17:27:18 +0800] 'GET /auth/getLoginBrokerIdByCookie' 0.002 200 81 '-' '-' 'client ip'
127.0.0.1 - [17:27:18 +0800] 'GET /auth/getLoginBrokerIdByCookie' 0.001 200 81 '-' 'okhttp/4.2.2' 'client ip'
这时候想到「499」是NGINX标准里定义响应的时候客户端断开连接记录的响应code,那么在tomcat中是如何定义这种情况的code的呢?
网上查阅了一下文章后,我理解即使客户端断开连接,tomcat依然会继续处理请求,然后返回响应。
而当它响应的时候客户端断开了链接的情况,我猜测tomcat依然是将code记录为200,而不是像NGINX一样记录类似499这种异常code。
了解tomcat的同学希望能指证下是不是这样
GC日志
服务端同学再排查了GC日志,并没有频繁的进行GC。
业务日志
服务端同学根据http请求报文中的票据进行了业务日志查询,处理时间基本都是几毫秒。
下面是业务处理日志的时间:

机器负载
查看处理问题请求的机器负载情况,在15.03的时候cpu指标有一个飙升,但是在排查问题的17.26时,各项指标的负载情况并不算高。
网卡的流量也不算高,每秒几十kb
看集群的整体请求量,没有突然飙升

单机的线程池数量远超过接口的QPS,看起来不存在请求打到tomcat后,阻塞在处理队列中的情况

小结
那么现在看来,client请求整体超时,通过日志和负载猜测服务端的处理未超时。
分析与尝试
已知情况
- 客户端:可以正常建立TCP连接,没有异常重发报文。
- 服务端:处理请求在十毫秒以内,不存在处理超时日志,机器负载不高。
- 网络链路:链接稳定成功建立,不存在丢包情况。
通过上述情况归纳:链接全部正常建立,网络环境正常,服务端能够正常快速响应。
包本身
通过抓包文件可以看出,响应报文大小全都在560左右远没有到一个MSS。
之前的报文都可以正常传输,所以我理解不存在由于包过大导致产生了以下异常情况
- 可能超过了中间设备的MTU值被拦截(或包大小本身在阈值边缘,通过隧道时增加报文大小导致超过阈值)
- 由于拆包BUG导致的引起乱序、丢包的问题。
估算时间分布
这里本来想根据客户端发包的时间和服务端业务日志的时间来做一个差值,从而计算出请求从客户端出发到服务端花了多长时间。
过滤出一个问题请求后,观察抓包文件倒数第四个报文3014 号,业务端收到响应时间:27:18.541
再看服务端的业务日志可以看到,服务端开始处理时间:27:18.544
意识到客户端和服务端两台机器的时钟是有差值的,两者都是以本地时钟为准记录,而时间误差在20ms内都是NTP时间服务的正常范围内,所以根据现有线索是没法确定传输花费的时间的。
如果当时服务端可以抓包的话,就可以双端抓包,然后通过过滤条件来定位同一个请求。
方式是在wireshark中添加http过滤条件,例如我情况中的authCookieName这样一个登录票据,即可过滤出问题请求在客户端和服务侧的问题发包收包记录。
http contains "TT=dxxxxxdffd"
然后通过查看服务端和接入层nginx的交互时间,来估算服务端收到的时间和客户端发包时间的差值。
总体事务时间 = 客户端GET请求时间 - 收到响应时间
服务端的事务时间 = 后端收到接入层NGINX GET请求时间 - 后端返回给接入层NGINX响应的时间
通过得出两个事务时间,即可知道时间是损耗在哪部分。
但是由于没有服务端的集群权限,无法服务端抓包,没有办法估算时间在路径上的分布。
网络传输过慢
整体的链路
「前半段」已经从client的抓包部分分析过了,不存在问题。再来看「后半段」的流程
server机器处理的速度在前面已经确认速度很快,线程数够用,机器的各项负载也不高。而内部网络一般情况传输速度很快,丢包率也是非常低。
而nginx接收完整的http报文后,进行解析,然后根据配置规则生成新的http报文后,建立新的连接或通过已有的链接转发给server端。Nginx自身是一个高性能的服务,请求的并发数按理来说远远不及它的瓶颈。
那么到底什么原因呢,难道问题发生在接入层与上游server的收包&发包上吗?
接入层Nginx
这时突然注意到服务方同学提供的域名为xxxcorp.com,这个域名是公司可以外部访问的办公网域名,例如oa、工单审核等内部使用的web站点域名。
使用这个域名一般都是用于内部使用,需要通过oa鉴权或秘钥后才能使用,那么问题会不会出现在接入层节点的处理逻辑,或者本身的负载上呢?
首先让服务端同学为集群申请了内网接口专用域名xxdns.org,然后我的客户端集群灰度了一台机器使用该域名,持续观察http异常日志,该灰度机器再未发现超时。
快速将剩余机器全量上线,http请求异常日志消失,至此问题解决。
联系了运维同学确认这个域名的LB是否存在一些特殊的逻辑,运维同学表示额外多出来的流程是:会判断ip是不是内网,如果不是的话会走鉴权逻辑,是的话则继续向下执行。
但是这是在location之前的逻辑,也就是建立链接前,如果是这样的话upstream_response_time统计出来的时间这么高就说不通了。除非问题还是发生在建立连接发包,和收包的阶段。
但是切换域名后确实问题现象就消失了,具体这个接入层是怎么导致发生这个问题的没法考证了。
总结
在线上遇到请求接口超时的情况,排查的地方也就三个
- 发送方
- 网络链路中段
- 服务方
首先,确定是不是自身的问题,从应用代码层面分析,然后通过客户端抓包。
然后,排查服务端的服务器日志和GC日志,看是否是由于服务端处理逻辑导致的超时。如果不是逻辑导致的问题,再进行服务端抓包,通过双侧抓包过滤出有问题的报文,大概率就能看出来问题所在。
最后,再进行网络链路中段的排查,看是否存在传输上的问题。
案例中是通过变更接入层反向代理,解决了大量接口响应超时的问题。
由于作为业务开发,当时中间的机器和服务端没有权限进行抓包排查,没有确定到实际具体的原因,实在可惜。
后记:
后续再在线上机器切换回corp域名,也没有再复现出来请求超时的例子。 如果有老哥也遇到过这种情况,或者有什么猜测,希望能分享一下。