文章来源:https://juejin.cn/post/7140462361759973384

背景

公司有个渠道系统,专门对接三方渠道使用,没有什么业务逻辑,主要是转换报文和参数校验之类的工作,起着一个承上启下的作用。

最近在优化接口的响应时间,优化了代码之后,但是时间还是达不到要求;有一个诡异的100ms左右的耗时问题,在接口中打印了请求处理时间后,和调用方的响应时间还有差了100ms左右。比如程序里记录150ms,但是调用方等待时间却为250ms左右。

下面记录下当时详细的定位&解决流程(其实解决很简单,关键在于怎么定位并找到解决问题的方法)

一、定位过程

分析代码

渠道系统是一个常见的spring-boot web工程,使用了集成的tomcat。分析了代码之后,发现并没有特殊的地方,没有特殊的过滤器或者拦截器,所以初步排除是业务代码问题

分析调用流程

出现这个问题之后,首先确认了下接口的调用流程。由于是内部测试,所以调用流程较少。

Nginx -反向代理-> 渠道系统

公司是云服务器,网络走的也是云的内网。由于不明确问题的原因,所以用排除法,首先确认服务器网络是否有问题。

先确认发送端到Nginx Host是否有问题:

[jboss@VM_0_139_centos ~]$ ping 10.0.0.139
PING 10.0.0.139 (10.0.0.139) 56(84) bytes of data.
64 bytes from 10.0.0.139: icmp_seq=1 ttl=64 time=0.029 ms
64 bytes from 10.0.0.139: icmp_seq=2 ttl=64 time=0.041 ms
64 bytes from 10.0.0.139: icmp_seq=3 ttl=64 time=0.040 ms
64 bytes from 10.0.0.139: icmp_seq=4 ttl=64 time=0.040 ms

从ping结果上看,发送端到Nginx主机的延迟是无问题的,接下来查看Nginx到渠道系统的网络。

# 由于日志是没问题的,这里直接复制上面日志了
[jboss@VM_0_139_centos ~]$ ping 10.0.0.139
PING 10.0.0.139 (10.0.0.139) 56(84) bytes of data.
64 bytes from 10.0.0.139: icmp_seq=1 ttl=64 time=0.029 ms
64 bytes from 10.0.0.139: icmp_seq=2 ttl=64 time=0.041 ms
64 bytes from 10.0.0.139: icmp_seq=3 ttl=64 time=0.040 ms
64 bytes from 10.0.0.139: icmp_seq=4 ttl=64 time=0.040 ms

从ping结果上看,Nginx到渠道系统服务器网络延迟也是没问题的

既然网络看似没问题,那么可以继续排除法,砍掉Nginx,客户端直接再渠道系统的服务器上,通过回环地址(localhost)直连,避免经过网卡/dns,缩小问题范围看看能否复现(这个应用和地址是我后期模拟的,测试的是一个空接口):

[jboss@VM_10_91_centos tmp]$ curl -w "@curl-time.txt" http://127.0.0.1:7744/send
successhttp: 200dns: 0.001sredirect: 0.000stime_connect: 0.001stime_appconnect: 0.000stime_pretransfer: 0.001s
time_starttransfer: 0.073ssize_download: 7bytesspeed_download: 95.000B/s----------time_total: 0.073s 请求总耗时

从curl日志上看,通过回环地址调用一个空接口耗时也有73ms。这就奇怪了,跳过了中间所有调用节点(包括过滤器&拦截器之类),直接请求应用一个空接口,都有73ms的耗时,再请求一次看看:

[jboss@VM_10_91_centos tmp]$ curl -w "@curl-time.txt" http://127.0.0.1:7744/send
successhttp: 200dns: 0.001sredirect: 0.000stime_connect: 0.001stime_appconnect: 0.000stime_pretransfer: 0.001s
time_starttransfer: 0.003ssize_download: 7bytesspeed_download: 2611.000B/s----------time_total: 0.003s

更奇怪的是,第二次请求耗时就正常了,变成了3ms。经查阅资料,linux curl是默认开启http keep-alive的。就算不开启keep-alive,每次重新handshake,也不至于需要70ms。

