Spring Boot 項目啟動慢排查

背景


打開一個幾年前的老項目,發(fā)現(xiàn)啟動巨慢,同樣的代碼沒動過,當(dāng)年在渣渣 i5 上運行也只要十幾秒,現(xiàn)在升了配置反而要好幾分鐘。

原始慢啟動日志

截圖中的項目已經(jīng)精簡了部分內(nèi)容

問題定位


INFO  RocketmqRemoting:95 : closeChannel: close the connection to remote address[] result: true

首先注意到這句報錯,意思是 RocketMQ 沒連上 ,按理說應(yīng)該沒影響,不過以防萬一還是本地啟動一下 RocketMQ

start mqnamesrv && mqbroker -c ../conf/broker.conf

重啟項目發(fā)現(xiàn)耗時還是差不多( 相對于總時長來說幾秒的誤差可以忽略不計 )。

再注意到下面的日志, RocketMQListener 注冊成功之前等待了大約 10 秒的時間。

2022-12-19 18:53:21.484 - 42601   [NettyClientSelector_1] INFO  RocketmqRemoting:95 : closeChannel: close the connection to remote address[] result: true
2022-12-19 18:53:32.552 - 53669   [NettyClientSelector_1] INFO  RocketmqRemoting:95 : closeChannel: close the connection to remote address[] result: true
2022-12-19 18:53:32.554 - 53671   [main] INFO  org.apache.rocketmq.spring.support.DefaultRocketMQListenerContainer:243 : running container: DefaultRocketMQListenerContainer{consumerGroup='DEFAULT_GROUP', nameServer='127.0.0.1:9876', topic='DEFAULT_TOPIC', consumeMode=CONCURRENTLY, selectorType=TAG, selectorExpression='*', messageModel=CLUSTERING}
2022-12-19 18:53:32.554 - 53671   [main] INFO  org.apache.rocketmq.spring.autoconfigure.ListenerContainerConfiguration:107 : Register the listener to container, listenerBeanName:myRocketMQListener, containerBeanName:org.apache.rocketmq.spring.support.DefaultRocketMQListenerContainer_1

為了測試方便,之前只保留了一個 RocketMQListener ,其他的都注釋掉了,現(xiàn)在放開注釋后發(fā)現(xiàn),每個 RocketMQListener 注冊成功之前都會等待大約 10 秒。說明這部分的代碼是有點問題的,但還不是啟動慢的主要原因,繼續(xù)看日志( 把 RocketMQListener 都先注釋掉 ):

2022-12-19 18:52:45.440 - 6557    [main] INFO  com.Application:658 : The following profiles are active: dev
WARNING: An illegal reflective access operation has occurred
WARNING: Illegal reflective access by org.springframework.cglib.core.ReflectUtils$1 (file:/H:/maven/org/springframework/spring-core/5.0.12.RELEASE/spring-core-5.0.12.RELEASE.jar) to method java.lang.ClassLoader.defineClass(java.lang.String,byte[],int,int,java.security.ProtectionDomain)
WARNING: Please consider reporting this to the maintainers of org.springframework.cglib.core.ReflectUtils$1
WARNING: Use --illegal-access=warn to enable warnings of further illegal reflective access operations
WARNING: All illegal access operations will be denied in a future release
2022-12-19 18:53:18.903 - 40020   [main] INFO  org.apache.coyote.http11.Http11NioProtocol:180 : Initializing ProtocolHandler ["http-nio-0.0.0.0-3000"]

這部分警告日志的前后間隔了大約 30 秒,說明 Spring 初始化有點慢,但看不到具體的細節(jié),需要在 log4j2 配置中放開一下 Springdebug 日志:

<AsyncLogger name="org.springframework" level="debug"/>

可以看到每個 Bean 實例化都挺快的,唯獨下面兩個部分卡了一會兒:

2022-12-19 19:15:29.185 - 7538    [main] INFO  org.springframework.context.support.PostProcessorRegistrationDelegate$BeanPostProcessorChecker:326 : Bean 'rocketmq-org.apache.rocketmq.spring.autoconfigure.RocketMQProperties' of type [org.apache.rocketmq.spring.autoconfigure.RocketMQProperties] is not eligible for getting processed by all BeanPostProcessors (for example: not eligible for auto-proxying)
2022-12-19 19:15:39.070 - 17423   [main] INFO  org.springframework.context.support.PostProcessorRegistrationDelegate$BeanPostProcessorChecker:326 : Bean 'defaultMQProducer' of type [org.apache.rocketmq.client.producer.DefaultMQProducer] is not eligible for getting processed by all BeanPostProcessors (for example: not eligible for auto-proxying)
2022-12-19 19:15:39.120 - 17473   [main] INFO  org.springframework.context.support.PostProcessorRegistrationDelegate$BeanPostProcessorChecker:326 : Bean 'jacksonObjectMapper' of type [com.fasterxml.jackson.databind.ObjectMapper] is not eligible for getting processed by all BeanPostProcessors (for example: not eligible for auto-proxying)
2022-12-19 19:16:03.671 - 42024   [main] INFO  org.springframework.context.support.PostProcessorRegistrationDelegate$BeanPostProcessorChecker:326 : Bean 'rocketMQTemplate' of type [org.apache.rocketmq.spring.core.RocketMQTemplate] is not eligible for getting processed by all BeanPostProcessors (for example: not eligible for auto-proxying)

