你的SpringBoot能夠再快一點

公司最近推出了一款一體機產品,因而老闆就每天提個小箱子跑客戶作POC,倍兒有範兒。跑了一陣子客戶反響(問題)不錯(很多),其中最大的問題就是開機進系統太慢,按照老闆的說法:java

我按下開機鍵已經準備天花亂墜了,愣是給我一個系統維護界面5分鐘才能進去。只好跟他們解釋說咱們是工業一體機比較嚴謹,開機前要作好充分的自檢。spring

其實開機慢咱們是有預期的,咱們的應用是雲端微服務應用,爲了快速響應公司號召稍加改造就變成了邊緣應用。在資源配置各方面大幅縮水的狀況下,不慢都對不起咱們30w行的代碼量。 數組

言歸正傳,萬里長征第一步:重現。經過秒錶屢次測量的結果顯示:安全

開機進入維護界面須要30ssession

進入登陸頁須要230smaven

登陸進系統須要近4分鐘。ide

雖然沒有反饋的那麼誇張,這個速度也確實有點慢了。函數

問題定位

基於上面的測量結果,咱們能夠獲得以下分佈圖:spring-boot

image.png

系統啓動時間佔比較小,並且在操做系統級別的優化比較複雜收益不高,咱們將優化的重心放在應用啓動和系統登陸兩個部分。微服務

u  應用啓動

咱們經過查看應用日誌能夠發現應用的實際啓動時間爲109s,這個時間和咱們以前實測的時間也比較吻合,能夠做爲咱們應用優化的基線。

u  系統登陸

系統登陸時間的消耗看起來不太合理,這是由於咱們的應用使用了Eureka做爲微服務的註冊發現組件,致使了在應用啓動後要經歷屢次心跳驗證才能真正可用。這部分的優化策略是在一體機中去掉EurekaRestTemplate直接訪問本機Restful服務便可。 

通過以上初步分析,咱們明確了咱們優化的對象就是109s的應用啓動時間。

在開始以前咱們先說一下咱們面臨的應用規模,30w業務代碼行,800+ spring管理的對象,Jar包大小70M左右

他山之石

經過查閱資料能夠找到一些先行者,雖然案例大可能是很簡單的應用,好比說只有一個依賴的狀況優化到1s以內,可是原理上仍是相通的。

延遲初始化

咱們能夠經過@Lazy指定單個bean的延遲初始化,或者經過@ComponentScan指定lazyInit=true,也能夠實現一個LazyInitBeanFactoryPostProcessor類來靈活的指定。

在實際過程當中咱們發現不是全部的類都能設置爲lazyInit的,好比消息隊列的監聽類若是一開始不進行實例化那麼就永遠不會被實例化,這會致使消息永遠都不會被消費;還有定時任務類,一樣不適合設置成lazyInit

最終咱們採用LazyInitBeanFactoryPostProcessor的方式實現了兩個數組進行靈活定製:


private final String[] COMMON_INIT_LIST= {
                         "springContextUtil",
                         "custJobFactory"
};
private final String[] CUST_INIT_LIST= {
                         "userMsgReceiver",
                         "dgnsOperateReceiver",
                         "equipCondCalcReceiver",
                         "modelAnalysisReceiver",
                         "modelAnalysisScheduler"
};
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) {
    for (String beanName : beanFactory.getBeanDefinitionNames()) {        
            if(!needInitBean(beanName)) {
                BeanDefinition definition = beanFactory.getBeanDefinition(beanName);
                       definition.setLazyInit(true);
            }
      }
}
  
private boolean needInitBean(String beanName) {
            return ArrayUtils.contains(COMMON_INIT_LIST, beanName) 
            || ArrayUtils.contains(CUST_INIT_LIST, beanName);                         
}

經過延遲初始化,應用啓動時間從109s提高到48s,效果很是明顯。        

JVM啓動參數優化

這裏主要涉及的啓動參數設置是下面兩個

1, -XX:TieredStopAtLevel=1

使用C1編譯器,又稱爲客戶端模式,相對於C2也就是服務端模式,C1編譯生成的機器碼更加關注快速啓動可是因爲機器碼沒有通過編譯優化因此不適合在線上環境穩定運行。

2, -Xverify:none/ -noverify

經過去除字節碼的驗證來提高JVM啓動速度,一樣不適合線上對安全有要求的環境使用。

 

咱們平時開發的時候可能注意到在IDEEclipse中啓動一個SpringBoot應用的時候有一個選項叫Fast-startup,如圖:

image.png

咱們通常都是默認勾選的,卻不知這個選項對應的參數就是以上兩個JVM參數。

image.png

這兩個參數的設置能夠大大提高咱們本地啓動的速度,而本地啓動不存在穩定性和安全性的問題,因此適用這兩個參數。 

實際案例中咱們經過這兩個參數的設置,能夠將啓動時間提高到40s

生成組件索引

經過引入Maven依賴spring-context-indexer在編譯階段來爲組件生成索引加快類掃描速度。

具體作法分爲兩步

1, 添加Maven依賴

<dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-context-indexer</artifactId>
      <optional>true</optional>
  </dependency>


2, 配置Maven Plugin

<plugins>  
<plugin>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-maven-plugin</artifactId>
    <version>1.4.2.RELEASE</version><!--$NO-MVN-MAN-VER$-->
    <configuration>
                  <executable>true</executable>
                  <annotationProcessorPaths>
                         <path>
                     <groupId>org.springframework</groupId>
                     <artifactId>spring-context-indexer</artifactId>
                         </path>
                                    </annotationProcessorPaths>
    </configuration>