经过不断分析测试发现,连续请求的话时间就会很短,每次请求只需要几毫秒,但是如果隔一段时间再请求,就会花费70ms以上。

从这个现象猜想,可能是某些缓存机制导致的,连续请求因为有缓存,所以速度快,时间长缓存失效后导致时间长。

那么这个问题点到底在哪一层呢?tomcat层还是spring-webmvc呢?

光猜想定位不了问题,还是得实际测试一下,把渠道系统的代码放到本地ide里启动测试能否复现

但是导入本地Ide后,在Ide中启动后并不能复现问题,并没有70+ms的延迟问题。这下头疼了,本地无法复现,不能Debug,由于问题点不在业务代码,也不能通过加日志的方式来Debug

这时候可以祭出神器Arthas了

二、Arthas分析问题

Arthas 是Alibaba开源的Java诊断工具,深受开发者喜爱。当你遇到以下类似问题而束手无策时,Arthas可以帮助你解决:

1、这个类从哪个 jar 包加载的?为什么会报各种类相关的 Exception?

2、我改的代码为什么没有执行到?难道是我没 commit?分支搞错了?

3、遇到问题无法在线上 debug,难道只能通过加日志再重新发布吗?

4、线上遇到某个用户的数据处理有问题,但线上同样无法 debug,线下无法重现!

5、是否有一个全局视角来查看系统的运行状况?

6、有什么办法可以监控到JVM的实时运行状态?

上面是Arthas的官方简介,这次我只需要用他的一个小功能 trace 。动态计算方法调用路径和时间,这样我就可以定位时间在哪个地方被消耗了。

1、trace 方法内部调用路径,并输出方法路径上的每个节点上耗时

2、trace 命令能主动搜索 class-pattern/method-pattern

3、对应的方法调用路径,渲染和统计整个调用链路上的所有性能开销和追踪调用链路。

有了神器,那么该追踪什么方法呢?由于我对Tomcat源码不是很熟,所以只能从spring mvc下手,先来trace一下spring mvc的入口:

[jboss@VM_10_91_centos tmp]$ curl -w "@curl-time.txt" http://127.0.0.1:7744/send
successhttp: 200dns: 0.001sredirect: 0.000stime_connect: 0.001stime_appconnect: 0.000stime_pretransfer: 0.001s
time_starttransfer: 0.115ssize_download: 7bytesspeed_download: 60.000B/s----------time_total: 0.115s

本次调用,调用端时间花费115ms,但是从arthas trace上看,spring mvc只消耗了18ms,那么剩下的97ms去哪了呢?

本地测试后已经可以排除spring mvc的问题了,最后也是唯一可能出问题的点就是tomcat

可是本人并不熟悉tomcat中的源码,就连请求入口都不清楚,tomcat里需要trace的类都不好找。。。

不过没关系,有神器Arthas,可以通过stack命令来反向查找调用路径,以 org.springframework.web.servlet.DispatcherServlet作为参数:

stack 输出当前方法被调用的调用路径

很多时候我们都知道一个方法被执行,但这个方法被执行的路径非常多,或者你根本就不知道这个方法是从那里被执行了,此时你需要的是 stack 命令。

从stack日志上可以很直观的看出DispatchServlet的调用栈,那么这么长的路径,该trace哪个类呢(这里跳过spring mvc中的过滤器的trace过程,实际排查的时候也trace了一遍,但这诡异的时间消耗不是由这里过滤器产生的)?

有一定经验的老司机从名字上大概也能猜出来从哪里下手比较好,那就是 org.apache.coyote.http11.Http11Processor.service ,从名字上看,http1.1处理器,这可能是一个比较好的切入点。下面来trace一下:

日志里有一个129ms的耗时点(时间比没开arthas的时候更长是因为arthas本身带来的性能消耗,所以生产环境小心使用),这个就是要找的问题点。

打问题点找到了,那怎么定位是什么导致的问题呢,又如何解决呢?

