大促系統(tǒng)優(yōu)化之應(yīng)用啟動(dòng)速度優(yōu)化實(shí)踐
一、前言
本文記錄了在大促前針對SpringBoot應(yīng)用啟動(dòng)速度過慢而采取的優(yōu)化方案,主要介紹了如何定位啟動(dòng)速度慢的阻塞點(diǎn),以及如何解決這些問題。希望可以幫助大家了解如何定位該類問題以及提供一些解決此類問題的思路。下文介紹的JSF為京東內(nèi)部RPC框架,類似于阿里的Dubbo(本文使用的SpringBoot版本為2.6.2)
二、問題背景
- 廣告投放平臺核心應(yīng)用生產(chǎn)環(huán)境單臺機(jī)器部署需要400-500s,加上鏡像下載時(shí)間大約1分鐘,整體耗時(shí)接近10分鐘,應(yīng)用部署時(shí)間過長會(huì)帶來諸多問題,例如上線遇到問題需要進(jìn)行快速回滾,啟動(dòng)時(shí)間過長會(huì)延長恢復(fù)時(shí)間,影響業(yè)務(wù)的連續(xù)性。
- 會(huì)導(dǎo)致整個(gè)應(yīng)用的發(fā)布時(shí)間變長,目前我們這個(gè)應(yīng)用有上百個(gè)容器,單次部署編排時(shí)間超過半個(gè)小時(shí),上線等待時(shí)間過長;另一個(gè)問題是在測試預(yù)發(fā)環(huán)境聯(lián)調(diào)修改一次編譯+部署就要十幾分鐘,嚴(yán)重影響聯(lián)調(diào)進(jìn)度,浪費(fèi)了很多時(shí)間。
- 綜上所述,可以看出對應(yīng)用的啟動(dòng)速度優(yōu)化刻不容緩,浪費(fèi)時(shí)間就是在浪費(fèi)生命,所以我們必須要做些什么,下面通過一張圖先來看下舊的生產(chǎn)集群發(fā)布的概況。
圖片
三、解決方案
要想排查應(yīng)用啟動(dòng)速度過慢的問題,需要先定位到是哪里慢,乍看這句話,就和沒說一樣,其實(shí)就是陳述一下事實(shí)??,下面介紹幾種常見的排查方法:
1.使用SpringBoot的監(jiān)控actuator
SpringBoot的actuator是一個(gè)監(jiān)控和管理SpringBoot應(yīng)用的框架,它提供了許多內(nèi)置的端點(diǎn),允許你訪問應(yīng)用程序的運(yùn)行時(shí)信息,如健康檢查、環(huán)境屬性、日志、指標(biāo)、線程轉(zhuǎn)儲等,所以我們可以使用其中的一個(gè)端點(diǎn)startup去查看整個(gè)應(yīng)用中所有bean的啟動(dòng)耗時(shí)。
但是這種模式下也有個(gè)弊端,就是每個(gè)bean的加載耗時(shí)其實(shí)是不準(zhǔn)確的,比如A開頭的bean,他的總計(jì)耗時(shí)會(huì)非常的長,原因是因?yàn)樵谶@個(gè)bean里面去加載其他bean的耗時(shí)也會(huì)累加到這個(gè)bean上,導(dǎo)致數(shù)據(jù)不準(zhǔn)確,所以該方案只能當(dāng)做參考。(具體該組件的使用方式可以參考該鏈接 點(diǎn)試試 )
2.根據(jù)應(yīng)用的啟動(dòng)日志分析
這個(gè)方法對于項(xiàng)目啟動(dòng)日志比較少的情況有一定的效果,可以在應(yīng)用啟動(dòng)時(shí)通過開啟debug日志來逐秒分析,查看啟動(dòng)日志打印的空白秒,就可以大概定位到阻塞點(diǎn),定位到耗時(shí)的地方后再具體問題具體分析,但是對于大型項(xiàng)目,啟動(dòng)一次有幾萬行的啟動(dòng)日志,人工直接分析費(fèi)時(shí)費(fèi)力,而且還不好定位問題,所以此方法不具有普適性。雖然不好用,但是我們也是可以用一下的,通過這個(gè)方面我們定位到了兩處阻塞點(diǎn)。
2.1 Tomcat啟動(dòng)掃描TLD文件優(yōu)化
因?yàn)槲覀兪褂玫氖峭庵肨omcat容器,通過查看啟動(dòng)日志發(fā)現(xiàn)Tomcat啟動(dòng)日志打印的時(shí)間和Tomcat引導(dǎo)Spring容器啟動(dòng)中間隔了幾十秒沒有額外的日志,只有一行 org.apache.jasper.servlet.TldScanner.scanJars: At least one JAR was scanned for TLDs yet none were found. Enable debug logging for this logger to find out which JAR was scanned for TLDs,因?yàn)闆]有別的日志,只能從這行日志開刀,看這行日志大體的意思是Tomcat在啟動(dòng)時(shí)嘗試掃描某些JAR文件以查找TLD文件,但沒有找到。
那么Tomcat在啟動(dòng)的時(shí)候?yàn)槭裁匆獟呙鑄LD文件(什么是TLD文件?點(diǎn)擊這里了解 ),Tomcat 作為 Servlet 規(guī)范的實(shí)現(xiàn)者,它在應(yīng)用啟動(dòng)的時(shí)候會(huì)掃描 Jar 包里面的 .tld 文件,加載里面定義的標(biāo)簽庫,但是,我們項(xiàng)目沒有使用 JSP 作為 Web 頁面的模板,為了加快應(yīng)用的啟動(dòng)速度,我們可以把 Tomcat 里面的這個(gè)功能給關(guān)掉那么如何關(guān)閉這個(gè)掃描以提高啟動(dòng)速度?,通過查閱文檔發(fā)現(xiàn)可以修改Tomcat的catalina.properties文件,
修改一個(gè)配置項(xiàng)tomcat.util.scan.StandardJarScanFilter.jarsToSkip設(shè)置值為 *.jar就可以關(guān)閉掃描。
2.2 應(yīng)用啟動(dòng)Hbase異步預(yù)熱
通過debug日志發(fā)現(xiàn)另一個(gè)地方有大約6s的空白,通過查看前后關(guān)聯(lián)日志發(fā)現(xiàn)是在初始化Hbase配置的時(shí)候出現(xiàn)的,我們項(xiàng)目中使用的Hbase在應(yīng)用啟動(dòng)后第一次訪問會(huì)非常慢,原因是在第一次請求時(shí)需要緩存元數(shù)據(jù)到本地,導(dǎo)致接口超時(shí),所以后來就改成了啟動(dòng)時(shí),實(shí)現(xiàn)SmartInitializingSingleton的afterSingletonsInstantiated接口去預(yù)熱,但是最早實(shí)現(xiàn)的時(shí)候使用了localhost線程,所以會(huì)阻塞主流程,優(yōu)化方案很簡單,就是改成起一個(gè)異步線程去預(yù)熱。
3、使用自定義BeanPostProcessor方案
3.1 @JsfConsumer消費(fèi)者異步導(dǎo)出
通過上面兩項(xiàng)優(yōu)化,我們的應(yīng)用啟動(dòng)速度有了稍許改善,但是上面的都還是小試牛刀,下面要講的才是真正解決啟動(dòng)速度過慢的方案。
我們知道Spring容器啟動(dòng)大體可分為Bean定義的掃描和注冊、bean的實(shí)例化、屬性注入、bean的后置處理等,其實(shí)真正耗時(shí)的部分基本都在bean的后置處理部分,當(dāng)然有時(shí)候?qū)τ贐ean定義掃描范圍過大也可能會(huì)帶來一定的耗時(shí),但是這塊不是本文章的重點(diǎn)部分 ,我們暫時(shí)先忽略,那如何去定位是哪些bean的后置處理過慢,我們可以通過增加兩個(gè)BeanPostProcess來實(shí)現(xiàn)。
通過自定義BeanPostProcess,我們可以在每個(gè)bean的初始化前后加埋點(diǎn),這樣就可以統(tǒng)計(jì)出單個(gè)bean初始化的耗時(shí)情況,具體的方案是使用兩個(gè)BeanPostProcess,分別是TimeCostPriorityOrderedBeanPostProcessor、ZLowOrderTimeCostBeanPostProcessor,下面分別來介紹這兩個(gè)后置處理器的作用。
我們知道BeanPostProcessor其實(shí)有兩個(gè)回調(diào)方法,我們可以簡單的稱它們?yōu)閎efore和after方法,TimeCostPriorityOrderedBeanPostProcessor是一個(gè)高優(yōu)先級的后置處理器,所以會(huì)優(yōu)先執(zhí)行,我們可以用它來監(jiān)控容器中所有bean執(zhí)行BeanPostProcessor的before方法的執(zhí)行耗時(shí),具體實(shí)現(xiàn)也很簡單,就是在這個(gè)后置處理器的before方法里面先記錄下每個(gè)bean的執(zhí)行的初始時(shí)間,然后在after方法里面計(jì)算結(jié)束時(shí)間,中間的差值就是每個(gè)bean執(zhí)行所有BeanPostProcessor的耗時(shí),具體代碼如下
@Component
@Slf4j
public class TimeCostPriorityOrderedBeanPostProcessor implements BeanPostProcessor, PriorityOrdered {
private Map<String, Long> costMap = Maps.newConcurrentMap();
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
costMap.put(beanName, System.currentTimeMillis());
return bean;
}
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
if (costMap.containsKey(beanName)) {
Long start = costMap.get(beanName);
long cost = System.currentTimeMillis() - start;
if (cost > 50) {
log.info("==>Before方法耗時(shí)beanName:{},操作耗時(shí):{}", beanName, cost);
}
}
return bean;
}
@Override
public int getOrder() {
return Integer.MIN_VALUE;
}
}
另外一個(gè)后置處理器是ZLowOrderTimeCostBeanPostProcessor ,他基本會(huì)最后執(zhí)行,原因是他沒有實(shí)現(xiàn)優(yōu)先級接口,同時(shí)類名還是Z開頭的,我們可以用它來監(jiān)控容器中所有bean執(zhí)行BeanPostProcessor的after方法的執(zhí)行耗時(shí),具體實(shí)現(xiàn)方式和上文類似,下面直接貼出源碼
@Component
@Slf4j
public class ZBeanPostProcessor implements BeanPostProcessor {
private Map<String, Long> costMap = Maps.newConcurrentMap();
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
costMap.put(beanName, System.currentTimeMillis());
return bean;
}
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
if (costMap.containsKey(beanName)) {
Long start = costMap.get(beanName);
long cost = System.currentTimeMillis() - start;
if (cost > 50) {
log.info("==>After方法耗時(shí)beanName:{},操作耗時(shí):{}", beanName, cost);
}
}
return bean;
}
}
通過在項(xiàng)目中增加上面的兩個(gè)BeanPostProcessor,我們重新啟動(dòng)應(yīng)用后,發(fā)現(xiàn)有很多bean的初始化時(shí)間都超過了50ms,再進(jìn)一步觀察后會(huì)發(fā)現(xiàn),這些加載慢的bean基本都有一個(gè)共同的特征,那就是這些bean的屬性上面或多或少都帶有@JsfConsumer注解,那么為什么屬性上帶有@JsfConsumer注解會(huì)導(dǎo)致這個(gè)bean初始化變慢,顯而易見,肯定是這個(gè)注解對應(yīng)的驅(qū)動(dòng)類做了什么特殊操作導(dǎo)致了變慢,下面我們來分析一下。
- 我們項(xiàng)目中引用了大約170+的JSF consumer接口,@JsfConsumer注解是我們內(nèi)部自己定義的一個(gè)針對Cosnumer接口使用的注解,他的使用方式很簡單,只需要在引用的接口上打上這個(gè)注解,內(nèi)部的starter會(huì)自動(dòng)的幫你去做consumer的refer,在RPC調(diào)用中,我們作為消費(fèi)者去調(diào)用生產(chǎn)者,其實(shí)我們只是依賴了一個(gè)接口,需要在啟動(dòng)的時(shí)候調(diào)用refer方法幫我們?nèi)ド纱韺ο笪覀儾拍馨l(fā)起調(diào)用,通過翻閱starter的源碼發(fā)現(xiàn)是通過一個(gè)BeanPostProcessor的postProcessBeforeInitialization方法去做的,源碼如下
@SneakyThrows
@Override
public Object postProcessBeforeInitialization(@NonNull Object bean, String beanName) throws BeansException {
if (this.isMatchPackage(bean)) {
if (isProvider()) {
this.scanProvider(bean, beanName);
}
if (isConsumer()) {
this.scanConsumer(bean, beanName);
}
}
return bean;
}
private void scanConsumer(Object bean, String beanName) {
Class<?> beanClass = bean.getClass();
Set<Field> fields = getFieldSetWithSuperClassFields(beanClass);
for (Field field : fields) {
boolean accessible = field.isAccessible();
try {
if (!accessible) {
field.setAccessible(true);
}
JsfConsumerTemplate jsfConsumerTemplate = null;
JsfConsumer jsfConsumer = field.getAnnotation(JsfConsumer.class);
if (jsfConsumer != null) {
jsfConsumerTemplate = convert(jsfConsumer, JsfConsumerTemplate.class);
}
if (jsfConsumerTemplate != null) {
logger.info("==> Final consumer config for JSF interface [{}]: {}", beanClass, JSON.toJSONString(jsfConsumerTemplate));
//生成客戶端配置
ConsumerConfig consumerConfig = this.parseAnnotationConsumer(jsfConsumerTemplate, field.getType());
addFilters(beanName, consumerConfig);
//觸發(fā)refer方法執(zhí)行,生成代理對象
Object ref = consumerConfig.refer();
if (ref != null) {
if (!this.beanFactory.containsSingleton(field.getName())) {
//將生成的對象注冊到spring容器
this.beanFactory.registerSingleton(field.getName(), consumerConfig);
}
Object fieldBean = beanFactory.getBean(field.getName());
try {
logger.info("JsfConsumer class {} field {} inject", bean.getClass().getSimpleName(), field.getName());
//將代理對象設(shè)置到對應(yīng)的屬性
field.set(bean, fieldBean);
} catch (IllegalAccessException exp) {
throw new InitErrorException("Set proxy to field error", exp);
}
}
}
} finally {
if (!accessible) {
field.setAccessible(false);
}
}
}
}
- 通過分析上面的代碼,可以發(fā)現(xiàn)唯一耗時(shí)的地方只可能在refer方法的調(diào)用,所以修改stater源碼,在refer方法執(zhí)行的前后打印日志,重新部署后查看日志,發(fā)現(xiàn)果然就是這個(gè)方法執(zhí)行很慢,有的接口甚至達(dá)到了十幾秒,因?yàn)閟pring容器的初始化是單線程執(zhí)行,這些時(shí)間累加起來就是一個(gè)很大的量級,隨即閱讀JSF源碼,探究一下為什么會(huì)慢。
- 通過從refer方法進(jìn)入,一步步探究,最終找到了和服務(wù)端建立連接的代碼,,這個(gè)方法的大體流程就是找到這個(gè)接口所有的提供者,和這些提供者所在的機(jī)器建立長連接用于后續(xù)的通信,建立的時(shí)候使用了多線程,但是如果一個(gè)接口對應(yīng)的提供者太多,或者某些提供者機(jī)器不健康了,就可能會(huì)導(dǎo)致整個(gè)建立連接的時(shí)間過長。
- 分析出原因以后,解決方案也就很簡單了,既然JSF在建立連接的時(shí)候使用了線程池,那我們就可以在上面再套一層線程池,這樣refer方法的執(zhí)行就不會(huì)阻塞后續(xù)其他流程的執(zhí)行,只需要保證在應(yīng)用啟動(dòng)成功之前我們的異步線程都執(zhí)行完成即可,具體改動(dòng)如下
public static final ThreadPoolExecutor CONSUMER_REFER = new ThreadPoolExecutor(
32, 32,
60, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(102400),
ThreadFactories.create("JSF-REFER"),
new ThreadPoolExecutor.CallerRunsPolicy());
/**
* 掃描當(dāng)前bean的所有屬性,查看該屬性是否攜帶有JsfConsumer注解,如果有的話,需要調(diào)用{@link ConsumerConfig#refer()}生成代理,
* 該方法使用了異步線程池進(jìn)行操作,這樣可以不阻塞localhost線程的執(zhí)行
* 在進(jìn)行導(dǎo)出之前使用了synchronized關(guān)鍵字鎖住當(dāng)前字段,防止出現(xiàn)并發(fā),出現(xiàn)并發(fā)的情況可能是在兩個(gè)bean里面注入了同一個(gè)consumer導(dǎo)致
* @param bean
* @param beanName
*/
private void scanConsumer(Object bean, String beanName) {
Class<?> beanClass = bean.getClass();
Set<Field> fields = getFieldSetWithSuperClassFields(beanClass);
for (Field field : fields) {
boolean accessible = field.isAccessible();
//首先將需要處理的字段計(jì)數(shù),防止在異步線程丟失
REFER_COUNT.increment();
Future<?> referFuture = CONSUMER_REFER.submit(() -> {
try {
if (!accessible) {
field.setAccessible(true);
}
JsfConsumerTemplate jsfConsumerTemplate = null;
JsfConsumer jsfConsumer = field.getAnnotation(JsfConsumer.class);
if (jsfConsumer != null) {
jsfConsumerTemplate = convert(jsfConsumer, JsfConsumerTemplate.class);
}
if (jsfConsumerTemplate != null) {
//鎖住當(dāng)前字段,防止多線程并發(fā)導(dǎo)出導(dǎo)致異常
synchronized (field.getType()) {
logger.info("==> Final consumer config for JSF interface [{}]: {}", beanClass, JSON.toJSONString(jsfConsumerTemplate));
ConsumerConfig consumerConfig = this.parseAnnotationConsumer(jsfConsumerTemplate, field.getType());
addFilters(beanName, consumerConfig);
Object ref = consumerConfig.refer();
if (ref != null) {
if (!this.beanFactory.containsSingleton(field.getName())) {
this.beanFactory.registerSingleton(field.getName(), consumerConfig);
}
Object fieldBean = beanFactory.getBean(field.getName());
try {
logger.info("JsfConsumer class {} field {} inject", bean.getClass().getSimpleName(), field.getName());
field.set(bean, fieldBean);
} catch (IllegalAccessException var18) {
throw new InitErrorException("Set proxy to field error", var18);
}
}
}
}
} finally {
if (!accessible) {
field.setAccessible(false);
}
//執(zhí)行完成后減1
REFER_COUNT.decrement();
}
});
//加到future里面 會(huì)監(jiān)聽ContextRefreshedEvent判斷異步任務(wù)是否都已經(jīng)完成
REFER_FUTURE.add(referFuture);
}
}
/**
* 監(jiān)聽容器刷新事件
*
* @param contextRefreshedEvent the event to respond to
*/
@Override
@Order(Ordered.LOWEST_PRECEDENCE)
public synchronized void onApplicationEvent(@NonNull ContextRefreshedEvent contextRefreshedEvent) {
logger.info("==> Ready for JSF consumer refer! Application name: {} count:{}", contextRefreshedEvent.getApplicationContext().getApplicationName(), REFER_FUTURE.size());
CONSUMER_REFER.allowCoreThreadTimeOut(true);
try {
int i = 100;
boolean isDone = false;
while (i-- > 0) {
if (REFER_COUNT.sum() == 0) {
isDone = true;
break;
}
Thread.sleep(100);
}
if (!isDone) {
throw new InitErrorException(Strings.format("init jsf consumer error, undo sum :[{}]", REFER_COUNT.sum()));
}
for (Future<?> future : REFER_FUTURE) {
future.get();
}
} catch (Exception exp) {
// 在Web應(yīng)用中,容器可能是一個(gè)父子容器,因此關(guān)閉上下文時(shí)需要遞歸往上遍歷,把父容器也一起收拾掉
logger.error("<== Exception while batch exporting JSF refer!", exp instanceof ApplicationException ? exp.getCause() : exp);
ApplicationContext toClose = contextRefreshedEvent.getApplicationContext();
while (toClose instanceof ConfigurableApplicationContext) {
ApplicationContext parentApplicationContext = toClose.getParent();
try {
((ConfigurableApplicationContext) toClose).close();
} catch (Exception closeExp) {
logger.error("<== Exception while close application context: {}", toClose.getDisplayName(), closeExp);
}
toClose = parentApplicationContext;
}
return;
}
}
3.2 魔改JSF自帶的ConsumerBean
- 我們項(xiàng)目中除了上述通過@JsfConsumer注解生成客戶端代理外,還有另外一種方式來生成客戶端代理,那就是借助JSF自身提供的ConsumerBean類,該類實(shí)現(xiàn)了FactoryBean接口,可以將每個(gè)JsfConsumer的配置都生成一個(gè)BeanDefinition實(shí)例,同時(shí)設(shè)置BeanDefinition的beanClass屬性來使用它,使用這種方法可以確保生成的bean一定是單例的,防止上面那種方式可能存在不同類的同一個(gè)JSF Consumer配置不一致的情況。
- 熟悉Spring源碼的小伙伴都知道,容器刷新的時(shí)候會(huì)使用BeanDefinition里面的beanClass屬性來實(shí)例化對象,這里我們指定這個(gè)屬性為ConsumerBean.class,等到容器刷新初始化對象完成后,會(huì)繼續(xù)判斷該對象是否實(shí)現(xiàn)了FactoryBean接口,我們用的這個(gè)肯定實(shí)現(xiàn)了,然后會(huì)觸發(fā)該對象getObject方法調(diào)用,那么我們就看下JSF自帶的這個(gè)FactoryBean的getObject方法都做了些什么,源碼如下
/**
* 根據(jù)config實(shí)例化所需的Reference實(shí)例<br/>
* 返回的應(yīng)該是具備全操作能力的接口代理實(shí)現(xiàn)類對象
*
* @see org.springframework.beans.factory.FactoryBean#getObject()
*/
@Override
@JSONField(serialize = false)
public T getObject() throws Exception {
object = CommonUtils.isUnitTestMode() ? null : refer();
return object;
}
- 還是熟悉的配方,和我們上面分析的調(diào)用refer方法一模一樣,因?yàn)閏onsumer初始化的核心就是通過refer方法生成代理對象,然后客戶端持有代理對象,使用的時(shí)候通過代理對象去發(fā)起遠(yuǎn)程調(diào)用,但是這里有個(gè)關(guān)鍵問題就是之前的refer方法是我們自己控制的,我們可以任意去修改調(diào)用他的位置,但是現(xiàn)在的是JSF自己提供的,我們沒法修改JSF的源碼,而且他的觸發(fā)是容器回調(diào)的,那我們該怎么辦?
- 其實(shí)這個(gè)時(shí)候我們可以想一下,refer方法慢,我們想加速,可以和上面一樣使用異步模式,但是異步的代碼寫在哪里,其次是這個(gè)方法是容器回調(diào)觸發(fā),如果我們開啟了異步,那容器就得不到真實(shí)的引用,會(huì)導(dǎo)致錯(cuò)誤,那應(yīng)該怎么解決?
- 這個(gè)時(shí)候代理的作用就顯示出來了,我們其實(shí)可以先給容器返回一個(gè)我們自己生成的代理對象,然后我們這個(gè)代理對象再包裝原來refer產(chǎn)生的對象,這樣客戶端實(shí)際持有的是我們自己生成的代理對象,而不是JSF refer方法產(chǎn)生的代理對象,剩下的最后一步就是仿照原來的ConsumerBean魔改一個(gè)我們自己的版本,將getObject方法的邏輯改為使用線程池導(dǎo)出,同時(shí)先返回一個(gè)自己的代理對象,其實(shí)這種提前返回代理的思想也適用于其他一些場景。
- 我們可以通過一個(gè)流程圖來梳理一下上面說的流程。
圖片
- 魔改后的源碼如下,供大家參考。
/**
* 延遲導(dǎo)出的ConsumerBean
* 該類的getObject方法會(huì)返回一個(gè)自定義的代理對象
*/
public class DelayConsumerBean<T> extends ConsumerConfig<T> implements InitializingBean, FactoryBean<T>, ApplicationContextAware, DisposableBean, BeanNameAware {
private static final long serialVersionUID = 6835324481364430812L;
private static final Logger LOGGER = LoggerFactory.getLogger(DelayConsumerBean.class);
private ApplicationContext applicationContext;
@Setter
protected transient String beanName;
private transient T object;
private transient Class objectType;
public static final List<Future<?>> REFER_FUTURE_LIST = Lists.newArrayList();
public static final ThreadPoolExecutor CONSUMER_REFER_EXECUTOR = new ThreadPoolExecutor(
32, 32,
60, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(102400),
ThreadFactories.create("JSF-REFER-2"),
new ThreadPoolExecutor.CallerRunsPolicy());
@JSONField(
serialize = false
)
@SuppressWarnings("unchecked")
public T getObject() throws Exception {
Class<T> consumerInterfaceClass = ClassLoaderUtils.forName(this.interfaceId);
//先生成一個(gè)代理對象
T delayConsumer = (T) Proxy.newProxyInstance(
consumerInterfaceClass.getClassLoader(),
new Class[]{consumerInterfaceClass},
new DelayConsumerInvocationHandler()
);
//使用異步線程refer
REFER_FUTURE_LIST.add(CONSUMER_REFER_EXECUTOR.submit(() -> {
super.refer();
}));
object = CommonUtils.isUnitTestMode() ? null : delayConsumer;
//返回提前生成的代理對象
return object;
}
private class DelayConsumerInvocationHandler implements InvocationHandler {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if (method.getDeclaringClass() == Object.class) {
return method.invoke(DelayConsumerBean.this, args);
}
if (DelayConsumerBean.this.proxyIns == null) {
throw new RuntimeException("DelayConsumerBean.this.proxyIns is null");
}
try {
//客戶端發(fā)起調(diào)用后,觸發(fā)真實(shí)的consumer代理執(zhí)行
return method.invoke(DelayConsumerBean.this.proxyIns, args);
} catch (InvocationTargetException exp) {
throw exp.getTargetException();
} catch (Exception ex) {
throw ApplicationException.mirrorOf(ex);
}
}
}
/**
* 等待所有的任務(wù)完成,確保所有的consumer都refer成功了
* @param contextRefreshedEvent
*/
public static void waitForRefer(ContextRefreshedEvent contextRefreshedEvent) {
CONSUMER_REFER_EXECUTOR.allowCoreThreadTimeOut(true);
LOGGER.info("==> Ready for JSF consumer 2 refer! Application name: {} count:{}", contextRefreshedEvent.getApplicationContext().getApplicationName(), REFER_FUTURE_LIST.size());
try {
for (Future<?> future : REFER_FUTURE_LIST) {
future.get();
}
} catch (Exception exp) {
// 在Web應(yīng)用中,容器可能是一個(gè)父子容器,因此關(guān)閉上下文時(shí)需要遞歸往上遍歷,把父容器也一起關(guān)閉
LOGGER.error("<== Exception while batch exporting JSF provider!", exp instanceof ApplicationException ? exp.getCause() : exp);
ApplicationContext toClose = contextRefreshedEvent.getApplicationContext();
while (toClose instanceof ConfigurableApplicationContext) {
ApplicationContext parentApplicationContext = toClose.getParent();
try {
((ConfigurableApplicationContext) toClose).close();
} catch (Exception closeExp) {
LOGGER.error("<== Exception while close application context: {}", toClose.getDisplayName(), closeExp);
}
toClose = parentApplicationContext;
}
}
}
protected DelayConsumerBean() {
}
public void setApplicationContext(ApplicationContext appContext) throws BeansException {
this.applicationContext = appContext;
}
}
我們的這個(gè)應(yīng)用在不同的環(huán)境存在兩套tomcat版本,分別為8.0.53和8.5.42,通過分析不同版本的tomcat啟動(dòng)的debug日志可以發(fā)現(xiàn),在8.5.42版本下,Spring在實(shí)例化對象的時(shí)候比在8.0.53版本下慢,每個(gè)bean都慢一點(diǎn)點(diǎn),我們項(xiàng)目中一共2900+bean,所以就導(dǎo)致不同的tomcat版本也影響啟動(dòng)速度,這塊懷疑是高版本的Tomcat對于類加載的方式可能有變化導(dǎo)致,換到低版本以后速度就會(huì)變快。
5.金錢的力量
- 不同的機(jī)房對于應(yīng)用的啟動(dòng)速度也會(huì)有一些影響,如果使用的機(jī)器是一些老舊過保的機(jī)器,也會(huì)對應(yīng)用的啟動(dòng)速度有影響,使用舊機(jī)器也無法保障應(yīng)用的穩(wěn)定性,可以將應(yīng)用遷移到高性能的新機(jī)房去,這樣可以加快應(yīng)用啟動(dòng)的速度。
- 在京東內(nèi)部對比發(fā)現(xiàn),兩個(gè)不同的機(jī)房對于同一個(gè)應(yīng)用啟動(dòng)速度差距非常大,在新機(jī)房里面比舊機(jī)房快至少20%。
四、方案總結(jié)和效果展示
下面我們來總結(jié)一下我們都做了什么
- Tomcat啟動(dòng)關(guān)閉TLD文件掃描。
- 應(yīng)用啟動(dòng)Hbase預(yù)熱異步化,其實(shí)這塊我們可以應(yīng)用到其他場景,大家可以檢查下自己的項(xiàng)目啟動(dòng)的時(shí)候有沒有同步預(yù)熱的場景。
- JsfConsumer客戶端代理bean異步生成,此處主要核心在于使用一個(gè)自定義代理對象提前返回引用,這種思想我們也可以應(yīng)用到其他需要異步初始化對象的地方去,具體可以參考3.2節(jié)的流程圖。
- 不同Tomcat版本對于應(yīng)用啟動(dòng)速度的影響。
- 更換到高性能機(jī)房對于啟動(dòng)速度的提升。
通過上述的幾項(xiàng)優(yōu)化過后,應(yīng)用的啟動(dòng)速度得到了大幅改善,下圖展示是一個(gè)簡略的新舊對比,可以看到應(yīng)用的啟動(dòng)速度從之前400-500秒到現(xiàn)在的130-150s,提升了近60%多。
(優(yōu)化后)
(優(yōu)化前)