實戰!Arthas定位介面的超時問題,直接起飛!

👉 這是一個或許對你有用的社群
🐱 一對一交流/面試小冊/簡歷最佳化/求職解惑,歡迎加入芋道快速開發平臺知識星球。下面是星球提供的部分資料:
👉這是一個或許對你有用的開源專案
國產 Star 破 10w+ 的開源專案,前端包括管理後臺 + 微信小程式,後端支援單體和微服務架構。
功能涵蓋 RBAC 許可權、SaaS 多租戶、資料許可權、商城、支付、工作流、大屏報表、微信公眾號、CRM 等等功能:
  • Boot 倉庫:https://gitee.com/zhijiantianya/ruoyi-vue-pro
  • Cloud 倉庫:https://gitee.com/zhijiantianya/yudao-cloud
  • 影片教程:https://doc.iocoder.cn
【國內首批】支援 JDK 21 + SpringBoot 3.2.2、JDK 8 + Spring Boot 2.7.18 雙版本 

背景

公司有個渠道系統,專門對接三方渠道使用,沒有什麼業務邏輯,主要是轉換報文和引數校驗之類的工作,起著一個承上啟下的作用。
最近在最佳化介面的響應時間,優化了程式碼之後,但是時間還是達不到要求;有一個詭異的100ms左右的耗時問題,在介面中列印了請求處理時間後,和呼叫方的響應時間還有差了100ms左右。比如程式裡記錄150ms,但是呼叫方等待時間卻為250ms左右。
下面記錄下當時詳細的定位&解決流程(其實解決很簡單,關鍵在於怎麼定位並找到解決問題的方法)
基於 Spring Boot + MyBatis Plus + Vue & Element 實現的後臺管理系統 + 使用者小程式,支援 RBAC 動態許可權、多租戶、資料許可權、工作流、三方登入、支付、簡訊、商城等功能
  • 專案地址:https://github.com/YunaiV/ruoyi-vue-pro
  • 影片教程:https://doc.iocoder.cn/video/

定位過程

分析程式碼

渠道系統是一個常見的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到渠道系統的網路。

> 基於 Spring Cloud Alibaba + Gateway + Nacos + RocketMQ + Vue & Element 實現的後臺管理系統 + 使用者小程式,支援 RBAC 動態許可權、多租戶、資料許可權、工作流、三方登入、支付、簡訊、商城等功能

>

> * 專案地址:<https:

//github.com/YunaiV/yudao-cloud>

> * 影片教程:<https:

//doc.iocoder.cn/video/>

# 由於日誌是沒問題的,這裡直接複製上面日誌了

[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

success

              http: 

200

               dns: 

0.001

s

          redirect: 

0.000

s

      time_connect: 

0.001

s

   time_appconnect: 

0.000

s

  time_pretransfer: 

0.001

s

time_starttransfer: 

0.073

s

     size_download: 

7

bytes

    speed_download: 

95.000

B/s

                  ----------

        time_total: 

0.073

s 請求總耗時

從curl日誌上看,透過迴環地址呼叫一個空介面耗時也有73ms。這就奇怪了,跳過了中間所有呼叫節點(包括過濾器&攔截器之類),直接請求應用一個空介面,都有73ms的耗時,再請求一次看看:

[jboss

@VM

_10_91_centos tmp]$ curl -w 

"@curl-time.txt"

 http:

//127.0.0.1:7744/send

success

              http: 

200

               dns: 

0.001

s

          redirect: 

0.000

s

      time_connect: 

0.001

s

   time_appconnect: 

0.000

s

  time_pretransfer: 

0.001

s

time_starttransfer: 

0.003

s

     size_download: 

7

bytes

    speed_download: 

2611.000

B/s

                  ----------

        time_total: 

0.003

s

更奇怪的是,第二次請求耗時就正常了,變成了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可以幫助你解決:
  • 這個類從哪個 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) 

costin

 508 

ms

.

