阿里问题定位神器 Arthas 的骚操作,定位线上BUG,超给力
原文:https://mp.weixin.qq.com/s/Y58PFhCgbC-GBjj_sHxvDg

公司有个渠道系统,专门对接三方渠道使用,没有什么业务逻辑,主要是转换报文和参数校验之类的工作,起着一个承上启下的作用。
最近在优化接口的响应时间,优化了代码之后,但是时间还是达不到要求;有一个诡异的100ms左右的耗时问题,在接口中打印了请求处理时间后,和调用方的响应时间还有差了100ms左右。比如程序里记录150ms,但是调用方等待时间却为250ms左右。
下面记录下当时详细的定位&解决流程(其实解决很简单,关键在于怎么定位并找到解决问题的方法)
定位过程
分析代码
渠道系统是一个常见的spring-boot web工程,使用了集成的tomcat。分析了代码之后,发现并没有特殊的地方,没有特殊的过滤器或者拦截器,所以初步排除是业务代码问题
分析调用流程
出现这个问题之后,首先确认了下接口的调用流程。由于是内部测试,所以调用流程较少。
Nginx -反向代理-> 渠道系统
公司是云服务器,网络走的也是云的内网。由于不明确问题的原因,所以用排除法,首先确认服务器网络是否有问题。
先确认发送端到Nginx Host是否有问题:

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