继续trace吧,细化到具体的代码块或者内容。trace由于性能考虑,不会展示所有的调用路径,如果调用路径过深,只有手动深入trace,原则就是trace耗时长的那个方法:

一段无聊的手动深入trace之后………………

发现了一个值得暂停思考的点:

+---[min=0.004452ms,max=34.479307ms,total=74.206249ms,count=31] org.apache.catalina.webresources.TomcatJarInputStream:getNextJarEntry() #117

这行代码加载了31次,一共耗时74ms;从名字上看,应该是tomcat加载jar包时的耗时,那么是加载了31个jar包的耗时,还是加载了jar包内的某些资源31次耗时呢?

TomcatJarInputStream这个类源码的注释写到:

The purpose of this sub-class is to obtain references to the JarEntry objects for META-INF/ and META-INF/MANIFEST.MF that are otherwise swallowed by the JarInputStream implementation.

大概意思也就是,获取jar包内META-INF/,META-INF/MANIFEST的资源,这是一个子类,更多的功能在父类JarInputStream里。

其实看到这里大概也能猜到问题了,tomcat加载jar包内META-INF/,META-INF/MANIFEST的资源导致的耗时,至于为什么连续请求不会耗时,应该是tomcat的缓存机制(下面介绍源码分析)

不着急定位问题,试着通过Arthas最终定位问题细节,继续手动深入trace

[arthas@24851]$ trace org.apache.catalina.webresources.TomcatJarInputStream *
Press Q or Ctrl+C to abort.
Affect(class-cnt:1 , method-cnt:4) cost in 44 ms.
`---ts=2019-09-14 21:37:47;thread_name=http-nio-7744-exec-5;id=14;is_daemon=true;priority=5;TCCL=org.springframework.boot.loader.LaunchedURLClassLoader@20ad9418`---[0.234952ms] org.apache.catalina.webresources.TomcatJarInputStream:createZipEntry()+---[0.039455ms] java.util.jar.JarInputStream:createZipEntry() #43`---[0.007827ms] java.lang.String:equals() #44`---ts=2019-09-14 21:37:47;thread_name=http-nio-7744-exec-5;id=14;is_daemon=true;priority=5;TCCL=org.springframework.boot.loader.LaunchedURLClassLoader@20ad9418`---[0.050222ms] org.apache.catalina.webresources.TomcatJarInputStream:createZipEntry()+---[0.001889ms] java.util.jar.JarInputStream:createZipEntry() #43`---[0.001643ms] java.lang.String:equals() #46
#这里一共31个trace日志,删减了剩下的

从方法名上看,还是加载资源之类的意思。都已经到jdk源码了,这时候来看一下 TomcatJarInputStream 这个类的源码:

/*** Creates a new <code>JarEntry</code> (<code>ZipEntry</code>) for the* specified JAR file entry name. The manifest attributes of* the specified JAR file entry name will be copied to the new* <CODE>JarEntry</CODE>.** @param name the name of the JAR/ZIP file entry* @return the <code>JarEntry</code> object just created*/
protected ZipEntry createZipEntry(String name) {JarEntry e = new JarEntry(name);if (man != null) {e.attr = man.getAttributes(name);}return e;
}

这个 createZipEntry 有个name参数,从注释上看,是jar/zip文件名,如果能得到文件名这种关键信息,就可以直接定位问题了;还是通过Arthas,使用watch命令,动态监测方法调用数据

watch方法执行数据观测

让你能方便的观察到指定方法的调用情况。能观察到的范围为:返回值、抛出异常、入参,通过编写 OGNL 表达式进行对应变量的查看。

watch 该方法的入参

这下直接看到了具体加载的资源名,这么熟悉的名字:swagger-ui,一个国外的rest接口文档工具,又有国内开发者基于swagger-ui做了一套spring mvc的集成工具,通过注解就可以自动生成swagger-ui需要的接口定义json文件,用起来还比较方便,就是侵入性较强。

删除swagger的jar包后问题,诡异的70+ms就消失了