`---

ts

=

2019

-

09

-

1421

:

07

:

44

;thread_name=http-nio-

7744

-exec-

2

;id=

11

;is_daemon=

true

;priority=

5

;TCCL=org.springframework.boot.web.embedded.tomcat.TomcatEmbeddedWebappClassLoader@

7

c136917

    `---[

2.952142

ms] org.springframework.web.servlet.DispatcherServlet:buildLocaleContext()
`---ts=

2019

-

09

-

1421

:

07

:

44

;thread_name=http-nio-

7744

-exec-

2

;id=

11

;is_daemon=

true

;priority=

5

;TCCL=org.springframework.boot.web.embedded.tomcat.TomcatEmbeddedWebappClassLoader@

7

c136917

    `---[

18.08903

ms] org.springframework.web.servlet.DispatcherServlet:doService()

        +---[

0.041346

ms] org.apache.commons.logging.Log:isDebugEnabled() #

889

        +---[

0.022398

ms] org.springframework.web.util.WebUtils:isIncludeRequest() #

898

        +---[

0.014904

ms] org.springframework.web.servlet.DispatcherServlet:getWebApplicationContext() #

910

        +---[

1.071879

ms] javax.servlet.http.HttpServletRequest:setAttribute() #

910

        +---[

0.020977

ms] javax.servlet.http.HttpServletRequest:setAttribute() #

911

        +---[

0.017073

ms] javax.servlet.http.HttpServletRequest:setAttribute() #

912

        +---[

0.218277

ms] org.springframework.web.servlet.DispatcherServlet:getThemeSource() #

913

        |   `---[

0.137568

ms] org.springframework.web.servlet.DispatcherServlet:getThemeSource()

        |       `---[min=

0.00783

ms,max=

0.014251

ms,total=

0.022081

ms,count=

2

] org.springframework.web.servlet.DispatcherServlet:getWebApplicationContext() #

782

        +---[

0.019363

ms] javax.servlet.http.HttpServletRequest:setAttribute() #

913

        +---[

0.070694

ms] org.springframework.web.servlet.FlashMapManager:retrieveAndUpdate() #

916

        +---[

0.01839

ms] org.springframework.web.servlet.FlashMap:<init>() #

920

        +---[

0.016943

ms] javax.servlet.http.HttpServletRequest:setAttribute() #

920

        +---[

0.015268

ms] javax.servlet.http.HttpServletRequest:setAttribute() #

921

        +---[

15.050124

ms] org.springframework.web.servlet.DispatcherServlet:doDispatch() #

925

        |   `---[

14.943477

ms] org.springframework.web.servlet.DispatcherServlet:doDispatch()

        |       +---[

0.019135

ms] org.springframework.web.context.request.async.WebAsyncUtils:getAsyncManager() #

953

        |       +---[

2.108373

ms] org.springframework.web.servlet.DispatcherServlet:checkMultipart() #

960

        |       |   `---[

2.004436

ms] org.springframework.web.servlet.DispatcherServlet:checkMultipart()

        |       |       `---[

1.890845

ms] org.springframework.web.multipart.MultipartResolver:isMultipart() #

1117

        |       +---[

2.054361

ms] org.springframework.web.servlet.DispatcherServlet:getHandler() #

964

        |       |   `---[

1.961963

ms] org.springframework.web.servlet.DispatcherServlet:getHandler()

        |       |       +---[

0.02051

ms] java.util.List:iterator() #

1183

        |       |       +---[min=

0.003805

ms,max=

0.009641

ms,total=

0.013446

ms,count=

2

] java.util.Iterator:hasNext() #

1183

        |       |       +---[min=

0.003181

ms,max=

0.009751

ms,total=

0.012932

ms,count=

2

] java.util.Iterator:next() #

1183

        |       |       +---[min=

0.005841

ms,max=

0.015308

ms,total=

0.021149

ms,count=

2

] org.apache.commons.logging.Log:isTraceEnabled() #

1184

        |       |       `---[min=

0.474739

ms,max=

1.19145

ms,total=

1.666189

ms,count=

2

] org.springframework.web.servlet.HandlerMapping:getHandler() #

1188

        |       +---[

0.013071

ms] org.springframework.web.servlet.HandlerExecutionChain:getHandler() #

971

        |       +---[

0.372236

ms] org.springframework.web.servlet.DispatcherServlet:getHandlerAdapter() #

971

        |       |   `---[

0.280073

ms] org.springframework.web.servlet.DispatcherServlet:getHandlerAdapter()

        |       |       +---[

0.004804

ms] java.util.List:iterator() #

1224

        |       |       +---[

0.003668

ms] java.util.Iterator:hasNext() #