這里耗時大約 30 秒,和前面可以對上。

由于項目目前用不上 RocketMQ ,可以把 RocketMQAutoConfiguration 配置先屏蔽掉看下:

@SpringBootApplication(exclude = { RocketMQAutoConfiguration.class })
public class Application {

  // ...

  @Bean
  public RocketMQTemplate rocketMQTemplate() {
    // 造一個假的 rocketMQTemplate 避免項目報錯
    RocketMQTemplate rocketMQTemplate = new RocketMQTemplate();
    rocketMQTemplate.setProducer(new DefaultMQProducer("groupName"));
    return rocketMQTemplate;
  }
}

重啟項目只需要 20 多秒了,雖然還是慢但至少能接受,可以確定是 RocketMQ 的問題了。而且所有依賴的中間件都是本地部署的,基本可以忽略網(wǎng)絡(luò)問題了( 后面打臉 ),實在想不通為什么會慢。

解決方法


拿著 rocketMQTemplate 啟動慢 的關(guān)鍵詞去搜問題簡直是大海撈針,不過運氣好找到了一篇文章《 RocketMQ 很慢?引出了一個未解之謎 》, 里面指出了 NetworkInterface.getNetworkInterfaces 這個 JDK 方法的耗時問題 ,本地測試了一下果然如此:

public class NetworkInterfaceTest {

  public static void main(String[] args) throws SocketException {
    long start = System.currentTimeMillis();
    Enumeration<NetworkInterface> enumeration = NetworkInterface.getNetworkInterfaces();
    long end = System.currentTimeMillis();

    ArrayList<NetworkInterface> result = new ArrayList<>();
    while (enumeration.hasMoreElements()) {
      result.add(enumeration.nextElement());
    }
    System.out.printf(
      "NetworkInterface.getNetworkInterfaces() use %d ms, result size: %d%n",
      end - start,
      result.size()
    );
  }
}

getNetworkInterfaces 測試

真兇果然是它,一次調(diào)用耗時將近 6 秒,不加限制多調(diào)用幾次就能爆炸了??梢钥匆幌逻@個方法的返回結(jié)果是什么:

getNetworkInterfaces 返回結(jié)果

光看名字就能聯(lián)想到 控制面板\網(wǎng)絡(luò)和 Internet\網(wǎng)絡(luò)連接 中的 網(wǎng)絡(luò)適配器 ,基本能對上:

網(wǎng)絡(luò)適配器

之所以有這么多的設(shè)備是因為之前安裝 Genymotion 模擬器的時候 VirtualBox 給安裝上的,因為沒遇到過其他問題也就沒在意,沒想到卻導(dǎo)致了 getNetworkInterfaces 調(diào)用變慢的問題。而 getNetworkInterfaces 方法底層調(diào)用的是 getAll 這個 native 方法,所以代碼層面基本沒得改了,目前 唯一解決方法 就是 把沒用的網(wǎng)絡(luò)適配器都禁用掉 ,最后耗時只要 600 毫秒了( 不同的網(wǎng)卡耗時程度不同 ),這種程度基本能接受了。

getNetworkInterfaces 測試 - 禁用后

把測試代碼放 VMware 構(gòu)建的純凈系統(tǒng)環(huán)境下又測了一遍,發(fā)現(xiàn)最初調(diào)用只要 31 毫秒,這種程度的耗時才是正常的,難怪以前都沒發(fā)現(xiàn)這個問題,而安裝 Genymotion 后耗時果然就顯著增加了。

回到項目還原代碼驗證一下,啟動時間果然恢復(fù)到正常的 8 秒了。

問題解決后正常啟動日志

問題復(fù)盤


這個問題得到解決得虧有人用 RocketMQ 的時候遇到類似的問題,不然就只能不了了之了。不過和別人說 getNetworkInterfaces 方法調(diào)用很慢大概率會被人當(dāng)傻子,比如有人在 RocketMQ 倉庫提過 類似問題 直接被無視了。

xxxProducerxxxConsumer 都繼承 ClientConfig ,因此每實例化一個就會調(diào)用一次 getNetworkInterfaces 。

