关于java:客户端请求服务端莫名出现超时

31次阅读

共计 4110 个字符,预计需要花费 11 分钟才能阅读完成。

背景:
客户端申请服务端域名,进行查问操作,偶然呈现 200ms+ 提早,且对于该申请服务端执行速度 4ms 左右,此问题十分诡异,决定对此进行钻研. 上面记录下过后具体的定位 & 解决流程
问题定位:
1、剖析代码
服务端零碎是一个常见的 spring-boot web 工程,应用了集成的 tomcat。剖析了代码之后,发现并没有非凡的中央,没有非凡的过滤器或者拦截器,所以初步排除业务代码问题.
2、确定调用链路

3、问题排查剖析
3.1、网络层问题,抓包 (20210701)
确定耗时地位是 客户端调用 F5,还是 F5 调用 服务端?
3.1.1 服务端 ack 呈现大量 RST

通过跟踪 RST 的 HTTP 流 发现:在 FIN 最初一次敞开之后 , 服务端向客户端发送了 FIN 敞开连贯的申请,而后服务端又向客户端发了报文导致 RST. 抓包如下:

狐疑是连贯不能复用导致的建联耗时,经排查代码发现,客户端未应用连接池,因而 客户端减少 HTTP 连接池来解决此类问题 .
3.1.2 网络耗时长,抓包
通过 2.1.1 的优化之后,还是存在调用超时的景象,当初咱们通过 wireshark 抓包察看,发现客户端与 F5 之间无超时状况,客户端发出请求后,随机收到了 F5 的 ACK;F5 发出请求后,距离 40ms 收到服务端的 ack , 此处就引出了另外一个问题,就是 TCP 神奇的 40 ms (https://cloud.tencent.com/dev… 大家能够自行去备注的链接去研读,这里篇幅工夫无限,不在进行赘述 .
3.2、网络层无稳定,思考应用层
开始思考服务端 socket 连贯 和 服务端容器方向
3.2.1. 服务器 socket 连贯
跳过 F5 负载,间接应用 IP:port 的形式申请服务端(20210713)
20210714 察看日志:

客户端发动申请: 2021-07-14 14:05:02.670
服务端收到申请: 2021-07-14 14:05:02.939
服务端返回后果: 2021-07-14 14:05:03.280
客户端收到返回: 2021-07-14 14:05:03.280
客户端整体申请耗时: 610ms 服务端程序耗时: 546ms
对服务申请抓包进行剖析(d1134184-2a52-4915-8e63-7f32ed6d274d)

呈现大量乱序重传,临时狐疑是抓包工具镜像提早导致, 此问题待 应用 tcpdump 抓包后给出论断.
20210716 tcpdump 抓包
确认了以上问题是抓包工具导致 .
3.2.2. 服务端容器
服务端应用的容器是 tomcat 8.5, 内存是 NIO 模型,不存在阻塞状况。对于 tomcat NIO 来说,是由一系列框架类和数据读写类来组成的,同时这些类运行在不同的线程中,独特维持整个 tomcat NIO 架构。包含原始 socket 监听的 acceptor 线程,监测注册在原始 scoket 上的事件是否产生的 poller thread 事件线程,进行数据读写和运行 servlet API 的 tomcat io 线程。当数据须要屡次读写的时候,监测注册在原始 scoket 上的读写事件的 block poller 事件线程。即 NIO 整体架构运行着 4 种线程:
Acceptor 线程
Poller 线程
Tomcat IO 线程
BlockPoller 线程
这里要留神:
tomcat 有一个 acceptor 线程来 accept socket 连贯,而后有工作线程来进行业务解决。对于 client 端的一个申请进来,流程是这样的:tcp 的三次握手建设连贯,建设连贯的过程中,OS 保护了半连贯队列 (syn 队列) 以及齐全连贯队列 (accept 队列),在第三次握手之后,server 收到了 client 的 ack,则进入 establish 的状态,而后该连贯由 syn 队列挪动到 accept 队列。tomcat 的 acceptor 线程则负责从 accept 队列中取出该 connection,承受该 connection,而后交给工作线程去解决(读取申请参数、解决逻辑、返回响应等等;如果该连贯不是 keep alived 的话,则敞开该连贯,而后该工作线程开释回线程池,如果是 keep alived 的话,则期待下一个数据包的到来直到 keepAliveTimeout,而后敞开该连贯开释回线程池),而后本人接着去 accept 队列取 connection(当以后 socket 连贯超过 maxConnections 的时候,acceptor 线程本人会阻塞期待,等连贯降下去之后,才去解决 accept 队列的下一个连贯)。acceptCount 指的就是这个 accept 队列的大小。
具体 tomcat 的线程的介绍: https://segmentfault.com/a/11…

通过对服务端代码审查发现,未曾配置过 tomcat 线程相干参数,即始终应用默认值
3.2.2.1 调整原服务容器参数(20210708)
这里狐疑服务端线程设置过小,始终在创立新的线程上耗费工夫,所以对服务端配置进行调整,调整如下:
server:
port: ${port:8080}
tomcat:

max-threads: 1000
min-spare-threads: 500
accept-count: 1000
max-connections: 10000

connection-timeout: 300000
投产后发现,客户端仍存在原来的状况,此次调整失败.
3.2.2.2 下线服务端 A 服务器 仅供客户端 IP 调用 (20210716)
狐疑 accept-count 设置过大,导致申请始终在队列中,然而还未满 1000,不会创立新的线程导致。20210716 摘除服务端的 A 服务器,进入察看

本次调用 耗时 137ms
抓包:
客户端发送申请 : 2021-07-16 14:10:11.600


服务端接管申请在 14:10:11.602897

申请在服务端容器耗时
为了验证上述判断,咱们打印一下 tomcat 的 access_log 日志,并放弃服务端 A 服务器对外下线状态(20210720)
20210721 察看发现,
tomcat 容器收到申请: 2021-07-21 14:18:14.872
servlet 开始执行: 2021-07-21 14:18:15.014

3.2.2.3 看见光明,降级 tomcat 容器
本次定位问题为 tomcat 容器导致. 然而到底问题是在 tomcat 的什么中央呢?咱们持续往下找
首先这里咱们查了下客户端的申请量,每分钟不到 1000,临时不思考 tomcat 队列积压的问题
而后,这里打算寄出咱们的神器了,关上 debug 日志, 进行察看,看看会不会有惊喜(其实这里曾经狐疑 springBoot 对 tomcat 的反对不是那么的敌对)
过不期然,再关上了 debug 日志之后,咱们发现了这样的一个问题

![上传中 …]()

之后,咱们定位源代码,找到了 OrderedRequestContextFilter。
咱们查阅了网上的材料后(https://segmentfault.com/a/11…

在认真研读了以上问题后,咱们开始对咱们的服务进行了“监督”:

对客户端进行 arthas 的 trace 之后![上传中 …]()

这行代码加载了 31 次,一共耗时 140ms. 这里的景象简直和帖子上的统一
后续咱们剖析了服务端的包构造,果然存在如下 jar 包

到这里,咱们决定不再进行 trace, 间接降级 tomcat 版本到 8.5.40
降级办法如下:
compile(‘org.springframework.boot:spring-boot-starter-web’) {

    exclude module: "spring-boot-starter-tomcat"
}
compile 'org.apache.tomcat.embed:tomcat-embed-core:8.5.40'
compile 'org.apache.tomcat.embed:tomcat-embed-el:8.5.40'

// compile ‘org.apache.tomcat.embed:tomcat-embed-logging-juli:+’

compile 'org.apache.tomcat.embed:tomcat-embed-logging-juli:8.0.37'
compile 'org.apache.tomcat.embed:tomcat-embed-websocket:8.5.40'

降级于 20210722 上线部署
20210723 察看日志如下:

察看一天,调用耗时均有明显降低,但依然偶有工夫长 (超过 10ms) 景象 , 日志剖析如下:

本次耗时不在容器层,为网络稳定失常导致.

3.2.2.4 更换容器为 undertow 容器
3.3 最终定位为 Tomcat embed Bug(以下摘自 https://segmentfault.com/a/11…
3.3.1 为什么每次申请会加载 Jar 包内的动态资源
关键在于 org.apache.catalina.mapper.Mapper#internalMapWrapper 这个办法,该版本下解决申请的形式有问题,导致每次都校验动态资源。
3.3.2 为什么间断申请不会呈现问题
因为 Tomcat 对于这种动态资源的解析是有缓存的,优先从缓存查找,缓存过期后再从新解析。具体参考 org.apache.catalina.webresources.Cache,默认过期工夫 ttl 是 5000ms。
3.3.3 为什么本地不会复现
其实确切的说,是通过 spring-boot 打包插件后不能复现。因为启动形式的不同,tomcat 应用了不同的类去解决动态资源,所以没问题
解决方案:
1、降级 tomcat-embed 版本即可,呈现 bug 的版本为
springBoot:1.5.9 tomcat: Apache Tomcat/8.5.23
降级 tomcat embed 版本至 8.5.40+ 即可解决此问题,新版本曾经修复了
2、降级 spring boot 版本
springboot 2.1.0.RELEASE 中的 tomcat embed 版本曾经大于 8.5.31 了,所以间接将 springboot 降级至该版本及以上版本就能够解决此问题

致谢
1、感激为解决此次问题提供帮忙的共事们
2、感激把教训分享进去的大拿们
谢谢大家的帮忙

正文完
 0