1224

        |       |       +---[

0.003038

ms] java.util.Iterator:next() #

1224

        |       |       +---[

0.006451

ms] org.apache.commons.logging.Log:isTraceEnabled() #

1225

        |       |       `---[

0.012683

ms] org.springframework.web.servlet.HandlerAdapter:supports() #

1228

        |       +---[

0.012848

ms] javax.servlet.http.HttpServletRequest:getMethod() #

974

        |       +---[

0.013132

ms] java.lang.String:equals() #

975

        |       +---[

0.003025

ms] org.springframework.web.servlet.HandlerExecutionChain:getHandler() #

977

        |       +---[

0.008095

ms] org.springframework.web.servlet.HandlerAdapter:getLastModified() #

977

        |       +---[

0.006596

ms] org.apache.commons.logging.Log:isDebugEnabled() #

978

        |       +---[

0.018024

ms] org.springframework.web.context.request.ServletWebRequest:<init>() #

981

        |       +---[

0.017869

ms] org.springframework.web.context.request.ServletWebRequest:checkNotModified() #

981

        |       +---[

0.038542

ms] org.springframework.web.servlet.HandlerExecutionChain:applyPreHandle() #

986

        |       +---[

0.00431

ms] org.springframework.web.servlet.HandlerExecutionChain:getHandler() #

991

        |       +---[

4.248493

ms] org.springframework.web.servlet.HandlerAdapter:handle() #

991

        |       +---[

0.014805

ms] org.springframework.web.context.request.async.WebAsyncManager:isConcurrentHandlingStarted() #

993

        |       +---[

1.444994

ms] org.springframework.web.servlet.DispatcherServlet:applyDefaultViewName() #

997

        |       |   `---[

0.067631

ms] org.springframework.web.servlet.DispatcherServlet:applyDefaultViewName()

        |       +---[

0.012027

ms] org.springframework.web.servlet.HandlerExecutionChain:applyPostHandle() #

998

        |       +---[

0.373997

ms] org.springframework.web.servlet.DispatcherServlet:processDispatchResult() #

1008

        |       |   `---[

0.197004

ms] org.springframework.web.servlet.DispatcherServlet:processDispatchResult()

        |       |       +---[

0.007074

ms] org.apache.commons.logging.Log:isDebugEnabled() #

1075

        |       |       +---[

0.005467

ms] org.springframework.web.context.request.async.WebAsyncUtils:getAsyncManager() #

1081

        |       |       +---[

0.004054

ms] org.springframework.web.context.request.async.WebAsyncManager:isConcurrentHandlingStarted() #

1081

        |       |       `---[

0.011988

ms] org.springframework.web.servlet.HandlerExecutionChain:triggerAfterCompletion() #

1087

        |       `---[

0.004015

ms] org.springframework.web.context.request.async.WebAsyncManager:isConcurrentHandlingStarted() #

1018

        +---[

0.005055

ms] org.springframework.web.context.request.async.WebAsyncUtils:getAsyncManager() #

928

        `---[

0.003422

ms] 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.001

s

          redirect: 

0.000

s

      time_connect: 

0.001

s

   time_appconnect: 

0.000

s

  time_pretransfer: 

0.001

s

time_starttransfer: 

0.115

s

     size_download: 

7

bytes

    speed_download: 

60.000

B/s

                  ----------

        time_total: 

0.115

s

本次呼叫,呼叫端時間花費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) 

costin

 495 

ms

.

ts

=

2019

-

09

-

1421

:

15

:

19

;thread_name=http-nio-

7744

-exec-

5

;id=

14

;is_daemon=

true

;priority=

5

;TCCL=org.springframework.boot.web.embedded.tomcat.TomcatEmbeddedWebappClassLoader@

7

c136917

@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

-

1421

:

15

:

19

;thread_name=http-nio-

7744

-exec-

5

;id=

14

;is_daemon=

true

;priority=

5

;TCCL=org.springframework.boot.web.embedded.tomcat.TomcatEmbeddedWebappClassLoader@

7

c136917

@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) 

costin

 269 

ms

.

`---

ts

=

2019

-

09

-

1421

:

22

:

51

;thread_name=http-nio-

7744

-exec-

8

;id=

17

;is_daemon=

true

;priority=

5

;TCCL=org.springframework.boot.loader.LaunchedURLClassLoader@

20

ad9418

    `---[

131.650285

ms] org.apache.coyote.http11.Http11Processor:service()

        +---[

0.036851

ms] org.apache.coyote.Request:getRequestProcessor() #