繼續(xù)查資料看有沒有方法可以定位耗時的代碼塊,在《 SpringBoot 服務(wù)啟動慢排查思路 》一文中看到了一張 JProfiler 的方法調(diào)用樹截圖,里面清楚標紅了耗時比較長的方法,但是文章中沒有具體的操作步驟,只能自己研究下。

后來才發(fā)現(xiàn)在 JProfiler 幫助文檔 中關(guān)于這部分內(nèi)容已經(jīng)介紹的很詳細了。

操作步驟:

  • 下載安裝 JProfiler

  • IDEA 中用 JProfiler 圖標啟動項目

  • JProfilerbin 目錄啟動主程序 jprofiler.exe

  • Attach > 選擇 背景色標綠 的項目進程 > 開始

    配置項一路確定就好了。

    許可證密鑰 錯誤會導(dǎo)致 啟動報錯 的,需要選擇 評估 ( 試用 10 天 )。

    修改系統(tǒng)時間后重啟 JProfiler 可以繼續(xù)試用。( 用完了再把系統(tǒng)時間還原回去 )

    JProfiler> ERROR: Invalid license key. Aborting.
    JProfiler> Killing process
    

    清除已填寫的注冊碼:

    cd /d "%USERPROFILE%/.jprofiler13" && del jprofiler_config.xml
    
  • CPU 視圖 > 調(diào)用樹 > 開始記錄

    這一步手速要快一點,不然可能會錯過一些重要的調(diào)用過程。當(dāng)然也可以在 會話啟動 的配置窗口中設(shè)置 初始化記錄分析 以自動開始記錄。

    JProfiler 會話啟動
  • 等待項目啟動完成

    JProfiler 分析調(diào)用樹

調(diào)用樹結(jié)果是有了,但坑爹的是展開之后報紅的線索就斷了,基本是 1% 以內(nèi)的正常方法調(diào)用,完全看不到里層的慢方法。點擊工具欄中的 分析 按鈕,將結(jié)果轉(zhuǎn)為火焰圖看下:

JProfiler 查看火焰圖

可以看到兩側(cè)缺失了一大片的內(nèi)容,說明有一些方法被隱藏了沒記錄在內(nèi),這時回到調(diào)用樹,注意到每個方法前面的圖標都不太一樣,正好工具欄有一個 顯示圖例 的按鈕,點擊查看內(nèi)容:

JProfiler 樹圖例

這說明部分方法被過濾了,最后在 會話設(shè)置 > 調(diào)用樹過濾器 > 定義過濾器 中發(fā)現(xiàn)默認配置把一堆第三方庫都過濾了:

JProfiler 定義過濾器 - 默認

這里只需要添加被分析的包( 注意順序,具體配置規(guī)則見 官方文檔 )即可:

java. // 看情況是否要加
javax.
org.apache.
JProfiler 定義過濾器 - 自定義

重啟項目重新開始分析就能得到想要的結(jié)果了:

JProfiler 分析調(diào)用樹 - 最終結(jié)果

查看火焰圖效果更直觀,可以看到大部分時間都花費在 getNetworkInterfaces 上了( 8 次調(diào)用共耗時 37 秒 ),也證實了前面問題定位沒有錯。

JProfiler 查看火焰圖 - 最終結(jié)果

其他相關(guān)問題


解決完這個問題忽然想到前段時間碰到的另一個問題:用到 JSP 的項目從 JDK1.8 切換到 JDK11 后,第一次訪問頁面很慢,并有警告日志:

WARN 6244 --- [nio-8080-exec-1] o.a.c.util.SessionIdGeneratorBase        : Creation of SecureRandom instance for session ID generation using [SHA1PRNG] took [6,337] milliseconds.

SecureRandom 的耗時問題 以前在 Docker 容器 中運行 Tomcat 時遇到過,但和這次顯然不是一回事,定位到打日志的代碼:

createSecureRandom

發(fā)現(xiàn)耗時的地方在 SecureRandom.nextInt() ,只是肉眼去看代碼不好去分析更底層的調(diào)用棧,還好剛學(xué)會了 JProfiler ,分析結(jié)果一目了然,這個問題同樣也是 getNetworkInterfaces 在搗鬼。

JProfiler 分析調(diào)用樹 - SecureRandom

至于為什么 JDK1.8 沒問題,由于調(diào)用棧發(fā)生了變化一時也查不出原因,我猜測可能是其他地方( 可能無法被分析器攔截到 )提前觸發(fā)了 SecureRandom.SeederHolderstatic 代碼初始化。比如 debug 模式 運行項目時打斷點發(fā)現(xiàn) sun.management.Agent.startAgent() 中就間接調(diào)用了 SecureRandom 。


轉(zhuǎn)載請注明出處: https://github.com/anyesu/blog/issues/44

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容