从curl日志上看,通过回环地址调用一个空接口耗时也有73ms。这就奇怪了,跳过了中间所有调用节点(包括过滤器&拦截器之类),直接请求应用一个空接口,都有73ms的耗时,再请求一次看看:
更奇怪的是,第二次请求耗时就正常了,变成了3ms。经查阅资料,linux curl是默认开启http keep-alive的(Keep-Alive的介绍可以参考我的另一篇文章)。就算不开启keep-alive,每次重新handshake,也不至于需要70ms。
经过不断分析测试发现,连续请求的话时间就会很短,每次请求只需要几毫秒,但是如果隔一段时间再请求,就会花费70ms以上。
从这个现象猜想,可能是某些缓存机制导致的,连续请求因为有缓存,所以速度快,时间长缓存失效后导致时间长。
那么这个问题点到底在哪一层呢?tomcat层还是spring-webmvc呢?
光猜想定位不了问题,还是得实际测试一下,把渠道系统的代码放到本地ide里启动测试能否复现
但是导入本地Ide后,在Ide中启动后并不能复现问题,并没有70+ms的延迟问题。这下头疼了,本地无法复现,不能Debug,由于问题点不在业务代码,也不能通过加日志的方式来Debug
这时候可以祭出神器Arthas了
Arthas分析问题
Arthas 是Alibaba开源的Java诊断工具,深受开发者喜爱。当你遇到以下类似问题而束手无策时,Arthas可以帮助你解决:
-
这个类从哪个 jar 包加载的?
-
为什么会报各种类相关的 Exception?
-
我改的代码为什么没有执行到?
-
难道是我没 commit?分支搞错了?
-
遇到问题无法在线上 debug,难道只能通过加日志再重新发布吗?
-
线上遇到某个用户的数据处理有问题,但线上同样无法 debug,线下无法重现!
-
是否有一个全局视角来查看系统的运行状况?
-
有什么办法可以监控到JVM的实时运行状态?
上面是Arthas的官方简介,这次我只需要用他的一个小功能trace。动态计算方法调用路径和时间,这样我就可以定位时间在哪个地方被消耗了。
trace 方法内部调用路径,并输出方法路径上的每个节点上耗时 trace 命令能主动搜索 class-pattern/method-pattern 对应的方法调用路径,渲染和统计整个调用链路上的所有性能开销和追踪调用链路。
有了神器,那么该追踪什么方法呢?由于我对Tomcat源码不是很熟,所以只能从spring mvc下手,先来trace一下spring mvc的入口:
[
arthas@24851
]
$ trace org
.
springframework
.
web
.
servlet
.
DispatcherServlet
*
Press
Q
or
Ctrl
+
C to abort
.
Affect
(
class
-
cnt
:
1
,
method
-
cnt
:
44
)
cost
in
508
ms
.
`---ts=2019-09-14 21:07:44;thread_name=http-nio-7744-exec-2;id=11;is_daemon=true;priority=5;TCCL=org.springframework.boot.web.embedded.tomcat.TomcatEmbeddedWebappClassLoader@7c136917
`
---[
2.952142ms
]
org
.
springframework
.
web
.
servlet
.
DispatcherServlet
:
buildLocaleContext
()
`---ts=2019-09-14 21:07:44;thread_name=http-nio-7744-exec-2;id=11;is_daemon=true;priority=5;TCCL=org.springframework.boot.web.embedded.tomcat.TomcatEmbeddedWebappClassLoader@7c136917
`
---[
18.08903ms
]
org
.
springframework
.
web
.
servlet
.
DispatcherServlet
:
doService
()
+---[
0.041346ms
]
org
.
apache
.
commons
.
logging
.
Log
:
isDebugEnabled
()
#889
+---[
0.022398ms
]
org
.
springframework
.
web
.
util
.
WebUtils
:
isIncludeRequest
()
#898
+---[
0.014904ms
]
org
.
springframework
.
web
.
servlet
.
DispatcherServlet
:
getWebApplicationContext
()
#910
+---[
1.071879ms
]
javax
.
servlet
.
http
.
HttpServletRequest
:
setAttribute
()
#910
+---[
0.020977ms
]
javax
.
servlet
.
http
.
HttpServletRequest
:
setAttribute
()
#911
+---[
0.017073ms
]
javax
.
servlet
.
http
.
HttpServletRequest
:
setAttribute
()
#912
+---[
0.218277ms
]
org
.
springframework
.
web
.
servlet
.
DispatcherServlet
:
getThemeSource
()
#913
|
`---[0.137568ms] org.springframework.web.servlet.DispatcherServlet:getThemeSource()
| `
---[
min
=
0.00783ms
,
max
=
0.014251ms
,
total
=
0.022081ms
,
count
=
2
]
org
.
springframework
.
web
.
servlet
.
DispatcherServlet
:
getWebApplicationContext
()
#782
+---[
0.019363ms
]
javax
.
servlet
.
http
.
HttpServletRequest
:
setAttribute
()
#913
+---[
0.070694ms
]
org
.
springframework
.
web
.
servlet
.
FlashMapManager
:
retrieveAndUpdate
()
#916
+---[
0.01839ms
]
org
.
springframework
.
web
.
servlet
.
FlashMap
:<
init
>()
#920
+---[
0.016943ms
]
javax
.
servlet
.
http
.
HttpServletRequest
:
setAttribute
()
#920
+---[
0.015268ms
]
javax
.
servlet
.
http
.
HttpServletRequest
:
setAttribute
()
#921
+---[
15.050124ms
]
org
.
springframework
.
web
.
servlet
.
DispatcherServlet
:
doDispatch
()
#925
|
`---[14.943477ms] org.springframework.web.servlet.DispatcherServlet:doDispatch()
| +---[0.019135ms] org.springframework.web.context.request.async.WebAsyncUtils:getAsyncManager() #953
| +---[2.108373ms] org.springframework.web.servlet.DispatcherServlet:checkMultipart() #960
| | `
---[
2.004436ms
]
org
.
springframework
.
web
.
servlet
.
DispatcherServlet
:
checkMultipart
()
|
|
`---[1.890845ms] org.springframework.web.multipart.MultipartResolver:isMultipart() #1117
| +---[2.054361ms] org.springframework.web.servlet.DispatcherServlet:getHandler() #964
| | `
---[
1.961963ms
]
org
.
springframework
.
web
.
servlet
.
DispatcherServlet
:
getHandler
()
|
|
+---[
0.02051ms
]
java
.
util
.
List
:
iterator
()
#1183
|
|
+---[
min
=
0.003805ms
,
max
=
0.009641ms
,
total
=
0.013446ms
,
count
=
2
]
java
.
util
.
Iterator
:
hasNext
()
#1183
|
|
+---[
min
=
0.003181ms
,
max
=
0.009751ms
,
total
=
0.012932ms
,
count
=
2
]
java
.
util
.
Iterator
:
next
()
#1183
|
|
+---[
min
=
0.005841ms
,
max
=
0.015308ms
,
total
=
0.021149ms
,
count
=
2
]
org
.
apache
.
commons
.
logging
.
Log
:
isTraceEnabled
()
#1184
|
|
`---[min=0.474739ms,max=1.19145ms,total=1.666189ms,count=2] org.springframework.web.servlet.HandlerMapping:getHandler() #1188
| +---[0.013071ms] org.springframework.web.servlet.HandlerExecutionChain:getHandler() #971
| +---[0.372236ms] org.springframework.web.servlet.DispatcherServlet:getHandlerAdapter() #971
| | `
---[
0.280073ms
]
org
.
springframework
.
web
.
servlet
.
DispatcherServlet
:
getHandlerAdapter
()
|
|
+---[
0.004804ms
]
java
.
util
.
List
:
iterator
()
#1224
|
|
+---[
0.003668ms
]
java
.
util
.
Iterator
:
hasNext
()
#1224
|
|
+---[
0.003038ms
]
java
.
util
.
Iterator
:
next
()
#1224
|
|
+---[
0.006451ms
]
org
.
apache
.
commons
.
logging
.
Log
:
isTraceEnabled
()
#1225
|
|
`---[0.012683ms] org.springframework.web.servlet.HandlerAdapter:supports() #1228
| +---[0.012848ms] javax.servlet.http.HttpServletRequest:getMethod() #974
| +---[0.013132ms] java.lang.String:equals() #975
| +---[0.003025ms] org.springframework.web.servlet.HandlerExecutionChain:getHandler() #977
| +---[0.008095ms] org.springframework.web.servlet.HandlerAdapter:getLastModified() #977
| +---[0.006596ms] org.apache.commons.logging.Log:isDebugEnabled() #978
| +---[0.018024ms] org.springframework.web.context.request.ServletWebRequest:<init>() #981
| +---[0.017869ms] org.springframework.web.context.request.ServletWebRequest:checkNotModified() #981
| +---[0.038542ms] org.springframework.web.servlet.HandlerExecutionChain:applyPreHandle() #986
| +---[0.00431ms] org.springframework.web.servlet.HandlerExecutionChain:getHandler() #991
| +---[4.248493ms] org.springframework.web.servlet.HandlerAdapter:handle() #991
| +---[0.014805ms] org.springframework.web.context.request.async.WebAsyncManager:isConcurrentHandlingStarted() #993
| +---[1.444994ms] org.springframework.web.servlet.DispatcherServlet:applyDefaultViewName() #997
| | `
---[
0.067631ms
]
org
.
springframework
.
web
.
servlet
.
DispatcherServlet
:
applyDefaultViewName
()
|
+---[
0.012027ms
]
org
.
springframework
.
web
.
servlet
.
HandlerExecutionChain
:
applyPostHandle
()
#998
|
+---[
0.373997ms
]
org
.
springframework
.
web
.
servlet
.
DispatcherServlet
:
processDispatchResult
()
#1008
|
|
`---[0.197004ms] org.springframework.web.servlet.DispatcherServlet:processDispatchResult()
| | +---[0.007074ms] org.apache.commons.logging.Log:isDebugEnabled() #1075
| | +---[0.005467ms] org.springframework.web.context.request.async.WebAsyncUtils:getAsyncManager() #1081
| | +---[0.004054ms] org.springframework.web.context.request.async.WebAsyncManager:isConcurrentHandlingStarted() #1081
| | `
---[
0.011988ms
]
org
.
springframework
.
web
.
servlet
.
HandlerExecutionChain
:
triggerAfterCompletion
()
#1087
|
`---[0.004015ms] org.springframework.web.context.request.async.WebAsyncManager:isConcurrentHandlingStarted() #1018
+---[0.005055ms] org.springframework.web.context.request.async.WebAsyncUtils:getAsyncManager() #928
`
---[
0.003422ms
]
org
.
springframework
.
web
.
context
.
request
.
async
.
WebAsyncManager
:
isConcurrentHandlingStarted
()
#928
[
jboss@VM_10_91_centos tmp
]
$ curl
-
w
"@curl-time.txt"
http
:
//127.0.0.1:7744/send
success
http
:
200
dns
:
0.001s
redirect
:
0.000s
time_connect
:
0.001s
time_appconnect
:
0.000s
time_pretransfer
:
0.001s
time_starttransfer
:
0.115s
size_download
:
7bytes
speed_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 命令。
[
arthas@24851
]
$ stack org
.
springframework
.
web
.
servlet
.
DispatcherServlet
*
Press
Q
or
Ctrl
+
C to abort
.
Affect
(
class
-
cnt
:
1
,
method
-
cnt
:
44
)
cost
in
495
ms
.
ts
=
2019
-
09
-
14
21
:
15
:
19
;
thread_name
=
http
-
nio
-
7744
-
exec
-
5
;
id
=
14
;
is_daemon
=
true
;
priority
=
5
;
TCCL
=
org
.
springframework
.
boot
.
web
.
embedded
.
tomcat
.
TomcatEmbeddedWebappClassLoader@7c136917
@org
.
springframework
.
web
.
servlet
.
FrameworkServlet
.
processRequest
()
at org
.
springframework
.
web
.
servlet
.
FrameworkServlet
.
doGet
(
FrameworkServlet
.
java
:
866
)
at javax
.
servlet
.
http
.
HttpServlet
.
service
(
HttpServlet
.
java
:
635
)
at org
.
springframework
.
web
.
servlet
.
FrameworkServlet
.
service
(
FrameworkServlet
.
java
:
851
)
at javax
.
servlet
.
http
.
HttpServlet
.
service
(
HttpServlet
.
java
:
742
)
at org
.
apache
.
catalina
.
core
.
ApplicationFilterChain
.
internalDoFilter
(
ApplicationFilterChain
.
java
:
231
)
at org
.
apache
.
catalina
.
core
.
ApplicationFilterChain
.
doFilter
(
ApplicationFilterChain
.
java
:
166
)
at org
.
apache
.
tomcat
.
websocket
.
server
.
WsFilter
.
doFilter
(
WsFilter
.
java
:
52
)
at org
.
apache
.
catalina
.
core
.
ApplicationFilterChain
.
internalDoFilter
(
ApplicationFilterChain
.
java
:
193
)
at org
.
apache
.
catalina
.
core
.
ApplicationFilterChain
.
doFilter
(
ApplicationFilterChain
.
java
:
166
)
at org
.
springframework
.
web
.
filter
.
RequestContextFilter
.
doFilterInternal
(
RequestContextFilter
.
java
:
99
)
at org
.
springframework
.
web
.
filter
.
OncePerRequestFilter
.
doFilter
(
OncePerRequestFilter
.
java
:
107
)
at org
.
apache
.
catalina
.
core
.
ApplicationFilterChain
.
internalDoFilter
(
ApplicationFilterChain
.
java
:
193
)
at org
.
apache
.
catalina
.
core
.
ApplicationFilterChain
.
doFilter
(
ApplicationFilterChain
.
java
:
166
)
at org
.
springframework
.
web
.
filter
.
HttpPutFormContentFilter
.
doFilterInternal
(
HttpPutFormContentFilter
.
java
:
109
)
at org
.
springframework
.
web
.
filter
.
OncePerRequestFilter
.
doFilter
(
OncePerRequestFilter
.
java
:
107
)
at org
.
apache
.
catalina
.
core
.
ApplicationFilterChain
.
internalDoFilter
(
ApplicationFilterChain
.
java
:
193
)
at org
.
apache
.
catalina
.
core
.
ApplicationFilterChain
.
doFilter
(
ApplicationFilterChain
.
java
:
166
)
at org
.
springframework
.
web
.
filter
.
HiddenHttpMethodFilter
.
doFilterInternal
(
HiddenHttpMethodFilter
.
java
:
81
)
at org
.
springframework
.
web
.
filter
.
OncePerRequestFilter
.
doFilter
(
OncePerRequestFilter
.
java
:
107
)
at org
.
apache
.
catalina
.
core
.
ApplicationFilterChain
.
internalDoFilter
(
ApplicationFilterChain
.
java
:
193
)
at org
.
apache
.
catalina
.
core
.
ApplicationFilterChain
.
doFilter
(
ApplicationFilterChain
.
java
:
166
)
at org
.
springframework
.
web
.
filter
.
CharacterEncodingFilter
.
doFilterInternal
(
CharacterEncodingFilter
.
java
:
200
)
at org
.
springframework
.
web
.
filter
.
OncePerRequestFilter
.
doFilter
(
OncePerRequestFilter
.
java
:
107
)
at org
.
apache
.
catalina
.
core
.
ApplicationFilterChain
.
internalDoFilter
(
ApplicationFilterChain
.
java
:
193
)
at org
.
apache
.
catalina
.
core
.
ApplicationFilterChain
.
doFilter
(
ApplicationFilterChain
.
java
:
166
)
at org
.
apache
.
catalina
.
core
.
StandardWrapperValve
.
invoke
(
StandardWrapperValve
.
java
:
198
)
at org
.
apache
.
catalina
.
core
.
StandardContextValve
.
invoke
(
StandardContextValve
.
java
:
96
)
at org
.
apache
.
catalina
.
authenticator
.
AuthenticatorBase
.
invoke
(
AuthenticatorBase
.
java
:
496
)
at org
.
apache
.
catalina
.
core
.
StandardHostValve
.
invoke
(
StandardHostValve
.
java
:
140
)
at org
.
apache
.
catalina
.
valves
.
ErrorReportValve
.
invoke
(
ErrorReportValve
.
java
:
81
)
at org
.
apache
.
catalina
.
core
.
StandardEngineValve
.
invoke
(
StandardEngineValve
.
java
:
87
)
at org
.
apache
.
catalina
.
connector
.
CoyoteAdapter
.
service
(
CoyoteAdapter
.
java
:
342
)
at org
.
apache
.
coyote
.
http11
.
Http11Processor
.
service
(
Http11Processor
.
java
:
803
)
at org
.
apache
.
coyote
.
AbstractProcessorLight
.
process
(
AbstractProcessorLight
.
java
:
66
)
at org
.
apache
.
coyote
.
AbstractProtocol$ConnectionHandler
.
process
(
AbstractProtocol
.
java
:
790
)
at org
.
apache
.
tomcat
.
util
.
net
.
NioEndpoint$SocketProcessor
.
doRun
(
NioEndpoint
.
java
:
1468
)
at org
.
apache
.
tomcat
.
util
.
net
.
SocketProcessorBase
.
run
(
SocketProcessorBase
.
java
:
49
)
at java
.
util
.
concurrent
.
ThreadPoolExecutor
.
runWorker
(
ThreadPoolExecutor
.
java
:
1149
)
at java
.
util
.
concurrent
.
ThreadPoolExecutor$Worker
.
run
(
ThreadPoolExecutor
.
java
:
624
)
at org
.
apache
.
tomcat
.
util
.
threads
.
TaskThread$WrappingRunnable
.
run
(
TaskThread
.
java
:
61
)
at java
.
lang
.
Thread
.
run
(
Thread
.
java
:
748
)
ts
=
2019
-
09
-
14
21
:
15
:
19
;
thread_name
=
http
-
nio
-
7744
-
exec
-
5
;
id
=
14
;
is_daemon
=
true
;
priority
=
5
;
TCCL
=
org
.
springframework
.
boot
.
web
.
embedded
.
tomcat
.
TomcatEmbeddedWebappClassLoader@7c136917
@org
.
springframework
.
web
.
servlet
.
DispatcherServlet
.
doService
()
at org
.
springframework
.
web
.
servlet
.
FrameworkServlet
.
processRequest
(
FrameworkServlet
.
java
:
974
)
at org
.
springframework
.
web
.
servlet
.
FrameworkServlet
.
doGet
(
FrameworkServlet
.
java
:
866
)
at javax
.
servlet
.
http
.
HttpServlet
.
service
(
HttpServlet
.
java
:
635
)
at org
.
springframework
.
web
.
servlet
.
FrameworkServlet
.
service
(
FrameworkServlet
.
java
:
851
)
at javax
.
servlet
.
http
.
HttpServlet
.
service
(
HttpServlet
.
java
:
742
)
at org
.
apache
.
catalina
.
core
.
ApplicationFilterChain
.
internalDoFilter
(
ApplicationFilterChain
.
java
:
231
)
at org
.
apache
.
catalina
.
core
.
ApplicationFilterChain
.
doFilter
(
ApplicationFilterChain
.
java
:
166
)
at org
.
apache
.
tomcat
.
websocket
.
server
.
WsFilter
.
doFilter
(
WsFilter
.
java
:
52
)
at org
.
apache
.
catalina
.
core
.
ApplicationFilterChain
.
internalDoFilter
(
ApplicationFilterChain
.
java
:
193
)
at org
.
apache
.
catalina
.
core
.
ApplicationFilterChain
.
doFilter
(
ApplicationFilterChain
.
java
:
166
)
at org
.
springframework
.
web
.
filter
.
RequestContextFilter
.
doFilterInternal
(
RequestContextFilter
.
java
:
99
)
at org
.
springframework
.
web
.
filter
.
OncePerRequestFilter
.
doFilter
(
OncePerRequestFilter
.
java
:
107
)
at org
.
apache
.
catalina
.
core
.
ApplicationFilterChain
.
internalDoFilter
(
ApplicationFilterChain
.
java
:
193
)
at org
.
apache
.
catalina
.
core
.
ApplicationFilterChain
.
doFilter
(
ApplicationFilterChain
.
java
:
166
)
at org
.
springframework
.
web
.
filter
.
HttpPutFormContentFilter
.
doFilterInternal
(
HttpPutFormContentFilter
.
java
:
109
)
at org
.
springframework
.
web
.
filter
.
OncePerRequestFilter
.
doFilter
(
OncePerRequestFilter
.
java
:
107
)
at org
.
apache
.
catalina
.
core
.
ApplicationFilterChain
.
internalDoFilter
(
ApplicationFilterChain
.
java
:
193
)
at org
.
apache
.
catalina
.
core
.
ApplicationFilterChain
.
doFilter
(
ApplicationFilterChain
.
java
:
166
)
at org
.
springframework
.
web
.
filter
.
HiddenHttpMethodFilter
.
doFilterInternal
(
HiddenHttpMethodFilter
.
java
:
81
)
at org
.
springframework
.
web
.
filter
.
OncePerRequestFilter
.
doFilter
(
OncePerRequestFilter
.
java
:
107
)
at org
.
apache
.
catalina
.
core
.
ApplicationFilterChain
.
internalDoFilter
(
ApplicationFilterChain
.
java
:
193
)
at org
.
apache
.
catalina
.
core
.
ApplicationFilterChain
.
doFilter
(
ApplicationFilterChain
.
java
:
166
)
at org
.
springframework
.
web
.
filter
.
CharacterEncodingFilter
.
doFilterInternal
(
CharacterEncodingFilter
.
java
:
200
)
at org
.
springframework
.
web
.
filter
.
OncePerRequestFilter
.
doFilter
(
OncePerRequestFilter
.
java
:
107
)
at org
.
apache
.
catalina
.
core
.
ApplicationFilterChain
.
internalDoFilter
(
ApplicationFilterChain
.
java
:
193
)
at org
.
apache
.
catalina
.
core
.
ApplicationFilterChain
.
doFilter
(
ApplicationFilterChain
.
java
:
166
)
at org
.
apache
.
catalina
.
core
.
StandardWrapperValve
.
invoke
(
StandardWrapperValve
.
java
:
198
)
at org
.
apache
.
catalina
.
core
.
StandardContextValve
.
invoke
(
StandardContextValve
.
java
:
96
)
at org
.
apache
.
catalina
.
authenticator
.
AuthenticatorBase
.
invoke
(
AuthenticatorBase
.
java
:
496
)
at org
.
apache
.
catalina
.
core
.
StandardHostValve
.
invoke
(
StandardHostValve
.
java
:
140
)
at org
.
apache
.
catalina
.
valves
.
ErrorReportValve
.
invoke
(
ErrorReportValve
.
java
:
81
)
at org
.
apache
.
catalina
.
core
.
StandardEngineValve
.
invoke
(
StandardEngineValve
.
java
:
87
)
at org
.
apache
.
catalina
.
connector
.
CoyoteAdapter
.
service
(
CoyoteAdapter
.
java
:
342
)
at org
.
apache
.
coyote
.
http11
.
Http11Processor
.
service
(
Http11Processor
.
java
:
803
)
at org
.
apache
.
coyote
.
AbstractProcessorLight
.
process
(
AbstractProcessorLight
.
java
:
66
)
at org
.
apache
.
coyote
.
AbstractProtocol$ConnectionHandler
.
process
(
AbstractProtocol
.
java
:
790
)
at org
.
apache
.
tomcat
.
util
.
net
.
NioEndpoint$SocketProcessor
.
doRun
(
NioEndpoint
.
java
:
1468
)
at org
.
apache
.
tomcat
.
util
.
net
.
SocketProcessorBase
.
run
(
SocketProcessorBase
.
java
:
49
)
at java
.
util
.
concurrent
.
ThreadPoolExecutor
.
runWorker
(
ThreadPoolExecutor
.
java
:
1149
)
at java
.
util
.
concurrent
.
ThreadPoolExecutor$Worker
.
run
(
ThreadPoolExecutor
.
java
:
624
)
at org
.
apache
.
tomcat
.
util
.
threads
.
TaskThread$WrappingRunnable
.
run
(
TaskThread
.
java
:
61
)
at java
.
lang
.
Thread
.
run
(
Thread
.
java
:
748
)
从stack日志上可以很直观的看出DispatchServlet的调用栈,那么这么长的路径,该trace哪个类呢(这里跳过spring mvc中的过滤器的trace过程,实际排查的时候也trace了一遍,但这诡异的时间消耗不是由这里过滤器产生的)?有一定经验的老司机从名字上大概也能猜出来从哪里下手比较好,那就是org.apache.coyote.http11.Http11Processor.service,从名字上看,http1.1处理器,这可能是一个比较好的切入点。下面来trace一下:
[ arthas@24851 ] $ trace org . apache . coyote . http11 . Http11Processor service Press Q or Ctrl + C to abort . Affect ( class - cnt : 1 , method - cnt : 1 ) cost in 269 ms . `---ts=2019-09-14 21:22:51;thread_name=http-nio-7744-exec-8;id=17;is_daemon=true;priority=5;TCCL=org.springframework.boot.loader.LaunchedURLClassLoader@20ad9418 ` ---[ 131.650285ms ] org . apache . coyote . http11 . Http11Processor : service () +---[ 0.036851ms ] org . apache . coyote . Request : getRequestProcessor () #667 +---[ 0.009986ms ] org . apache . coyote . RequestInfo : setStage () #668 +---[ 0.008928ms ] org . apache . coyote . http11 . Http11Processor : setSocketWrapper () #671 +---[ 0.013236ms ] org . apache . coyote . http11 . Http11InputBuffer : init () #672 +---[ 0.00981ms ] org . apache . coyote . http11 . Http11OutputBuffer : init () #673 +---[ min = 0.00213ms , max = 0.007317ms , total = 0.009447ms , count = 2 ] org . apache . coyote . http11 . Http11Processor : getErrorState () #683 +---[ min = 0.002098ms , max = 0.008888ms , total = 0.010986ms , count = 2 ] org . apache . coyote . ErrorState : isError () #683 +---[ min = 0.002448ms , max = 0.007149ms , total = 0.009597ms , count = 2 ] org . apache . coyote . http11 . Http11Processor : isAsync () #683 +---[ min = 0.002399ms , max = 0.00852ms , total = 0.010919ms , count = 2 ] org . apache . tomcat . util . net . AbstractEndpoint : isPaused () #683 +---[ min = 0.033587ms , max = 0.11832ms , total = 0.151907ms , count = 2 ] org . apache . coyote . http11 . Http11InputBuffer : parseRequestLine () #687 +---[ 0.005384ms ] org . apache . tomcat . util . net . AbstractEndpoint : isPaused () #695 +---[ 0.007924ms ] org . apache . coyote . Request : getMimeHeaders () #702 +---[ 0.006744ms ] org . apache . tomcat . util . net . AbstractEndpoint : getMaxHeaderCount () #702 +---[ 0.012574ms ] org . apache . tomcat . util . http . MimeHeaders : setLimit () #702 +---[ 0.14319ms ] org . apache . coyote . http11 . Http11InputBuffer : parseHeaders () #703 +---[ 0.003997ms ] org . apache . coyote . Request : getMimeHeaders () #743 +---[ 0.026561ms ] org . apache . tomcat . util . http . MimeHeaders : values () #743 +---[ min = 0.002869ms , max = 0.01203ms , total = 0.014899ms , count = 2 ] java . util . Enumeration : hasMoreElements () #745 +---[ 0.070114ms ] java . util . Enumeration : nextElement () #746 +---[ 0.010921ms ] java . lang . String : toLowerCase () #746 +---[0.008453ms] java.lang.String:contains() #746 +---[ 0.002698ms ] org . apache . coyote . http11 . Http11Processor : getErrorState () #775 +---[ 0.00307ms ] org . apache . coyote . ErrorState : isError () #775 +---[ 0.002708ms ] org . apache . coyote . RequestInfo : setStage () #777 +---[ 0.171139ms ] org . apache . coyote . http11 . Http11Processor : prepareRequest () #779 +---[ 0.009349ms ] org . apache . tomcat . util . net . SocketWrapperBase : decrementKeepAlive () #794 +---[ 0.002574ms ] org . apache . coyote . http11 . Http11Processor : getErrorState () #800 +---[ 0.002696ms ] org . apache . coyote . ErrorState : isError () #800 +---[ 0.002499ms ] org . apache . coyote . RequestInfo : setStage () #802 +---[ 0.005641ms ] org . apache . coyote . http11 . Http11Processor : getAdapter () #803 +---[ 129.868916ms ] org . apache . coyote . Adapter : service () #803 +---[ 0.003859ms ] org . apache . coyote . http11 . Http11Processor : getErrorState () #809 +---[ 0.002365ms ] org . apache . coyote . ErrorState : isError () #809 +---[ 0.003844ms ] org . apache . coyote . http11 . Http11Processor : isAsync () #809 +---[ 0.002382ms ] org . apache . coyote . Response : getStatus () #809 +---[ 0.002476ms ] org . apache . coyote . http11 . Http11Processor : statusDropsConnection () #809 +---[ 0.002284ms ] org . apache . coyote . RequestInfo : setStage () #838 +---[ 0.00222ms ] org . apache . coyote . http11 . Http11Processor : isAsync () #839 +---[ 0.037873ms ] org . apache . coyote . http11 . Http11Processor : endRequest () #843 +---[ 0.002188ms ] org . apache . coyote . RequestInfo : setStage () #845 +---[ 0.002112ms ] org . apache . coyote . http11 . Http11Processor : getErrorState () #849 +---[ 0.002063ms ] org . apache . coyote . ErrorState : isError () #849 +---[ 0.002504ms ] org . apache . coyote . http11 . Http11Processor : isAsync () #853 +---[ 0.009808ms ] org . apache . coyote . Request : updateCounters () #854 +---[ 0.002008ms ] org . apache . coyote . http11 . Http11Processor : getErrorState () #855 +---[ 0.002192ms ] org . apache . coyote . ErrorState : isIoAllowed () #855 +---[ 0.01968ms ] org . apache . coyote . http11 . Http11InputBuffer : nextRequest () #856 +---[ 0.010065ms ] org . apache . coyote . http11 . Http11OutputBuffer : nextRequest () #857 +---[ 0.002576ms ] org . apache . coyote . RequestInfo : setStage () #870 +---[ 0.016599ms ] org . apache . coyote . http11 . Http11Processor : processSendfile () #872 +---[ 0.008182ms ] org . apache . coyote . http11 . Http11InputBuffer : getParsingRequestLinePhase () #688 +---[ 0.0075ms ] org . apache . coyote . http11 . Http11Processor : handleIncompleteRequestLineRead () #690 +---[ 0.001979ms ] org . apache . coyote . RequestInfo : setStage () #875 +---[ 0.001981ms ] org . apache . coyote . http11 . Http11Processor : getErrorState () #877 +---[ 0.001934ms ] org . apache . coyote . ErrorState : isError () #877 +---[ 0.001995ms ] org . apache . tomcat . util . net . AbstractEndpoint : isPaused () #877 +---[ 0.002403ms ] org . apache . coyote . http11 . Http11Processor : isAsync () #879 `---[0.006176ms] org.apache.coyote.http11.Http11Processor:isUpgrade() #881
日志里有一个129ms的耗时点(时间比没开arthas的时候更长是因为arthas本身带来的性能消耗,所以生产环境小心使用),这个就是要找的问题点。
打问题点找到了,那怎么定位是什么导致的问题呢,又如何解决呢?
继续trace吧,细化到具体的代码块或者内容。trace由于性能考虑,不会展示所有的调用路径,如果调用路径过深,只有手动深入trace,原则就是trace耗时长的那个方法:
[ arthas@24851 ] $ trace org . apache . coyote . Adapter service Press Q or Ctrl + C to abort . Affect ( class - cnt : 1 , method - cnt : 1 ) cost in 608 ms . `---ts=2019-09-14 21:34:33;thread_name=http-nio-7744-exec-1;id=10;is_daemon=true;priority=5;TCCL=org.springframework.boot.loader.LaunchedURLClassLoader@20ad9418 ` ---[ 81.70999ms ] org . apache . catalina . connector . CoyoteAdapter : service () +---[ 0.032546ms ] org . apache . coyote . Request : getNote () #302 +---[ 0.007148ms ] org . apache . coyote . Response : getNote () #303 +---[ 0.007475ms ] org . apache . catalina . connector . Connector : getXpoweredBy () #324 +---[ 0.00447ms ] org . apache . coyote . Request : getRequestProcessor () #331 +---[ 0.007902ms ] java . lang . ThreadLocal : get () #331 +---[ 0.006522ms ] org . apache . coyote . RequestInfo : setWorkerThreadName () #331 +---[ 73.793798ms ] org . apache . catalina . connector . CoyoteAdapter : postParseRequest () #336 +---[ 0.001536ms ] org . apache . catalina . connector . Connector : getService () #339 +---[ 0.004469ms ] org . apache . catalina . Service : getContainer () #339 +---[ 0.007074ms ] org . apache . catalina . Engine : getPipeline () #339 +---[ 0.004334ms ] org . apache . catalina . Pipeline : isAsyncSupported () #339 +---[ 0.002466ms ] org . apache . catalina . connector . Request : setAsyncSupported () #339 +---[ 6.01E-4ms ] org . apache . catalina . connector . Connector : getService () #342 +---[ 0.001859ms ] org . apache . catalina . Service : getContainer () #342 +---[ 9.65E-4ms ] org . apache . catalina . Engine : getPipeline () #342 +---[ 0.005231ms ] org . apache . catalina . Pipeline : getFirst () #342 +---[ 7.239154ms ] org . apache . catalina . Valve : invoke () #342 +---[ 0.006904ms ] org . apache . catalina . connector . Request : isAsync () #345 +---[ 0.00509ms ] org . apache . catalina . connector . Request : finishRequest () #372 +---[ 0.051461ms ] org . apache . catalina . connector . Response : finishResponse () #373 +---[ 0.007244ms ] java . util . concurrent . atomic . AtomicBoolean :< init >() #379 +---[ 0.007314ms ] org . apache . coyote . Response : action () #380 +---[ 0.004518ms ] org . apache . catalina . connector . Request : isAsyncCompleting () #382 +---[ 0.001072ms ] org . apache . catalina . connector . Request : getContext () #394 +---[ 0.007166ms ] java . lang . System : currentTimeMillis () #401 +---[ 0.004367ms ] org . apache . coyote . Request : getStartTime () #401 +---[ 0.011483ms ] org . apache . catalina . Context : logAccess () #401 +---[ 0.0014ms ] org . apache . coyote . Request : getRequestProcessor () #406 +---[ min = 8.0E-4ms , max = 9.22E-4ms , total = 0.001722ms , count = 2 ] java . lang . Integer :< init >() #406 +---[ 0.001082ms ] java . lang . reflect . Method : invoke () #406 +---[ 0.001851ms ] org . apache . coyote . RequestInfo : setWorkerThreadName () #406 +---[ 0.035805ms ] org . apache . catalina . connector . Request : recycle () #410 `---[0.007849ms] org.apache.catalina.connector.Response:recycle() #411
一段无聊的手动深入trace之后………………
[ arthas@24851 ] $ trace org . apache . catalina . webresources . AbstractArchiveResourceSet getArchiveEntries Press Q or Ctrl + C to abort . Affect ( class - cnt : 4 , method - cnt : 2 ) cost in 150 ms . `---ts=2019-09-14 21:36:26;thread_name=http-nio-7744-exec-3;id=12;is_daemon=true;priority=5;TCCL=org.springframework.boot.loader.LaunchedURLClassLoader@20ad9418 ` ---[ 75.743681ms ] org . apache . catalina . webresources . JarWarResourceSet : getArchiveEntries () +---[ 0.025731ms ] java . util . HashMap :< init >() #106 +---[ 0.097729ms ] org . apache . catalina . webresources . JarWarResourceSet : openJarFile () #109 +---[ 0.091037ms ] java . util . jar . JarFile : getJarEntry () #110 +---[ 0.096325ms ] java . util . jar . JarFile : getInputStream () #111 +---[ 0.451916ms ] org . apache . catalina . webresources . TomcatJarInputStream :< init >() #113 +---[ min = 0.001175ms , max = 0.001176ms , total = 0.002351ms , count = 2 ] java . lang . Integer :< init >() #114 +---[ 0.00104ms ] java . lang . reflect . Method : invoke () #114 +---[ 0.045105ms ] org . apache . catalina . webresources . TomcatJarInputStream : getNextJarEntry () #114 +---[ min = 5.02E-4ms , max = 0.008531ms , total = 0.028864ms , count = 31 ] java . util . jar . JarEntry : getName () #116 +---[ min = 5.39E-4ms , max = 0.022805ms , total = 0.054647ms , count = 31 ] java . util . HashMap : put () #116 +---[ min = 0.004452ms , max = 34.479307ms , total = 74.206249ms , count = 31 ] org . apache . catalina . webresources . TomcatJarInputStream : getNextJarEntry () #117 +---[ 0.018358ms ] org . apache . catalina . webresources . TomcatJarInputStream : getManifest () #119 +---[ 0.006429ms ] org . apache . catalina . webresources . JarWarResourceSet : setManifest () #120 +---[ 0.010904ms ] org . apache . tomcat . util . compat . JreCompat : isJre9Available () #121 +---[ 0.003307ms ] org . apache . catalina . webresources . TomcatJarInputStream : getMetaInfEntry () #133 +---[ 5.5E-4ms ] java . util . jar . JarEntry : getName () #135 +---[ 6.42E-4ms ] java . util . HashMap : put () #135 +---[ 0.001981ms ] org . apache . catalina . webresources . TomcatJarInputStream : getManifestEntry () #137 +---[ 0.064484ms ] org . apache . catalina . webresources . TomcatJarInputStream : close () #141 +---[ 0.007961ms ] org . apache . catalina . webresources . JarWarResourceSet : closeJarFile () #151 `---[0.004643ms] java.io.InputStream:close() #155
发现了一个值得暂停思考的点:
+---[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 该方法的入参
-
[arthas@24851]$ watch org.apache.catalina.webresources.TomcatJarInputStream createZipEntry "{params[0]}" -
Press Q or Ctrl+C to abort. -
Affect(class-cnt:1 , method-cnt:1) cost in 27 ms. -
ts=2019-09-14 21:51:14; [cost=0.14547ms] result=@ArrayList[ -
@String[META-INF/], -
] -
ts=2019-09-14 21:51:14; [cost=0.048028ms] result=@ArrayList[ -
@String[META-INF/MANIFEST.MF], -
] -
ts=2019-09-14 21:51:14; [cost=0.046071ms] result=@ArrayList[ -
@String[META-INF/resources/], -
] -
ts=2019-09-14 21:51:14; [cost=0.033855ms] result=@ArrayList[ -
@String[META-INF/resources/swagger-ui.html], -
] -
ts=2019-09-14 21:51:14; [cost=0.039138ms] result=@ArrayList[ -
@String[META-INF/resources/webjars/], -
] -
ts=2019-09-14 21:51:14; [cost=0.033701ms] result=@ArrayList[ -
@String[META-INF/resources/webjars/springfox-swagger-ui/], -
] -
ts=2019-09-14 21:51:14; [cost=0.033644ms] result=@ArrayList[ -
@String[META-INF/resources/webjars/springfox-swagger-ui/favicon-16x16.png], -
] -
ts=2019-09-14 21:51:14; [cost=0.033976ms] result=@ArrayList[ -
@String[META-INF/resources/webjars/springfox-swagger-ui/springfox.css], -
] -
ts=2019-09-14 21:51:14; [cost=0.032818ms] result=@ArrayList[ -
@String[META-INF/resources/webjars/springfox-swagger-ui/swagger-ui-standalone-preset.js.map], -
] -
ts=2019-09-14 21:51:14; [cost=0.04651ms] result=@ArrayList[ -
@String[META-INF/resources/webjars/springfox-swagger-ui/swagger-ui.css], -
] -
ts=2019-09-14 21:51:14; [cost=0.034793ms] result=@ArrayList[ -
@String[META-INF/resources/webjars/springfox-swagger-ui/swagger-ui.js.map],
这下直接看到了具体加载的资源名,这么熟悉的名字: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处理请求的核心类图
为什么每次请求会加载Jar包内的静态资源 关键在于org.apache.catalina.mapper.Mapper#internalMapWrapper这个方法,该版本下处理请求的方式有问题,导致每次都校验静态资源。
为什么连续请求不会出现问题
因为Tomcat对于这种静态资源的解析是有缓存的,优先从缓存查找,缓存过期后再重新解析。具体参考org.apache.catalina.webresources.Cache,默认过期时间ttl是5000ms。
为什么本地不会复现
其实确切的说,是通过spring-boot打包插件后不能复现。由于启动方式的不同,tomcat使用了不同的类去处理静态资源,所以没问题
如何解决
升级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>
升级spring boot版本
springboot 2.1.0.RELEASE中的tomcat embed版本已经大于8.5.31了,所以直接将springboot升级至该版本及以上版本就可以解决此问题。
看完打开支付宝扫一扫领个红包吧!

浙公网安备 33010602011771号