667

        +---[

0.009986

ms] org.apache.coyote.RequestInfo:setStage() #

668

        +---[

0.008928

ms] org.apache.coyote.http11.Http11Processor:setSocketWrapper() #

671

        +---[

0.013236

ms] org.apache.coyote.http11.Http11InputBuffer:init() #

672

        +---[

0.00981

ms] org.apache.coyote.http11.Http11OutputBuffer:init() #

673

        +---[min=

0.00213

ms,max=

0.007317

ms,total=

0.009447

ms,count=

2

] org.apache.coyote.http11.Http11Processor:getErrorState() #

683

        +---[min=

0.002098

ms,max=

0.008888

ms,total=

0.010986

ms,count=

2

] org.apache.coyote.ErrorState:isError() #

683

        +---[min=

0.002448

ms,max=

0.007149

ms,total=

0.009597

ms,count=

2

] org.apache.coyote.http11.Http11Processor:isAsync() #

683

        +---[min=

0.002399

ms,max=

0.00852

ms,total=

0.010919

ms,count=

2

] org.apache.tomcat.util.net.AbstractEndpoint:isPaused() #

683

        +---[min=

0.033587

ms,max=

0.11832

ms,total=

0.151907

ms,count=

2

] org.apache.coyote.http11.Http11InputBuffer:parseRequestLine() #

687

        +---[

0.005384

ms] org.apache.tomcat.util.net.AbstractEndpoint:isPaused() #

695

        +---[

0.007924

ms] org.apache.coyote.Request:getMimeHeaders() #

702

        +---[

0.006744

ms] org.apache.tomcat.util.net.AbstractEndpoint:getMaxHeaderCount() #

702

        +---[

0.012574

ms] org.apache.tomcat.util.http.MimeHeaders:setLimit() #

702

        +---[

0.14319

ms] org.apache.coyote.http11.Http11InputBuffer:parseHeaders() #

703

        +---[

0.003997

ms] org.apache.coyote.Request:getMimeHeaders() #

743

        +---[

0.026561

ms] org.apache.tomcat.util.http.MimeHeaders:values() #

743

        +---[min=

0.002869

ms,max=

0.01203

ms,total=

0.014899

ms,count=

2

] java.util.Enumeration:hasMoreElements() #

745

        +---[

0.070114

ms] java.util.Enumeration:nextElement() #

746

        +---[

0.010921

ms] java.lang.String:toLowerCase() #

746

        +---[

0.008453

ms] java.lang.String:contains() #

746

        +---[

0.002698

ms] org.apache.coyote.http11.Http11Processor:getErrorState() #

775

        +---[

0.00307

ms] org.apache.coyote.ErrorState:isError() #

775

        +---[

0.002708

ms] org.apache.coyote.RequestInfo:setStage() #

777

        +---[

0.171139

ms] org.apache.coyote.http11.Http11Processor:prepareRequest() #

779

        +---[

0.009349

ms] org.apache.tomcat.util.net.SocketWrapperBase:decrementKeepAlive() #

794

        +---[

0.002574

ms] org.apache.coyote.http11.Http11Processor:getErrorState() #

800

        +---[

0.002696

ms] org.apache.coyote.ErrorState:isError() #

800

        +---[

0.002499

ms] org.apache.coyote.RequestInfo:setStage() #

802

        +---[

0.005641

ms] org.apache.coyote.http11.Http11Processor:getAdapter() #

803

        +---[

129.868916

ms] org.apache.coyote.Adapter:service() #

803

        +---[

0.003859

ms] org.apache.coyote.http11.Http11Processor:getErrorState() #

809

        +---[

0.002365

ms] org.apache.coyote.ErrorState:isError() #

809

        +---[

0.003844

ms] org.apache.coyote.http11.Http11Processor:isAsync() #

809

        +---[

0.002382

ms] org.apache.coyote.Response:getStatus() #

809

        +---[

0.002476

ms] org.apache.coyote.http11.Http11Processor:statusDropsConnection() #

809

        +---[

0.002284

ms] org.apache.coyote.RequestInfo:setStage() #

