• 应用启动加速-并发初始化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 {
        private static Logger log = LoggerFactory.getLogger(SpringbeanAnalyse.class);
        private Map  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 entry: mapBeantime.entrySet()) {
                    if (entry.getValue() > 1000) {
                       log.warn("slowSpringbean => :",entry.getKey());
                    }
                }
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35

    这样我们就能得到应用中耗时比较高的spring bean。可以看下这些bean的特点,大部分都是在
    afterPropertiesSet,postconstruct,init方法中有初始化逻辑

    eg. AgentConfig中有个构建bean,并调用init方法初始化。

    @Bean(initMethod="init')
    BeanA initBeanA(){
    xxx
    }
    
    • 1
    • 2
    • 3
    • 4

    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");
        }
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33

    输出结果

    constructC
    resourceC
    EnvironmentC
    postConstructC
    afterC
    
    • 1
    • 2
    • 3
    • 4
    • 5

    看下代码
    单个类的加载顺序org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory

    在这里插入图片描述

    单个类的方法顺序是确定了,但是不同类的加载顺序是不确定的。默认是按照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
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    这时候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。

    欢迎提出新的优化方案讨论。

  • 相关阅读:
    display:grid的基本使用、行和列的基本设置、间距、行列宽高
    【UE5 虚幻引擎】新建C++类:类的类型 命名 类的目标模块
    致敬逆行者网页设计作品 大学生抗疫感动专题网页设计作业模板 疫情感动人物静态HTML网页模板下载
    【CFD小工坊】浅水方程的离散及求解方法
    前端:综合例题详细解说(含源代码),导航栏,商品展示页面。
    php-fpm详解
    C语言练习百题之9的次数
    【Java开发笔记】JAVA根据经纬度坐标点集合计算面积
    Python 魔法方法
    threejs 透明贴图,模型透明,白边
  • 原文地址:https://blog.csdn.net/FS1360472174/article/details/125904603