背景
隨著需求的不斷迭代,服務承載的內容越來越多,依賴越來越多,導致服務啟動慢,從最開始的2min以內增長到5min,導致服務發布很慢,嚴重影響開發效率,以及線上問題的修復速度,所以需要進行啟動加速,
方案
應用啟動加速的優化方案通常有
- 編譯階段的優化,比如無用依賴的優化
- dockerfile的優化
- 依賴的中間件優化,中間件有大量的網路連接建立,有很大的優化手段
- 富客戶端的優化
- 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
單個類的方法順序是確定了,但是不同類的加載順序是不確定的,默認是按照module,package的ascii順序來加載,但這個類的初始化順序不是固定的,在不同機器上表現形式不一樣,類似于
Jvm加載jar包的順序
控制不同類的加載順序
可以通過以下方法來控制bean加載順序
- 依賴 @DependOn
- bean依賴 構造器,或者@Autowired
- @Order 指定順序
對BeanB添加了BeanC的依賴,輸出結果為
constructC
resourceC
constructB
resourceB
EnvironmentB
postConstructB
afterB
EnvironmentC
postConstructC
afterC
這時候bean的加載順序為
- 呼叫物件的建構式
- 為物件注入依賴,執行依賴物件的初始化程序
- 執行PostConstruct,afterPropertiesSet等生命周期方法,
這意味著我們可以按照bean的加載的各個階段進行優化,
并發加載spring bean
全域依賴拓撲
因為spring容器管理bean是單執行緒加載的,所以耗時慢,我們的解決思路是通過并發來優化,通過并發的前提是相互沒有依賴,這個顯然是不現實的,一個應用中的spring bean有大量依賴,甚至是有很多回圈依賴,
對于回圈依賴,可以通過分解拓撲關系來解決,但是按照我們上面分析,spring又提供了大量的擴展能力,讓開發者去定義bean的依賴,這樣導致我們無法得到一個spring bean的全域依賴圖,因此無法通過自動配置的手段來解決spring bean單執行緒加載的問題,
區域異步加載
既然無法通過全自動配置手段來完成所有bean的全自動并發加載,那我們退而求其次,通過手動配置耗時分析中得到的,耗時比較高的bean,這樣特殊處理也能達到我們優化啟動時間目的,
同時因為單個bean加載有多個階段,有些階段耗時并不高,都是通用的操作,可以繼續委托spring 容器去管理,這樣就不必去處理復雜的回圈依賴的問題,
按照這個思路,解決方案就比較簡單
- 定義待并發加載的bean
- 重寫bean的initmethod,如果是在第一步的配置里,就提交到執行緒池中,如果不在,就呼叫父類的加載方法
總結
最后通過并發加載原本耗時超過1s的bean,將我們的其中一個微服務啟動耗時時間降低了100s,取得了階段性的成果,
當然這個方案并不是很完善,
- 需要依賴人工配置,做不到自動化
- 安全得不到保障,需要確保不同bean之間
afterPropertiesSet等擴展方法中無依賴,當然這一點不止是并發加載時需要保障,即使是單執行緒加載時也需要保障,原因是bean的加載順序得不到保障,可能會引發潛在的bug,
歡迎提出新的優化方案討論,
我正在參與掘金技術社區創作者簽約計劃招募活動
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/499942.html
標籤:其他