<!--pom 里删除这两个引用,这两个包时国内开发者封装的,swagger-ui并没有提供java spring-mvc的支持包,swagger只是一个浏览器端的ui+editor -->
<dependency><groupId>io.springfox</groupId><artifactId>springfox-swagger2</artifactId><version>2.9.2</version>
</dependency>
<dependency><groupId>io.springfox</groupId><artifactId>springfox-swagger-ui</artifactId><version>2.9.2</version>
</dependency>

那么为什么swagger会导致请求耗时呢,为什么每次请求偶读会加载swagger内部的静态资源呢?

其实这是tomcat-embed的一个bug吧,下面详细介绍一下该Bug

三、Tomcat embed Bug分析&解决

源码分析过程实在太漫长,而且也不是本文的重点,所以就不介绍了, 下面直接介绍下分析结果

顺便贴一张tomcat处理请求的核心类图

1、为什么每次请求会加载Jar包内的静态资源

关键在于 org.apache.catalina.mapper.Mapper#internalMapWrapper  这个方法,该版本下处理请求的方式有问题,导致每次都校验静态资源。

2、为什么连续请求不会出现问题

因为Tomcat对于这种静态资源的解析是有缓存的,优先从缓存查找,缓存过期后再重新解析。具体参考org.apache.catalina.webresources.Cache ,默认过期时间ttl是5000ms。

3、为什么本地不会复现

其实确切的说,是通过spring-boot打包插件后不能复现。由于启动方式的不同,tomcat使用了不同的类去处理静态资源,所以没问题

4、如何解决

升级tomcat-embed版本即可

当前出现Bug的版本为:

spring-boot:2.0.2.RELEASE,内置的tomcat embed版本为8.5.31

升级tomcat embed版本至8.5.40+即可解决此问题,新版本已经修复了

通过替换springboot pom properties方式

如果项目是maven是继承的springboot,即parent配置为springboot的,或者dependencyManagement中import spring boot包的

<parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>2.0.2.RELEASE</version><relativePath/> <!-- lookup parent from repository --></parent>

pom中直接覆盖properties即可:

<properties><tomcat.version>8.5.40</tomcat.version>
</properties>

5、升级spring boot版本

springboot 2.1.0.RELEASE中的tomcat embed版本已经大于8.5.31了,所以直接将springboot升级至该版本及以上版本就可以解决此问题!

SpringSecurity 权限系统设计!

linux下 18 个实用的终端命令行工具

如何写出难以维护的“烂代码”?

Spring Boot+Netty+Websocket实现后台向前端推送信息

SpringCloud微服务的熔断机制和熔断的意义?

java项目线上JVM调优实践,FullGC大大减少

如何优雅记录 HTTP 请求/ 响应数据

回复干货】获取精选干货视频教程

回复加群】加入疑难问题攻坚交流群

回复mat】获取内存溢出问题分析详细文档教程

回复赚钱】获取用java写一个能赚钱的微信机器人

回复副业】获取程序员副业攻略一份

好文请点赞+分享