838

        +---[

0.00222

ms] org.apache.coyote.http11.Http11Processor:isAsync() #

839

        +---[

0.037873

ms] org.apache.coyote.http11.Http11Processor:endRequest() #

843

        +---[

0.002188

ms] org.apache.coyote.RequestInfo:setStage() #

845

        +---[

0.002112

ms] org.apache.coyote.http11.Http11Processor:getErrorState() #

849

        +---[

0.002063

ms] org.apache.coyote.ErrorState:isError() #

849

        +---[

0.002504

ms] org.apache.coyote.http11.Http11Processor:isAsync() #

853

        +---[

0.009808

ms] org.apache.coyote.Request:updateCounters() #

854

        +---[

0.002008

ms] org.apache.coyote.http11.Http11Processor:getErrorState() #

855

        +---[

0.002192

ms] org.apache.coyote.ErrorState:isIoAllowed() #

855

        +---[

0.01968

ms] org.apache.coyote.http11.Http11InputBuffer:nextRequest() #

856

        +---[

0.010065

ms] org.apache.coyote.http11.Http11OutputBuffer:nextRequest() #

857

        +---[

0.002576

ms] org.apache.coyote.RequestInfo:setStage() #

870

        +---[

0.016599

ms] org.apache.coyote.http11.Http11Processor:processSendfile() #

872

        +---[

0.008182

ms] org.apache.coyote.http11.Http11InputBuffer:getParsingRequestLinePhase() #

688

        +---[

0.0075

ms] org.apache.coyote.http11.Http11Processor:handleIncompleteRequestLineRead() #

690

        +---[

0.001979

ms] org.apache.coyote.RequestInfo:setStage() #

875

        +---[

0.001981

ms] org.apache.coyote.http11.Http11Processor:getErrorState() #

877

        +---[

0.001934

ms] org.apache.coyote.ErrorState:isError() #

877

        +---[

0.001995

ms] org.apache.tomcat.util.net.AbstractEndpoint:isPaused() #

877

        +---[

0.002403

ms] org.apache.coyote.http11.Http11Processor:isAsync() #

879

        `---[

0.006176

ms] 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) 

costin

 608 

ms

.

`---

ts

=

2019

-

09

-

1421

:

34

:

33

;thread_name=http-nio-

7744

-exec-

1

;id=

10

;is_daemon=

true

;priority=

5

;TCCL=org.springframework.boot.loader.LaunchedURLClassLoader@

20

ad9418

    `---[

81.70999

ms] org.apache.catalina.connector.CoyoteAdapter:service()

        +---[

0.032546

ms] org.apache.coyote.Request:getNote() #

302

        +---[

0.007148

ms] org.apache.coyote.Response:getNote() #

303

        +---[

0.007475

ms] org.apache.catalina.connector.Connector:getXpoweredBy() #

324

        +---[

0.00447

ms] org.apache.coyote.Request:getRequestProcessor() #

331

        +---[

0.007902

ms] java.lang.ThreadLocal:get() #

331

        +---[

0.006522

ms] org.apache.coyote.RequestInfo:setWorkerThreadName() #

331

        +---[

73.793798

ms] org.apache.catalina.connector.CoyoteAdapter:postParseRequest() #

336

        +---[

0.001536

ms] org.apache.catalina.connector.Connector:getService() #

339

        +---[

0.004469

ms] org.apache.catalina.Service:getContainer() #

339

        +---[

0.007074

ms] org.apache.catalina.Engine:getPipeline() #

339

        +---[

0.004334

ms] org.apache.catalina.Pipeline:isAsyncSupported() #

339

        +---[

0.002466

ms] org.apache.catalina.connector.Request:setAsyncSupported() #

339

        +---[

6.01E-4

ms] org.apache.catalina.connector.Connector:getService() #

342

        +---[

0.001859

ms] org.apache.catalina.Service:getContainer() #

342

        +---[

9.65E-4

ms] org.apache.catalina.Engine:getPipeline() #

342

        +---[

0.005231

ms] org.apache.catalina.Pipeline:getFirst() #

342

        +---[

7.239154

ms] org.apache.catalina.Valve:invoke() #

342

        +---[

0.006904

ms] org.apache.catalina.connector.Request:isAsync() #

345

        +---[

0.00509

ms] org.apache.catalina.connector.Request:finishRequest() #

372

        +---[

0.051461

ms] org.apache.catalina.connector.Response:finishResponse() #

373

        +---[

0.007244

ms] java.util.concurrent.atomic.AtomicBoolean:<init>() #

379

        +---[

0.007314

ms] org.apache.coyote.Response:action() #

380

        +---[

0.004518

ms] org.apache.catalina.connector.Request:isAsyncCompleting() #

382

        +---[

0.001072

ms] org.apache.catalina.connector.Request:getContext() #

394

        +---[

0.007166

ms] java.lang.System:currentTimeMillis() #

401

        +---[

0.004367

ms] org.apache.coyote.Request:getStartTime() #

401

        +---[

0.011483

ms] org.apache.catalina.Context:logAccess() #

401

        +---[

0.0014

ms] org.apache.coyote.Request:getRequestProcessor() #

406

        +---[min=

8.0E-4

ms,max=

9.22E-4

ms,total=

0.001722

ms,count=

2

] java.lang.Integer:<init>() #

406

        +---[

0.001082

ms] java.lang.reflect.Method:invoke() #

406

        +---[

0.001851

ms] org.apache.coyote.RequestInfo:setWorkerThreadName() #

406

        +---[

0.035805

ms] org.apache.catalina.connector.Request:recycle() #

410

        `---[

0.007849

ms] 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) 

