作者:京东零售 宋维飞
一、前言
本文记录了在大促前针对SpringBoot应用启动速度过慢而采取的优化方案,主要介绍了如何定位启动速度慢的阻塞点,以及如何解决这些问题。希望可以帮助大家了解如何定位该类问题以及提供一些解决此类问题的思路。(本文使用的SpringBoot版本为2.6.2,JSF版本为1.7.6-HOTFIX-T2)
二、问题背景
1.广告投放平台核心应用生产环境单台机器部署需要400-500s,加上镜像下载时间大约1分钟,整体耗时接近10分钟,应用部署时间过长会带来诸多问题,例如上线遇到问题需要进行快速回滚,启动时间过长会延长恢复时间,影响业务的连续性。
2.会导致整个应用的发布时间变长,目前我们这个应用有上百个容器,单次部署编排时间超过半个小时,上线等待时间过长;另一个问题是在测试预发环境联调修改一次编译+部署就要十几分钟,严重影响联调进度,浪费了很多时间。
3.综上所述,可以看出对应用的启动速度优化刻不容缓,浪费时间就是在浪费生命,所以我们必须要做些什么,下面通过一张图先来看下旧的生产集群发布的概况。

三、解决方案
要想排查应用启动速度过慢的问题,需要先定位到是哪里慢,乍看这句话,就和没说一样,其实就是陈述一下事实😀,下面介绍几种常见的排查方法:
1、使用SpringBoot的监控actuator
SpringBoot的actuator是一个监控和管理SpringBoot应用的框架,它提供了许多内置的端点,允许你访问应用程序的运行时信息,如健康检查、环境属性、日志、指标、线程转储等,所以我们可以使用其中的一个端点startup去查看整个应用中所有bean的启动耗时。
但是这种模式下也有个弊端,就是每个bean的加载耗时其实是不准确的,比如A开头的bean,他的总计耗时会非常的长,原因是因为在这个bean里面去加载其他bean的耗时也会累加到这个bean上,导致数据不准确,所以该方案只能当做参考。(具体该组件的使用方式可以参考该链接 点试试 )
2、根据应用的启动日志分析
这个方法对于项目启动日志比较少的情况有一定的效果,可以在应用启动时通过开启debug日志来逐秒分析,查看启动日志打印的空白秒,就可以大概定位到阻塞点,定位到耗时的地方后再具体问题具体分析,但是对于大型项目,启动一次有几万行的启动日志,人工直接分析费时费力,而且还不好定位问题,所以此方法不具有普适性。虽然不好用,但是我们也是可以用一下的,通过这个方面我们定位到了两处阻塞点。
2.1 Tomcat启动扫描TLD文件优化
因为我们使用的是外置Tomcat容器,通过查看启动日志发现Tomcat启动日志打印的时间和Tomcat引导Spring容器启动中间隔了几十秒没有额外的日志,只有一行 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,因为没有别的日志,只能从这行日志开刀,看这行日志大体的意思是Tomcat在启动时尝试扫描某些JAR文件以查找TLD文件,但没有找到。
那么Tomcat在启动的时候为什么要扫描TLD文件(什么是TLD文件?点击这里了解 ),Tomcat 作为 Servlet 规范的实现者,它在应用启动的时候会扫描 Jar 包里面的 .tld 文件,加载里面定义的标签库,但是,我们项目没有使用 JSP 作为 Web 页面的模板,为了加快应用的启动速度,我们可以把 Tomcat 里面的这个功能给关掉那么如何关闭这个扫描以提高启动速度?,通过查阅文档发现可以修改Tomcat的catalina.properties文件,修改一个配置项tomcat.util.scan.StandardJarScanFilter.jarsToSkip设置值为 *.jar就可以关闭扫描 但是我们的Tomcat都是打到镜像里面的,并没有给我们暴露修改配置参数的入口,通过查看行云的帮助文档,发现可以在启动脚本start_before.sh文件里面使用sed命令进行修改
sed -i '$a\tomcat.util.scan.StandardJarScanFilter.jarsToSkip=*.jar' /export/Domains/jdos应用名.jd.com/server1/conf/catalina.properties
2.2 应用启动Hbase异步预热
通过debug日志发现另一个地方有大约6s的空白,通过查看前后关联日志发现是在初始化Hbase配置的时候出现的,我们项目中使用的Hbase在应用启动后第一次访问会非常慢,原因是在第一次请求时需要缓存元数据到本地,导致接口超时,所以后来就改成了启动时,实现SmartInitializingSingleton的afterSingletonsInstantiated接口去预热,但是最早实现的时候使用了localhost线程,所以会阻塞主流程,优化方案很简单,就是改成起一个异步线程去预热。
3、使用自定义BeanPostProcessor方案
3.1 @JsfConsumer消费者异步导出
通过上面两项优化,我们的应用启动速度有了稍许改善,但是上面的都还是小试牛刀,下面要讲的才是真正解决启动速度过慢的方案。
我们知道Spring容器启动大体可分为Bean定义的扫描和注册、bean的实例化、属性注入、bean的后置处理等,其实真正耗时的部分基本都在bean的后置处理部分,当然有时候对于Bean定义扫描范围过大也可能会带来一定的耗时,但是这块不是本文章的重点部分 ,我们暂时先忽略,那如何去定位是哪些bean的后置处理过慢,我们可以通过增加两个BeanPostProcess来实现。
通过自定义BeanPostProcess,我们可以在每个bean的初始化前后加埋点,这样就可以统计出单个bean初始化的耗时情况,具体的方案是使用两个BeanPostProcess,分别是TimeCostPriorityOrderedBeanPostProcessor、ZLowOrderTimeCostBeanPostProcessor,下面分别来介绍这两个后置处理器的作用。
我们知道BeanPostProcessor其实有两个回调方法,我们可以简单的称它们为before和after方法,TimeCostPriorityOrderedBeanPostProcessor是一个高优先级的后置处理器,所以会优先执行,我们可以用它来监控容器中所有bean执行BeanPostProcessor的before方法的执行耗时,具体实现也很简单,就是在这个后置处理器的before方法里面先记录下每个bean的执行的初始时间,然后在after方法里面计算结束时间,中间的差值就是每个bean执行所有BeanPostProcessor的耗时,具体代码如下
1@Component 2@Slf4j 3public class TimeCostPriorityOrderedBeanPostProcessor implements BeanPostProcessor, PriorityOrdered { 4 5 private Map<String, Long> costMap = Maps.newConcurrentMap(); 6 7 @Override 8 public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { 9 costMap.put(beanName, System.currentTimeMillis()); 10 return bean; 11 } 12 13 @Override 14 public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { 15 if (costMap.containsKey(beanName)) { 16 Long start = costMap.get(beanName); 17 long cost = System.currentTimeMillis() - start; 18 if (cost > 50) { 19 log.info("==>Before方法耗时beanName:{},操作耗时:{}", beanName, cost); 20 } 21 } 22 return bean; 23 } 24 25 @Override 26 public int getOrder() { 27 return Integer.MIN_VALUE; 28 } 29}
另外一个后置处理器是ZLowOrderTimeCostBeanPostProcessor ,他基本会最后执行,原因是他没有实现优先级接口,同时类名还是Z开头的,我们可以用它来监控容器中所有bean执行BeanPostProcessor的after方法的执行耗时,具体实现方式和上文类似,下面直接贴出源码
1@Component 2@Slf4j 3public class ZBeanPostProcessor implements BeanPostProcessor { 4 5 private Map<String, Long> costMap = Maps.newConcurrentMap(); 6 7 @Override 8 public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { 9 costMap.put(beanName, System.currentTimeMillis()); 10 return bean; 11 } 12 13 @Override 14 public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { 15 if (costMap.containsKey(beanName)) { 16 Long start = costMap.get(beanName); 17 long cost = System.currentTimeMillis() - start; 18 if (cost > 50) { 19 log.info("==>After方法耗时beanName:{},操作耗时:{}", beanName, cost); 20 } 21 } 22 return bean; 23 } 24}
通过在项目中增加上面的两个BeanPostProcessor,我们重新启动应用后,发现有很多bean的初始化时间都超过了50ms,再进一步观察后会发现,这些加载慢的bean基本都有一个共同的特征,那就是这些bean的属性上面或多或少都带有@JsfConsumer注解,那么为什么属性上带有@JsfConsumer注解会导致这个bean初始化变慢,显而易见,肯定是这个注解对应的驱动类做了什么特殊操作导致了变慢,下面我们来分析一下。
1.我们项目中引用了大约170+的JSF consumer接口,@JsfConsumer注解是我们内部自己定义的一个针对Cosnumer接口使用的注解,他的使用方式很简单,只需要在引用的接口上打上这个注解,内部的starter会自动的帮你去做consumer的refer,在RPC调用中,我们作为消费者去调用生产者,其实我们只是依赖了一个接口,需要在启动的时候调用refer方法帮我们去生成代理对象我们才能发起调用,通过翻阅starter的源码发现是通过一个BeanPostProcessor的postProcessBeforeInitialization方法去做的,源码如下
1 @SneakyThrows 2 @Override 3 public Object postProcessBeforeInitialization(@NonNull Object bean, String beanName) throws BeansException { 4 if (this.isMatchPackage(bean)) { 5 6 if (isProvider()) { 7 this.scanProvider(bean, beanName); 8 } 9 10 if (isConsumer()) { 11 this.scanConsumer(bean, beanName); 12 } 13 } 14 return bean; 15 } 16 17 private void scanConsumer(Object bean, String beanName) { 18 Class<?> beanClass = bean.getClass(); 19 Set<Field> fields = getFieldSetWithSuperClassFields(beanClass); 20 21 for (Field field : fields) { 22 boolean accessible = field.isAccessible(); 23 24 try { 25 if (!accessible) { 26 field.setAccessible(true); 27 } 28 29 JsfConsumerTemplate jsfConsumerTemplate = null; 30 JsfConsumer jsfConsumer = field.getAnnotation(JsfConsumer.class); 31 if (jsfConsumer != null) { 32 jsfConsumerTemplate = convert(jsfConsumer, JsfConsumerTemplate.class); 33 } 34 35 if (jsfConsumerTemplate != null) { 36 logger.info("==> Final consumer config for JSF interface [{}]: {}", beanClass, JSON.toJSONString(jsfConsumerTemplate)); 37 //生成客户端配置 38 ConsumerConfig consumerConfig = this.parseAnnotationConsumer(jsfConsumerTemplate, field.getType()); 39 40 addFilters(beanName, consumerConfig); 41 //触发refer方法执行,生成代理对象 42 Object ref = consumerConfig.refer(); 43 if (ref != null) { 44 if (!this.beanFactory.containsSingleton(field.getName())) { 45 //将生成的对象注册到spring容器 46 this.beanFactory.registerSingleton(field.getName(), consumerConfig); 47 } 48 49 Object fieldBean = beanFactory.getBean(field.getName()); 50 try { 51 logger.info("JsfConsumer class {} field {} inject", bean.getClass().getSimpleName(), field.getName()); 52 //将代理对象设置到对应的属性 53 field.set(bean, fieldBean); 54 } catch (IllegalAccessException exp) { 55 throw new InitErrorException("Set proxy to field error", exp); 56 } 57 } 58 } 59 } finally { 60 if (!accessible) { 61 field.setAccessible(false); 62 } 63 64 } 65 } 66 } 67
2、通过分析上面的代码,可以发现唯一耗时的地方只可能在refer方法的调用,所以修改stater源码,在refer方法执行的前后打印日志,重新部署后查看日志,发现果然就是这个方法执行很慢,有的接口甚至达到了十几秒,因为spring容器的初始化是单线程执行,这些时间累加起来就是一个很大的量级,随即阅读JSF源码,探究一下为什么会慢。
3、通过从refer方法进入,一步步探究,最终找到了和服务端建立连接的代码,对应JSF源码里的方法是com.jd.jsf.gd.client.Client#connectToProviders,这个方法的大体流程就是找到这个接口所有的提供者,和这些提供者所在的机器建立长连接用于后续的通信,建立的时候使用了多线程,但是如果一个接口对应的提供者太多,或者某些提供者机器不健康了,就可能会导致整个建立连接的时间过长,源码如下
1/** 2 * 连接服务端,建立Connection 3 * 4 * @param providerList 服务端列表 5 */ 6 protected void connectToProviders(List<Provider> providerList) { 7 if (DataPlaneContext.isEnableRuntimeSvc(consumerConfig)) { 8 return; 9 } 10 final String interfaceId = consumerConfig.getInterfaceId(); 11 int providerSize = providerList.size(); 12 LOGGER.info("Init provider of {}, size is : {}", interfaceId, providerSize); 13 if (providerSize > 0) { 14 // 随机下,减少同一个provider瞬间压力 15 Collections.shuffle(providerList); 16 17 // 多线程建立连接 18 int threads = Math.min(consumerConfig.getMaxConnectThreads(), providerSize); // 默认最大10个 19 final CountDownLatch latch = new CountDownLatch(providerSize); 20 ThreadPoolExecutor initPool = new ThreadPoolExecutor(threads, threads, 21 0L, TimeUnit.MILLISECONDS, 22 new LinkedBlockingQueue<Runnable>(providerList.size()), 23 new NamedThreadFactory("JSF-CLI-CONN-" + interfaceId, true)); 24 int connectTimeout = consumerConfig.getConnectTimeout(); 25 26 final AtomicInteger initialSize = new AtomicInteger(consumerConfig.getConnStrategy() == null ? providerSize : consumerConfig.getConnStrategy().getInitialSize()); 27 if (initialSize.get() <= Constants.DEFAULT_INITIAL_SIZE || initialSize.get() > providerSize) { 28 initialSize.set(providerSize); 29 } 30 31 if (initialSize.get() < threads && providerSize >= threads) { 32 initialSize.set(threads); 33 } 34 35 for (final Provider provider : providerList) { 36 final ClientTransportConfig config = providerToClientConfig(provider); 37 config.setConnectionTimeout(connectTimeout); 38 initPool.execute(new Runnable() { 39 @Override 40 public void run() { 41 try { 42 if (initialSize.get() <= 0) { 43 connectionHolder.addCandidate(provider, config); 44 return; 45 } 46 47 ClientTransport transport = ClientTransportFactory.getClientTransport(config); 48 if (connectionHolder.checkTransport(interfaceId, provider, transport)) { 49 boolean isOk = connectionHolder.addAlive(provider, transport); 50 if (isOk) { 51 initialSize.decrementAndGet(); 52 printSuccess(interfaceId, provider, transport); 53 } else { 54 printFailure(interfaceId, provider, transport); 55 } 56 } else { 57 connectionHolder.addRetry(provider, transport); 58 printFailure(interfaceId, provider, transport); 59 } 60 } catch (Exception e) { 61 connectionHolder.addDead(provider, config); 62 printDead(interfaceId, provider, e); 63 } finally { 64 latch.countDown(); // 连上或者抛异常 65 } 66 } 67 }); 68 } 69 70 try { 71 int totalTimeout = ((providerSize % threads == 0) ? (providerSize / threads) : ((providerSize / 72 threads) + 1)) * connectTimeout + 500; 73 latch.await(totalTimeout, TimeUnit.MILLISECONDS); // 一直等到子线程都结束 74 } catch (InterruptedException e) { 75 LOGGER.error("Exception when init provider", e); 76 } finally { 77 initPool.shutdown(); // 关闭线程池 78 } 79 } 80 }
4、分析出原因以后,解决方案也就很简单了,既然JSF在建立连接的时候使用了线程池,那我们就可以在上面再套一层线程池,这样refer方法的执行就不会阻塞后续其他流程的执行,只需要保证在应用启动成功之前我们的异步线程都执行完成即可,具体改动如下
1 public static final ThreadPoolExecutor CONSUMER_REFER = new ThreadPoolExecutor( 2 32, 32, 3 60, TimeUnit.SECONDS, 4 new LinkedBlockingQueue<>(102400), 5 ThreadFactories.create("JSF-REFER"), 6 new ThreadPoolExecutor.CallerRunsPolicy()); 7 8 /** 9 * 扫描当前bean的所有属性,查看该属性是否携带有JsfConsumer注解,如果有的话,需要调用{@link ConsumerConfig#refer()}生成代理, 10 * 该方法使用了异步线程池进行操作,这样可以不阻塞localhost线程的执行 11 * 在进行导出之前使用了synchronized关键字锁住当前字段,防止出现并发,出现并发的情况可能是在两个bean里面注入了同一个consumer导致 12 * @param bean 13 * @param beanName 14 */ 15 private void scanConsumer(Object bean, String beanName) { 16 Class<?> beanClass = bean.getClass(); 17 Set<Field> fields = getFieldSetWithSuperClassFields(beanClass); 18 19 for (Field field : fields) { 20 boolean accessible = field.isAccessible(); 21 //首先将需要处理的字段计数,防止在异步线程丢失 22 REFER_COUNT.increment(); 23 Future<?> referFuture = CONSUMER_REFER.submit(() -> { 24 try { 25 if (!accessible) { 26 field.setAccessible(true); 27 } 28 JsfConsumerTemplate jsfConsumerTemplate = null; 29 JsfConsumer jsfConsumer = field.getAnnotation(JsfConsumer.class); 30 if (jsfConsumer != null) { 31 jsfConsumerTemplate = convert(jsfConsumer, JsfConsumerTemplate.class); 32 } 33 if (jsfConsumerTemplate != null) { 34 //锁住当前字段,防止多线程并发导出导致异常 35 synchronized (field.getType()) { 36 logger.info("==> Final consumer config for JSF interface [{}]: {}", beanClass, JSON.toJSONString(jsfConsumerTemplate)); 37 ConsumerConfig consumerConfig = this.parseAnnotationConsumer(jsfConsumerTemplate, field.getType()); 38 39 addFilters(beanName, consumerConfig); 40 Object ref = consumerConfig.refer(); 41 if (ref != null) { 42 if (!this.beanFactory.containsSingleton(field.getName())) { 43 this.beanFactory.registerSingleton(field.getName(), consumerConfig); 44 } 45 46 Object fieldBean = beanFactory.getBean(field.getName()); 47 try { 48 logger.info("JsfConsumer class {} field {} inject", bean.getClass().getSimpleName(), field.getName()); 49 field.set(bean, fieldBean); 50 } catch (IllegalAccessException var18) { 51 throw new InitErrorException("Set proxy to field error", var18); 52 } 53 } 54 } 55 } 56 } finally { 57 if (!accessible) { 58 field.setAccessible(false); 59 } 60 //执行完成后减1 61 REFER_COUNT.decrement(); 62 } 63 }); 64 //加到future里面 会监听ContextRefreshedEvent判断异步任务是否都已经完成 65 REFER_FUTURE.add(referFuture); 66 } 67 } 68 69 /** 70 * 监听容器刷新事件 71 * 72 * @param contextRefreshedEvent the event to respond to 73 */ 74 @Override 75 @Order(Ordered.LOWEST_PRECEDENCE) 76 public synchronized void onApplicationEvent(@NonNull ContextRefreshedEvent contextRefreshedEvent) { 77 logger.info("==> Ready for JSF consumer refer! Application name: {} count:{}", contextRefreshedEvent.getApplicationContext().getApplicationName(), REFER_FUTURE.size()); 78 CONSUMER_REFER.allowCoreThreadTimeOut(true); 79 try { 80 int i = 100; 81 boolean isDone = false; 82 while (i-- > 0) { 83 if (REFER_COUNT.sum() == 0) { 84 isDone = true; 85 break; 86 } 87 Thread.sleep(100); 88 } 89 if (!isDone) { 90 throw new InitErrorException(Strings.format("init jsf consumer error, undo sum :[{}]", REFER_COUNT.sum())); 91 } 92 for (Future<?> future : REFER_FUTURE) { 93 future.get(); 94 } 95 } catch (Exception exp) { 96 // 在Web应用中,容器可能是一个父子容器,因此关闭上下文时需要递归往上遍历,把父容器也一起收拾掉 97 logger.error("<== Exception while batch exporting JSF refer!", exp instanceof ApplicationException ? exp.getCause() : exp); 98 ApplicationContext toClose = contextRefreshedEvent.getApplicationContext(); 99 while (toClose instanceof ConfigurableApplicationContext) { 100 ApplicationContext parentApplicationContext = toClose.getParent(); 101 try { 102 ((ConfigurableApplicationContext) toClose).close(); 103 } catch (Exception closeExp) { 104 logger.error("<== Exception while close application context: {}", toClose.getDisplayName(), closeExp); 105 } 106 toClose = parentApplicationContext; 107 } 108 return; 109 } 110 }
3.2 魔改JSF自带的ConsumerBean
1、我们项目中除了上述通过@JsfConsumer注解生成客户端代理外,还有另外一种方式来生成客户端代理,那就是借助JSF自身提供的ConsumerBean类,该类实现了FactoryBean接口,可以将每个Jsf Consumer的配置都生成一个BeanDefinition实例,同时设置BeanDefinition的beanClass属性来使用它,使用这种方法可以确保生成的bean一定是单例的,防止上面那种方式可能存在不同类的同一个JSF Consumer配置不一致的情况。
2、熟悉Spring源码的小伙伴都知道,容器刷新的时候会使用BeanDefinition里面的beanClass属性来实例化对象,这里我们指定这个属性为ConsumerBean.class,等到容器刷新初始化对象完成后,会继续判断该对象是否实现了FactoryBean接口,我们用的这个肯定实现了,然后会触发该对象getObject方法调用,那么我们就看下JSF自带的这个FactoryBean的getObject方法都做了些什么,源码如下
1 /** 2 * 根据config实例化所需的Reference实例<br/> 3 * 返回的应该是具备全操作能力的接口代理实现类对象 4 * 5 * @see org.springframework.beans.factory.FactoryBean#getObject() 6 */ 7 @Override 8 @JSONField(serialize = false) 9 public T getObject() throws Exception { 10 object = CommonUtils.isUnitTestMode() ? null : refer(); 11 return object; 12 }
3、还是熟悉的配方,和我们上面分析的调用refer方法一模一样,因为consumer初始化的核心就是通过refer方法生成代理对象,然后客户端持有代理对象,使用的时候通过代理对象去发起远程调用,但是这里有个关键问题就是之前的refer方法是我们自己控制的,我们可以任意去修改调用他的位置,但是现在的是JSF自己提供的,我们没法修改JSF的源码,而且他的触发是容器回调的,那我们该怎么办?
4、其实这个时候我们可以想一下,refer方法慢,我们想加速,可以和上面一样使用异步模式,但是异步的代码写在哪里,其次是这个方法是容器回调触发,如果我们开启了异步,那容器就得不到真实的引用,会导致错误,那应该怎么解决?
5、这个时候代理的作用就显示出来了,我们其实可以先给容器返回一个我们自己生成的代理对象,然后我们这个代理对象再包装原来refer产生的对象,这样客户端实际持有的是我们自己生成的代理对象,而不是JSF refer方法产生的代理对象,剩下的最后一步就是仿照原来的ConsumerBean魔改一个我们自己的版本,将getObject方法的逻辑改为使用线程池导出,同时先返回一个自己的代理对象,其实这种提前返回代理的思想也适用于其他一些场景。
6、我们可以通过一个流程图来梳理一下上面说的流程。

7、魔改后的源码如下,供大家参考
1/** 2 * 延迟导出的ConsumerBean 3 * 该类的getObject方法会返回一个自定义的代理对象 4 */ 5public class DelayConsumerBean<T> extends ConsumerConfig<T> implements InitializingBean, FactoryBean<T>, ApplicationContextAware, DisposableBean, BeanNameAware { 6 7 private static final long serialVersionUID = 6835324481364430812L; 8 9 private static final Logger LOGGER = LoggerFactory.getLogger(DelayConsumerBean.class); 10 11 private ApplicationContext applicationContext; 12 13 @Setter 14 protected transient String beanName; 15 16 private transient T object; 17 18 private transient Class objectType; 19 20 public static final List<Future<?>> REFER_FUTURE_LIST = Lists.newArrayList(); 21 22 public static final ThreadPoolExecutor CONSUMER_REFER_EXECUTOR = new ThreadPoolExecutor( 23 32, 32, 24 60, TimeUnit.SECONDS, 25 new LinkedBlockingQueue<>(102400), 26 ThreadFactories.create("JSF-REFER-2"), 27 new ThreadPoolExecutor.CallerRunsPolicy()); 28 29 @JSONField( 30 serialize = false 31 ) 32 @SuppressWarnings("unchecked") 33 public T getObject() throws Exception { 34 Class<T> consumerInterfaceClass = ClassLoaderUtils.forName(this.interfaceId); 35 //先生成一个代理对象 36 T delayConsumer = (T) Proxy.newProxyInstance( 37 consumerInterfaceClass.getClassLoader(), 38 new Class[]{consumerInterfaceClass}, 39 new DelayConsumerInvocationHandler() 40 ); 41 42 //使用异步线程refer 43 REFER_FUTURE_LIST.add(CONSUMER_REFER_EXECUTOR.submit(() -> { 44 super.refer(); 45 })); 46 47 object = CommonUtils.isUnitTestMode() ? null : delayConsumer; 48 //返回提前生成的代理对象 49 return object; 50 } 51 52 private class DelayConsumerInvocationHandler implements InvocationHandler { 53 @Override 54 public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { 55 if (method.getDeclaringClass() == Object.class) { 56 return method.invoke(DelayConsumerBean.this, args); 57 } 58 if (DelayConsumerBean.this.proxyIns == null) { 59 throw new RuntimeException("DelayConsumerBean.this.proxyIns is null"); 60 } 61 try { 62 //客户端发起调用后,触发真实的consumer代理执行 63 return method.invoke(DelayConsumerBean.this.proxyIns, args); 64 } catch (InvocationTargetException exp) { 65 throw exp.getTargetException(); 66 } catch (Exception ex) { 67 throw ApplicationException.mirrorOf(ex); 68 } 69 } 70 } 71 72 /** 73 * 等待所有的任务完成,确保所有的consumer都refer成功了 74 * @param contextRefreshedEvent 75 */ 76 public static void waitForRefer(ContextRefreshedEvent contextRefreshedEvent) { 77 CONSUMER_REFER_EXECUTOR.allowCoreThreadTimeOut(true); 78 LOGGER.info("==> Ready for JSF consumer 2 refer! Application name: {} count:{}", contextRefreshedEvent.getApplicationContext().getApplicationName(), REFER_FUTURE_LIST.size()); 79 try { 80 for (Future<?> future : REFER_FUTURE_LIST) { 81 future.get(); 82 } 83 } catch (Exception exp) { 84 // 在Web应用中,容器可能是一个父子容器,因此关闭上下文时需要递归往上遍历,把父容器也一起关闭 85 LOGGER.error("<== Exception while batch exporting JSF provider!", exp instanceof ApplicationException ? exp.getCause() : exp); 86 ApplicationContext toClose = contextRefreshedEvent.getApplicationContext(); 87 while (toClose instanceof ConfigurableApplicationContext) { 88 ApplicationContext parentApplicationContext = toClose.getParent(); 89 try { 90 ((ConfigurableApplicationContext) toClose).close(); 91 } catch (Exception closeExp) { 92 LOGGER.error("<== Exception while close application context: {}", toClose.getDisplayName(), closeExp); 93 } 94 toClose = parentApplicationContext; 95 } 96 } 97 } 98 99 100 protected DelayConsumerBean() { 101 } 102 103 public void setApplicationContext(ApplicationContext appContext) throws BeansException { 104 this.applicationContext = appContext; 105 } 106 107}
4、玄学-Tomcat版本问题?
1、在优化调试期间发现了一个比较诡异的问题,就是预发的启动速度比生产快,刚开始怀疑预发的机器consumer别名对应的provider机器少,所以建立连接块,但是优化了refer方法以后,发现还是预发快,随即对比了预发和生产的配置。
2、发现预发使用的是Tomcat8.0.53版本,生产使用的是8.5.42,所以把预发的也换成8.5.42,发现预发的应用启动速度也变慢,分析debug日志看到,在8.5.42版本下,Spring在实例化对象的时候比在8.0.53版本下慢,每个bean都慢一点点,我们项目中一共2900+bean,所以就导致不同的tomcat版本也影响启动速度,这块初步怀疑是高版本的Tomcat对于类加载可能有变化导致变慢,目前还未寻找到答案,如果有大佬知道是什么原因,欢迎在评论区留言探讨交流。
3、8.0.53版本对应行云镜像名称为 java-jd-centos6-jdk18-191-ngx197v1:latest
4、8.5.42版本对应行云镜像名称为 java-jd-centos6-jdk8.0.192-tom8.5.42-ngx197.v3:latest
5、金钱的力量
1、你能想到不同的机房对于应用的启动速度差距很大吗?这个发现纯属是运气问题,之前我在预发环境有个廊坊10的分组,同样的分支和配置他的启动速度总是比别的分组快,所以我怀疑是机器配置问题导致的,随即使用 宿主机配置查看 输入宿主机的ip即可查看宿主机的配置,果然发现廊坊10的机器都是新的未过保的机器,而其他像汇天二区和廊坊9等都是已经过保的机器,cpu和磁盘也比较老旧,所以会对应用的启动速度有影响,这里也验证了上面说的扫描TLD文件很慢的问题,因为扫描TLD文件需要找到项目中所有的jar包,涉及到大量的io操作,如果机器性能不好,速度就会很慢。
2、通过我的实验,发现廊坊10区和固安1区都是比较新的机器,性能很高,下面是一个新旧机房的对比
旧机器

新机器

四、方案总结和效果展示
下面我们来总结一下我们都做了什么
1、Tomcat启动关闭TLD文件扫描。
2、应用启动Hbase预热异步化,其实这块我们可以应用到其他场景,大家可以检查下自己的项目启动的时候有没有同步预热的场景。
3、Jsf Consumer客户端代理bean异步生成,此处主要核心在于使用一个自定义代理对象提前返回引用,这种思想我们也可以应用到其他需要异步初始化对象的地方去,具体可以参考3.2节的流程图。
4、不同Tomcat版本对于应用启动速度的影响。
5、更换到高性能机房对于启动速度的提升。
通过上述的几项优化过后,应用的启动速度得到了大幅改善,下图展示是一个简略的新旧对比,可以看到应用的启动速度从之前400-500秒到现在的130-150s,提升了近60%多。

(优化后)

(优化前)
五、后续规划
通过上面的一系列优化后,应用的启动得到了明显的提升,但是对于应用中的Spring bean元素初始化还是在单线程中跑的,随着项目的迭代,应用启动速度还是可能随着时间的推移变长,所以后续考虑接入DongBoot组件,使用DongBoot的并行加载、异步初始化能力进一步提高应用启动速度。