我找到了一个快速定位SpringBoot接口超时问题的神器!相关推荐

  1. 使用Arthas快速定位SpringBoot接口超时问题的神器

    使用Arthas快速定位SpringBoot接口超时问题的神器 文章系转载,便于整理和分类,原文地址:https://mp.weixin.qq.com/s/Nm_QGzCtwY08Dd1XOtPaaw ...

  2. vscode不能跳转_vscode-goto-node-modules 一个快速定位 node 模块的 vscode 插件

    vscode-goto-node-modules 一个快速定位 node 模块的 vscode 插件 原文:http://www.aqcoder.com/post/43 在使用 VSCode 开发 N ...

  3. 推荐一个快速定位深度学习代码bug的炼丹神器!

    文 | McGL 源 | 知乎 写深度学习网络代码,最大的挑战之一,尤其对新手来说,就是把所有的张量维度正确对齐.如果以前就有TensorSensor这个工具,相信我的头发一定比现在更浓密茂盛! Te ...

  4. 后端技术:一个注解解决 SpringBoot 接口防刷

    说明:使用了注解的方式进行对接口防刷的功能,非常高大上,本文章仅供参考 技术要点:springboot的基本知识,redis基本操作, 首先是写一个注解类: import java.lang.anno ...

  5. vvv在线文档导出工具_使用ApiPost工具快速生成在线接口文档

    ApiPost是一个支持团队协作,并可直接生成文档的API调试.管理工具.它支持模拟POST.GET.PUT等常见请求,是后台接口开发者或前端.接口测试人员不可多得的工具 .使用者不仅可以利用apio ...

  6. SpringBoot接口频繁超时,长时间找不到原因,我用 Arthas 定位到了

    点击上方蓝色"方志朋",选择"设为星标" 回复"666"获取独家整理的学习资料! 公司有个渠道系统,专门对接三方渠道使用,没有什么业务逻辑, ...

  7. 其中一个页签慢_Word中如何快速定位到页、行、表格、公式,查找与替换方法...

    如果一个文档有几百页甚至上千页,要通过拖动滑块定位到某页将是十分不易的事,拉多了又过了,拉少了又离得太远.如果用 Word 2016 提供的定位功能,定位到某页将变得十分容易的事,并且速度相当快,瞬间 ...

  8. fastslam matlab,fastslam 快速定位和构图的源码,一个简单的例子,3D建模,可以用作学习智能机器人自主移动 matlab 272万源代码下载- www.pudn.com...

    文件名称: fastslam下载  收藏√  [ 5  4  3  2  1 ] 开发工具: matlab 文件大小: 31 KB 上传时间: 2015-03-19 下载次数: 22 详细说明:快速定 ...

  9. 如何快速定位出一个IP地址的归属地?——二分查找变体

    如何快速定位出一个IP地址的归属地?--二分查找变体 查找第一个值等于给定值的元素 查找最后一个值等于给定值的元素 查找第一个大于等于给定值的元素 查找最后一个小于等于给定值的元素 查找循环有序数组中 ...

最新文章

  1. Java 实现MapReduce函数
  2. Rhino脚本引擎技术介绍
  3. eclipse egit(分支管理 上)
  4. Linux删除重复内容命令uniq笔记
  5. 【一针见血】 JavaScript this
  6. Android TextView长按复制实现,Android复制文本
  7. 日常问题解决记录三:记一次Win10安装Oracle11g后遇到的问题
  8. 19 矩阵——矩阵的相抵、相抵标准形、秩1矩阵、矩阵的满秩分解
  9. 设计佣金问题的java程序_三角形、nextday、佣金问题实验报告.doc
  10. hutool依赖:BeanUtil工具类的使用:对象转对象、对象转map、map转对象
  11. RFID技术在图书馆中的应用
  12. 作业帮联手北师大、中国教育电视台以科技推进普惠教育发展
  13. 《近匠》专访启明星辰安全研究中心副总监侯浩俊——物联网安全攻防的“线上幽灵”...
  14. 20款有趣的英文卡通免费字体
  15. 从内网windows2008服务器复制文件到本地慢,Windows Server 2008网上邻居打开慢的解决...
  16. php万年历源代码!源代码![上一年、上一月、下一月、下一年、附加当天日期加背景颜色]-私聊源码
  17. centos7 下mono安装
  18. tsc条码标签打印机维修故障有哪些
  19. 《生物信息学:导论与方法》----导论与历史----听课笔记(一)
  20. python 计算斐波那契数列方法,递归方法求第N项的斐波那契数

热门文章

  1. 微软 地平线5 无法加入线上模式处理办法
  2. 极客头条招聘:内容编辑(结束)
  3. 传统武式太极拳练习五阶段
  4. access中数据类型转换函数
  5. Java,第一次作业——解一元二次方程
  6. python 实现用ISBN从豆瓣获取图书信息
  7. 计算机专业中经典书籍(程序猿和大学生必读)
  8. Vue2速成手册(原创不易,转载请注明出处)
  9. RT5350 openwrt添加Reset按键,实现短按重启系统,长按复位系统
  10. 安装minigui和mStudio