costin

 150 

ms

.

`---

ts

=

2019

-

09

-

1421

:

36

:

26

;thread_name=http-nio-

7744

-exec-

3

;id=

12

;is_daemon=

true

;priority=

5

;TCCL=org.springframework.boot.loader.LaunchedURLClassLoader@

20

ad9418

    `---[

75.743681

ms] org.apache.catalina.webresources.JarWarResourceSet:getArchiveEntries()

        +---[

0.025731

ms] java.util.HashMap:<init>() #

106

        +---[

0.097729

ms] org.apache.catalina.webresources.JarWarResourceSet:openJarFile() #

109

        +---[

0.091037

ms] java.util.jar.JarFile:getJarEntry() #

110

        +---[

0.096325

ms] java.util.jar.JarFile:getInputStream() #

111

        +---[

0.451916

ms] org.apache.catalina.webresources.TomcatJarInputStream:<init>() #

113

        +---[min=

0.001175

ms,max=

0.001176

ms,total=

0.002351

ms,count=

2

] java.lang.Integer:<init>() #

114

        +---[

0.00104

ms] java.lang.reflect.Method:invoke() #

114

        +---[

0.045105

ms] org.apache.catalina.webresources.TomcatJarInputStream:getNextJarEntry() #

114

        +---[min=

5.02E-4

ms,max=

0.008531

ms,total=

0.028864

ms,count=

31

] java.util.jar.JarEntry:getName() #

116

        +---[min=

5.39E-4

ms,max=

0.022805

ms,total=

0.054647

ms,count=

31

] java.util.HashMap:put() #

116

        +---[min=

0.004452

ms,max=

34.479307

ms,total=

74.206249

ms,count=

31

] org.apache.catalina.webresources.TomcatJarInputStream:getNextJarEntry() #

117

        +---[

0.018358

ms] org.apache.catalina.webresources.TomcatJarInputStream:getManifest() #

119

        +---[

0.006429

ms] org.apache.catalina.webresources.JarWarResourceSet:setManifest() #

120

        +---[

0.010904

ms] org.apache.tomcat.util.compat.JreCompat:isJre9Available() #

121

        +---[

0.003307

ms] org.apache.catalina.webresources.TomcatJarInputStream:getMetaInfEntry() #

133

        +---[

5.5E-4

ms] java.util.jar.JarEntry:getName() #

135

        +---[

6.42E-4

ms] java.util.HashMap:put() #

135

        +---[

0.001981

ms] org.apache.catalina.webresources.TomcatJarInputStream:getManifestEntry() #

137

        +---[

0.064484

ms] org.apache.catalina.webresources.TomcatJarInputStream:close() #

141

        +---[

0.007961

ms] org.apache.catalina.webresources.JarWarResourceSet:closeJarFile() #

