應用啟動加速-並發初始化spring bean

背景

隨着需求的不斷迭代,服務承載的內容越來越多,依賴越來越多,導致服務啟動慢,從最開始的2min以內增長到5min,導致服務發佈很慢,嚴重影響開發效率,以及線上問題的修復速度。所以需要進行啟動加速。

方案

應用啟動加速的優化方案通常有

  1. 編譯階段的優化,比如無用依賴的優化
  2. dockerfile的優化
  3. 依賴的中間件優化,中間件有大量的網絡連接建立,有很大的優化手段
  4. 富客戶端的優化
  5. spring bean加載的優化
    spring容器加載bean是通過單線程加載的,可以通過並發來提高加載速度。

鑒於1的優化難度比較大,2、3、4則一般與各個公司里的基礎組件有很大相關性,所以本篇只介紹spring bean加載的優化。

spring bean 加載耗時分析

分析bean加載耗時

首先需要分析加載耗時高的bean。spring bean 耗時 = timestampOfAfterInit – timestampOfBeforeInit.可以通過擴展BeanPostProcessor來實現,代碼如下

@Component
public class SpringbeanAnalyse implements BeanPostProcessor,
        ApplicationListener<ContextRefreshedEvent> {
    private static Logger log = LoggerFactory.getLogger(SpringbeanAnalyse.class);
    private Map<String, Long>  mapBeantime  = new HashMap<>();
    private static volatile AtomicBoolean started = new AtomicBoolean(false);


    @Autowired
    public Object postProcessBeforeInitialization(Object bean, String beanName) throws
            BeansException {
        mapBeantime.put(beanName, System.currentTimeMillis());
        return bean;
    }

    @Autowired
    public Object postProcessAfterInitialization(Object bean, String beanName) throws
            BeansException {
        Long begin = mapBeantime.get(beanName);
        if (begin != null) {
            mapBeantime.put(beanName, System.currentTimeMillis() - begin);
        }
        return bean;
    }
    @Override
    public void onApplicationEvent(final ContextRefreshedEvent event) {
        if (started.compareAndSet(false, true)) {
            for (Map.Entry<String,Long> entry: mapBeantime.entrySet()) {
                if (entry.getValue() > 1000) {
                   log.warn("slowSpringbean => :",entry.getKey());
                }
            }
        }
    }
}

這樣我們就能得到應用中耗時比較高的spring bean。可以看下這些bean的特點,大部分都是在
afterPropertiesSet,postconstruct,init方法中有初始化邏輯

eg. AgentConfig中有個構建bean,並調用init方法初始化。

@Bean(initMethod="init')
BeanA initBeanA(){
xxx
}

bean的生命周期

sampleCode

@Component
@Configuration
public class BeanC implements EnvironmentAware, InitializingBean{
    public BeanC() {
        System.out.println("constructC");
    }

    @Override
    public void afterPropertiesSet() throws Exception {
        System.out.println("afterC"  + Thread.currentThread().getName() + Thread.currentThread().getId());
    }

    @Resource
    public void resource(Environment environment) {
        System.out.println("resourceC");
    }

    @PostConstruct
    public void postConstruct() {
        System.out.println("postConstructC" +Thread.currentThread().getName() + Thread.currentThread().getId());
    }

    @Override
    public void setEnvironment(Environment environment) {
        System.out.println("EnvironmentC");
    }


    public void init(){
        System.out.println("InitC");
    }

}

輸出結果

constructC
resourceC
EnvironmentC
postConstructC
afterC

看下代碼
單個類的加載順序org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory

image.png

單個類的方法順序是確定了,但是不同類的加載順序是不確定的。默認是按照module,package的ascii順序來加載。但這個類的初始化順序不是固定的,在不同機器上表現形式不一樣。類似於
Jvm加載jar包的順序

控制不同類的加載順序

可以通過以下方法來控制bean加載順序

  1. 依賴 @DependOn
  2. bean依賴 構造器,或者@Autowired
  3. @Order 指定順序

對BeanB添加了BeanC的依賴,輸出結果為

constructC
resourceC
constructB
resourceB
EnvironmentB
postConstructB
afterB
EnvironmentC
postConstructC
afterC

這時候bean的加載順序為

  1. 調用對象的構造函數
  2. 為對象注入依賴,執行依賴對象的初始化過程
  3. 執行PostConstruct,afterPropertiesSet等生命周期方法。

這意味着我們可以按照bean的加載的各個階段進行優化。

並發加載spring bean

全局依賴拓撲

因為spring容器管理bean是單線程加載的,所以耗時慢,我們的解決思路是通過並發來優化,通過並發的前提是相互沒有依賴。這個顯然是不現實的,一個應用中的spring bean有大量依賴,甚至是有很多循環依賴。

對於循環依賴,可以通過分解拓撲關係來解決。但是按照我們上面分析,spring又提供了大量的擴展能力,讓開發者去定義bean的依賴,這樣導致我們無法得到一個spring bean的全局依賴圖。因此無法通過自動配置的手段來解決spring bean單線程加載的問題。

局部異步加載

既然無法通過全自動配置手段來完成所有bean的全自動並發加載,那我們退而求其次,通過手動配置耗時分析中得到的,耗時比較高的bean。這樣特殊處理也能達到我們優化啟動時間目的。

同時因為單個bean加載有多個階段,有些階段耗時並不高,都是通用的操作,可以繼續委託spring 容器去管理,這樣就不必去處理複雜的循環依賴的問題。

按照這個思路,解決方案就比較簡單

  1. 定義待並發加載的bean
  2. 重寫bean的initmethod,如果是在第一步的配置里,就提交到線程池中,如果不在,就調用父類的加載方法

總結

最後通過並發加載原本耗時超過1s的bean,將我們的其中一個微服務啟動耗時時間降低了100s,取得了階段性的成果。

當然這個方案並不是很完善,

  1. 需要依賴人工配置,做不到自動化
  2. 安全得不到保障,需要確保不同bean之間afterPropertiesSet等擴展方法中無依賴。當然這一點不止是並發加載時需要保障,即使是單線程加載時也需要保障,原因是bean的加載順序得不到保障,可能會引發潛在的bug。

歡迎提出新的優化方案討論。

我正在參與掘金技術社區創作者簽約計劃招募活動

Tags: