应用启动加速-并发初始化spring bean

打印 上一主题 下一主题

主题 978|帖子 978|积分 2934

背景

随着需求的不断迭代,服务承载的内容越来越多,依赖越来越多,导致服务启动慢,从最开始的2min以内增长到5min,导致服务发布很慢,严重影响开发效率,以及线上问题的修复速度。所以需要进行启动加速。
方案

应用启动加速的优化方案通常有

  • 编译阶段的优化,比如无用依赖的优化
  • dockerfile的优化
  • 依赖的中间件优化,中间件有大量的网络连接建立,有很大的优化手段
  • 富客户端的优化
  • spring bean加载的优化
    spring容器加载bean是通过单线程加载的,可以通过并发来提高加载速度。
鉴于1的优化难度比较大,2、3、4则一般与各个公司里的基础组件有很大相关性,所以本篇只介绍spring bean加载的优化。
spring bean 加载耗时分析

分析bean加载耗时

首先需要分析加载耗时高的bean。spring bean 耗时 = timestampOfAfterInit - timestampOfBeforeInit.可以通过扩展BeanPostProcessor来实现,代码如下
  1. @Component
  2. public class SpringbeanAnalyse implements BeanPostProcessor,
  3.         ApplicationListener<ContextRefreshedEvent> {
  4.     private static Logger log = LoggerFactory.getLogger(SpringbeanAnalyse.class);
  5.     private Map<String, Long>  mapBeantime  = new HashMap<>();
  6.     private static volatile AtomicBoolean started = new AtomicBoolean(false);
  7.     @Autowired
  8.     public Object postProcessBeforeInitialization(Object bean, String beanName) throws
  9.             BeansException {
  10.         mapBeantime.put(beanName, System.currentTimeMillis());
  11.         return bean;
  12.     }
  13.     @Autowired
  14.     public Object postProcessAfterInitialization(Object bean, String beanName) throws
  15.             BeansException {
  16.         Long begin = mapBeantime.get(beanName);
  17.         if (begin != null) {
  18.             mapBeantime.put(beanName, System.currentTimeMillis() - begin);
  19.         }
  20.         return bean;
  21.     }
  22.     @Override
  23.     public void onApplicationEvent(final ContextRefreshedEvent event) {
  24.         if (started.compareAndSet(false, true)) {
  25.             for (Map.Entry<String,Long> entry: mapBeantime.entrySet()) {
  26.                 if (entry.getValue() > 1000) {
  27.                    log.warn("slowSpringbean => :",entry.getKey());
  28.                 }
  29.             }
  30.         }
  31.     }
  32. }
复制代码
这样我们就能得到应用中耗时比较高的spring bean。可以看下这些bean的特点,大部分都是在
afterPropertiesSet,postconstruct,init方法中有初始化逻辑
eg. AgentConfig中有个构建bean,并调用init方法初始化。
  1. @Bean(initMethod="init')
  2. BeanA initBeanA(){
  3. xxx
  4. }
复制代码
bean的生命周期

sampleCode
  1. @Component
  2. @Configuration
  3. public class BeanC implements EnvironmentAware, InitializingBean{
  4.     public BeanC() {
  5.         System.out.println("constructC");
  6.     }
  7.     @Override
  8.     public void afterPropertiesSet() throws Exception {
  9.         System.out.println("afterC"  + Thread.currentThread().getName() + Thread.currentThread().getId());
  10.     }
  11.     @Resource
  12.     public void resource(Environment environment) {
  13.         System.out.println("resourceC");
  14.     }
  15.     @PostConstruct
  16.     public void postConstruct() {
  17.         System.out.println("postConstructC" +Thread.currentThread().getName() + Thread.currentThread().getId());
  18.     }
  19.     @Override
  20.     public void setEnvironment(Environment environment) {
  21.         System.out.println("EnvironmentC");
  22.     }
  23.     public void init(){
  24.         System.out.println("InitC");
  25.     }
  26. }
复制代码
输出结果
  1. constructC
  2. resourceC
  3. EnvironmentC
  4. postConstructC
  5. afterC
复制代码
看下代码
单个类的加载顺序org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory

单个类的方法顺序是确定了,但是不同类的加载顺序是不确定的。默认是按照module,package的ascii顺序来加载。但这个类的初始化顺序不是固定的,在不同机器上表现形式不一样。类似于
Jvm加载jar包的顺序
控制不同类的加载顺序

可以通过以下方法来控制bean加载顺序

  • 依赖 @DependOn
  • bean依赖 构造器,或者@Autowired
  • @Order 指定顺序
对BeanB添加了BeanC的依赖,输出结果为
  1. constructC
  2. resourceC
  3. constructB
  4. resourceB
  5. EnvironmentB
  6. postConstructB
  7. afterB
  8. EnvironmentC
  9. postConstructC
  10. 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。
欢迎提出新的优化方案讨论。
我正在参与掘金技术社区创作者签约计划招募活动

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

笑看天下无敌手

金牌会员
这个人很懒什么都没写!

标签云

快速回复 返回顶部 返回列表