151

        `---[

0.004643

ms] java.io.InputStream:close() #

155

發現了一個值得暫停思考的點:

+---[min=

0.004452

ms,max=

34.479307

ms,total=

74.206249

ms,count=

31

] org.apache.catalina.webresources.TomcatJarInputStream:getNextJarEntry() #

117

這行程式碼載入了31次,一共耗時74ms;從名字上看,應該是tomcat載入jar包時的耗時,那麼是載入了31個jar包的耗時,還是載入了jar包內的某些資源31次耗時呢?
TomcatJarInputStream這個類原始碼的註釋寫到:
The purpose of this sub-classistoobtainreferencestotheJarEntryobjectsforMETA

-

INF

andMETA

-

INF

/

MANIFEST

.

MFthatareotherwiseswallowedbytheJarInputStreamimplementation

.

大概意思也就是,獲取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) 

costin

 44 

ms

.

`---

ts

=

2019

-

09

-

1421

:

37

:

47

;thread_name=http-nio-

7744

-exec-

5

;id=

14

;is_daemon=

true

;priority=

5

;TCCL=org.springframework.boot.loader.LaunchedURLClassLoader@

20

ad9418

    `---[

0.234952

ms] org.apache.catalina.webresources.TomcatJarInputStream:createZipEntry()

        +---[

0.039455

ms] java.util.jar.JarInputStream:createZipEntry() #

43

        `---[

0.007827

ms] java.lang.String:equals() #

44

`---ts=

2019

-

09

-

1421

:

37

:

47

;thread_name=http-nio-

7744

-exec-

5

;id=

14

;is_daemon=

true

;priority=

5

;TCCL=org.springframework.boot.loader.LaunchedURLClassLoader@

20

ad9418

    `---[

0.050222

ms] org.apache.catalina.webresources.TomcatJarInputStream:createZipEntry()

        +---[

0.001889

ms] java.util.jar.JarInputStream:createZipEntry() #

43

        `---[

0.001643

ms] 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) 

costin

 27 

ms

.

ts

=

2019

-

09

-

1421

:

51

:

14

; [cost=

0.14547

ms] result=

@ArrayList

[

@String

[META-INF/],

]

ts=

2019

-

09

-

1421

:

51

:

14

; [cost=

0.048028

ms] result=

@ArrayList

[

@String

[META-INF/MANIFEST.MF],

]

ts=

2019

-

09

-

1421

:

51

:

14

; [cost=

0.046071

ms] result=

@ArrayList

[

@String

[META-INF/resources/],

]

ts=

2019

-

09

-

1421

:

51

:

14

; [cost=

0.033855

ms] result=

@ArrayList

[

@String

[META-INF/resources/swagger-ui.html],

]

ts=

2019

-

09

-

1421

:

51

:

14

; [cost=

0.039138

ms] result=

@ArrayList

[

@String

[META-INF/resources/webjars/],

]

ts=

2019

-

09

-

1421

:

51

:

14

; [cost=

0.033701

ms] result=

@ArrayList

[

@String

[META-INF/resources/webjars/springfox-swagger-ui/],

]

ts=

2019

-

09

-

1421

:

51

:

14

; [cost=

0.033644

ms] result=

@ArrayList

[

@String

[META-INF/resources/webjars/springfox-swagger-ui/favicon-

16

x16.png],

]

ts=

2019

-

09

-

1421

:

51

:

14

; [cost=

0.033976

ms] result=

@ArrayList

[

@String

[META-INF/resources/webjars/springfox-swagger-ui/springfox.css],

]

ts=

2019

-

09

-

1421

:

51

:

14

; [cost=

0.032818

ms] result=

@ArrayList

[

@String

[META-INF/resources/webjars/springfox-swagger-ui/swagger-ui-standalone-preset.js.map],

]

ts=

2019

-

09

-

1421

:

51

:

14

; [cost=

0.04651

ms] result=

@ArrayList

[

@String

[META-INF/resources/webjars/springfox-swagger-ui/swagger-ui.css],

]

ts=

2019

-

09

-

1421

:

51

:

14

; [cost=

0.034793

ms] 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升級至該版本及以上版本就可以解決此問題

歡迎加入我的知識星球,全面提升技術能力。
👉 加入方式,長按”或“掃描”下方二維碼噢
星球的內容包括:專案實戰、面試招聘、原始碼解析、學習路線。
文章有幫助的話,在看,轉發吧。
謝謝支援喲 (*^__^*)

相關文章