</plugin>
</plugins>

經過Maven install命令運行後在生成的jar包的META-INF目錄下面會生成spring.components文件,內容以下:

image.png

若是你的項目是多模塊項目,那麼在每一個模塊的jar下面都會生成一個索引文件。 

經過這步優化,啓動時間能夠提高到38s,效果不算明顯。

這和咱們項目自己的規模還有路徑掃描的速度有關,若是項目自己類很少或者路徑掃描自己很快,那建索引就沒有多大意義了,目前看來2s的提高聊勝於無吧。

上下求索

在通用優化建議的基礎上,咱們還根據本身的經驗和嘗試,進行了進一步的優化。

延遲初始化

此次的延遲初始化是從代碼層面來進行。經過第一步的延遲初始化處理,咱們篩選出一些須要提早初始化的類。而這些類的初始化因爲存在類依賴等因素又會牽扯出一大串的初始化,致使咱們在少許類的初始化上花費了較多的時間。

舉個例子,咱們有個消息消費類經過@Autowired強依賴了5service,那麼在這個Receiver類初始化的時候這5service也會被觸發初始化,service類中又經過@Autowired引入了其餘類的初始化,層層傳遞致使一個類的初始化實際觸發了幾十個類的初始化,已經破壞了咱們延遲初始化的設定,如圖:

image.pngimage.png

針對這種狀況能夠在@Autowired字段上加上@Lazy註解,可是容器在註冊屬性的時候會提示一個warningAnnotationUtils - Failed to meta-introspect annotation。雖然不影響後續初始化,可是看着仍是很糟心的。

因此我選擇的方式是乾脆把這幾個須要提早初始化的類裏面的@Autowired字段所有移除,使用的時候到ApplicationContext獲取。

@Autowired
private EquipCondService condService;
//替換爲使用時獲取,作到真正的延遲實例化
EquipCondService condService = SpringContextUtil.getBean(EquipCondService.class);

 經過代碼改造以後的延遲初始化升級,啓動時間提高到29s,效果還不錯。

Shiro初始化優化

Shiro的問題是經過查看Spring debug日誌中的跳變發現的。

image.png

在正常的日誌中通常兩個日誌的間隔也就幾十毫秒,而在shiro的初始化過程當中咱們發現了一段3s的間隔,那必定是發生了什麼不可告人的事情。經過二分查找的Debug終於發現了問題所在。

shiroConfig中須要定一個securityManager,咱們使用了Apache包裏自帶的DefaultWebSecurityManager。如下是DefaultWebSecurityManager類的構造函數:

public DefaultWebSecurityManager() {
            super();
            ((DefaultSubjectDAO) this.subjectDAO).setSessionStorageEvaluator(new DefaultWebSessionStorageEvaluator());
            this.sessionMode = HTTP_SESSION_MODE;
            setSubjectFactory(new DefaultWebSubjectFactory());
            setRememberMeManager(new CookieRememberMeManager());
            setSessionManager(new ServletContainerSessionManager());
}
public AbstractRememberMeManager() {
        this.serializer = new DefaultSerializer<PrincipalCollection>();
        AesCipherService cipherService = new AesCipherService();
        this.cipherService = cipherService;
        setCipherKey(cipherService.generateNewKey().getEncoded());
}


咱們發如今SecurityManager初始化的時候會初始化依賴的CookieRememberMeManager,最終調用到抽象類的構造函數。在這裏有一句話最終形成了3s的啓動延時:

cipherService.generateNewKey()

這是生成對稱加解密密鑰的方法,經過單元測試發現這句話單獨執行時間也是在3s左右,驗證了咱們的結論。

解決方法簡單粗暴,使用自定義的WebSecurityManager,去掉setRememberMeManager的調用便可:

image.png

通過這一步優化後,啓動時間優化到26s,恰好是3s的提高。 

溫故知新

在一體機開機速度提高的需求驅動下,咱們首先甄別出須要解決的關鍵問題就是應用啓動時間。咱們經過借鑑先行者的成功經驗,成功的將應用啓動時間從109s提高到38s。而後從日誌分析入手,找出日誌中的跳變點,解決了@Autowired引起的僞延遲問題和Shiro生成密鑰的時間損耗。最終咱們成功的將啓動時間控制到了30s以內(26s),而相應的一體機從開機到老闆開始天花亂墜也就只須要1分半鐘,喝口水就掩飾過去了。

下面羅列一下咱們的優化路徑,供後續參考借鑑。

image.png

image.png

最後有幾點須要重申一下:

1, 延遲初始化能夠加快應用的啓動速度,可是不少在初始化時暴露的問題如內存不足就不能提早發現,因此若是必定要用須要通過慎重嚴格的測試。

2, JVM的兩個優化參數都是適用於客戶端模式的,針對線上系統若是更加關注運行時的效率和穩定性,則不建議採用該項優化。

3, 關於Shiro的優化是在明確不須要RememberMe功能或者本身實現RememberMe的前提下使用,並且生成密鑰的方法在不一樣的內存型號下表現差別很大,咱們全部的優化數據都是在DDR3下進行,若是在DDR4下運行這個方法只要600ms,如此也就沒有必要特別優化了。

相關文章
相關標籤/搜索