应用启动加速-并发初始化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<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());
                }
            }
        }
    }
}
</string,long></string,></contextrefreshedevent>

这样我们就能得到应用中耗时比较高的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

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

单个类的方法顺序是确定了,但是不同类的加载顺序是不确定的。默认是按照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

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

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

我正在参与掘金技术社区创作者签约计划招募活动

Original: https://www.cnblogs.com/stoneFang/p/16500634.html
Author: stoneFang
Title: 应用启动加速-并发初始化spring bean

原创文章受到原创版权保护。转载请注明出处:https://www.johngo689.com/586678/

转载文章受原作者版权保护。转载请注明原作者出处!

(0)

大家都在看

  • ArrayList和Array数组类型转换

    package com.Mxhlin.arrayList; import java.util.ArrayList; import java.util.Arrays; import …

    Java 2023年6月7日
    0107
  • [spring]spring的bean自动装配机制

    是spring满足bean依赖的一种方式 spring会在上下文中自动寻找,并自动给bean装配属性 spring的装配方式: (1)手动装配 在people类中依赖了cat和do…

    Java 2023年6月6日
    083
  • Spring Ioc源码分析系列–Bean实例化过程(二)

    这篇文章是给上篇填坑的,上篇分析到真正创建Bean的 createBean(beanName, mbd, args)就没有继续深入去分析了,绕得太深,说不清楚。那么这一篇,就续上这…

    Java 2023年6月8日
    071
  • DM5加密的工具类

    代码: import org.springframework.security.crypto.password.PasswordEncoder; import java.math….

    Java 2023年6月13日
    081
  • 给 zsh 自定义命令添加参数自动补全

    有时我会自定义一些 zsh 命令,以便提升某些高频操作的效率。本文记录我给一个自定义命令添加参数自动补全的方法。 场景 我自定义了一个 zsh 命令 gmt,执行 gmt <…

    Java 2023年6月5日
    0103
  • windows container (docker) 容器资料笔记

    业务需求:简化公司私有云,公有云的部署,尝试寻找更好的,更优化的技术方案替换现有的虚拟机部署方案。 技术背景: .net Docker 学习资料 Windows container…

    Java 2023年6月8日
    075
  • ArrayList这篇就够了

    提起ArrayList,相信很多小伙伴都用过,而且还不少用。但在几年之前,我在一场面试中,面试官要求说出ArrayList的扩容机制。很显然,那个时候的我并没有关注这些,从而错过了…

    Java 2023年6月8日
    074
  • 分布式基础- 拜占庭将军问题

    一 背景 拜占庭将军问题是如何通过通讯方式来达成共识得问题,Leslie Lamport 来借助这个问题说明如何在分布式环境下达成共识。 拜占庭将军问题是这样的:拜占庭帝国的军队在…

    Java 2023年5月30日
    091
  • S

    第一部分:基础——增删查改【第一章】做好准备 Getting Started (时长25分钟)【第二章】在单一表格中检索数据 Retrieving Data From a Sing…

    Java 2023年6月7日
    091
  • 1.java的I/O演进道路

    I/O模型:就是用什么样的通道或者说是通信模式和框架进进行数据的传输和接收,很大程度上决定了程序通信的性能,java支持3种网络编程的IO模型 BIO NIO AIO 同步阻塞(传…

    Java 2023年6月5日
    0102
  • 项目准备

    项目导入 资料连接: https://pan.baidu.com/s/1Xp97dflG_i1a8DyTKJWAjg提取码:java 选择项目的pom.xml文件导入 项目启动 第…

    Java 2023年6月6日
    091
  • Java8新特性系列-Optional有什么意义?

    Java 8 中有一个称为 Optional 类的新功能,它应该可以解决 NullPointerExceptions。 显然,这些让开发人员感到恼火的程度比我想象的要多。 很明显,…

    Java 2023年6月7日
    068
  • 阿里云 Docker 设置阿里云镜像加速

    1、登录阿里云 找到页面 &#x5BB9;&#x5668;&#x955C;&#x50CF;&#x670D;&#x52A1; 2、找到…

    Java 2023年6月5日
    074
  • Windows 常用的快捷键

    键盘功能键 键盘功能键:Tab 、Shift 、Ctrl 、Alt 、空格 、 Enter 、 Window 、 ↑ 、 ↓ 、 ← 、 → *键盘快捷键:全选 、 复制 、 粘贴…

    Java 2023年6月8日
    0101
  • [Java编程思想] 第四章 控制执行流程

    4.1 true和false Java不允许将一个数字作为布尔值使用。 4.2 迭代 while、do-while和for控制着循环,有时将其划分为”迭代语句&#822…

    Java 2023年6月5日
    0105
  • 构建系统概念

    什么是构建系统 构建系统的 第一要务 是将系统源代码编译成可执行的文件。 在这基础之上,它允许通过机器自动创建 build,如提交代码到 GitHub 后自动触发构建。 javac…

    Java 2023年6月5日
    082
亲爱的 Coder【最近整理,可免费获取】👉 最新必读书单  | 👏 面试题下载  | 🌎 免费的AI知识星球