应用启动加速-并发初始化spring bean
背景
随着需求的不断迭代,服务承载的内容越来越多,依赖越来越多,导致服务启动慢,从最开始的2min以内增长到5min,导致服务发布很慢,严重影响开发效率,以及线上问题的修复速度。所以需要进行启动加速。
方案
应用启动加速的优化方案通常有
- 编译阶段的优化,比如无用依赖的优化
- dockerfile的优化
- 依赖的中间件优化,中间件有大量的网络连接建立,有很大的优化手段
- 富客户端的优化
- 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());
}
}
}
}
}
这样我们就能得到应用中耗时比较高的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
单个类的方法顺序是确定了,但是不同类的加载顺序是不确定的。默认是按照module,package的ascii顺序来加载。但这个类的初始化顺序不是固定的,在不同机器上表现形式不一样。类似于
Jvm加载jar包的顺序
控制不同类的加载顺序
可以通过以下方法来控制bean加载顺序
- 依赖 @DependOn
- bean依赖 构造器,或者@Autowired
- @Order 指定顺序
对BeanB添加了BeanC的依赖,输出结果为
constructC
resourceC
constructB
resourceB
EnvironmentB
postConstructB
afterB
EnvironmentC
postConstructC
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。
欢迎提出新的优化方案讨论。
我正在参与掘金技术社区创作者签约计划招募活动
应用启动加速-并发初始化spring bean的更多相关文章
- 如何在静态方法或非Spring Bean中注入Spring Bean
在项目中有时需要根据需要在自己new一个对象,或者在某些util方法或属性中获取Spring Bean对象,从而完成某些工作,但是由于自己new的对象和util方法并不是受Spring所 ...
- Spring Bean 生命周期之destroy——终极信仰
上一篇文章 Spring Bean 生命周期之我从哪里来 说明了我是谁? 和 我从哪里来? 的两大哲学问题,今天我们要讨论一下终极哲学我要到哪里去? 初始化 Spring Bean 有三种方式: @P ...
- 让Spring Boot项目启动时可以根据自定义配置决定初始化哪些Bean
让Spring Boot项目启动时可以根据自定义配置决定初始化哪些Bean 问题描述 实现思路 思路一 [不符合要求] 思路二[满足要求] 思路三[未试验] 问题描述 目前我工作环境下,后端主要的框架 ...
- spring bean的初始化以及销毁
spring bean初始化或销毁时执行某些方法,有很多使用场景.比如初始化时,启动bean中的线程池.销毁时释放资源,个人比较喜欢实现InitializingBean和 DisposableBean ...
- Spring点滴九:Spring bean的延迟初始化
Spring bean延迟初始化: 官网API: By default, ApplicationContext implementations eagerly create and configure ...
- 🙈羞,Spring Bean 初始化/销毁竟然有这么多姿势
文章来源:http://1t.click/bfHN 一.前言 日常开发过程有时需要在应用启动之后加载某些资源,或者在应用关闭之前释放资源.Spring 框架提供相关功能,围绕 Spring Bean ...
- SpringBoot IoC启动流程、初始化过程及Bean生命周期各个阶段的作用
目录 SpringBoot IoC启动流程.初始化过程及Bean生命周期各个阶段的作用 简述 首先明确IoC容器是啥 准备-SpringApplication的实例化 启动-SpringApplica ...
- Spring Boot 启动源码解析结合Spring Bean生命周期分析
转载请注明出处: 1.SpringBoot 源码执行流程图 2. 创建SpringApplication 应用,在构造函数中推断启动应用类型,并进行spring boot自动装配 public sta ...
- spring bean初始化和销毁
spring bean的创建与消亡由spring容器进行管理,除了使用<bean><property/></bean>进行简单的属性配置之外,spring支持更人性 ...
随机推荐
- Java安全之SnakeYaml反序列化分析
Java安全之SnakeYaml反序列化分析 目录 Java安全之SnakeYaml反序列化分析 写在前面 SnakeYaml简介 SnakeYaml序列化与反序列化 常用方法 序列化 反序列化 Sn ...
- 老生常谈系列之Aop--Spring Aop源码解析(一)
老生常谈系列之Aop--Spring Aop源码解析(一) 前言 上一篇文章老生常谈系列之Aop--Spring Aop原理浅析大概阐述了动态代理的相关知识,并且最后的图给了一个Spring Aop实 ...
- 【拖拽可视化大屏】全流程讲解用python的pyecharts库实现拖拽可视化大屏的背后原理,简单粗暴!
"整篇文章较长,干货很多!建议收藏后,分章节阅读." 一.设计方案 整体设计方案思维导图: 整篇文章,也将按照这个结构来讲解. 若有重点关注部分,可点击章节目录直接跳转! 二.项目 ...
- 简单的 useState 实现
简单的 useState 实现 本文写于 2020 年 10 月 21 日 以下是一段非常简单的 React 代码: const App = () => { const [n, setN] = ...
- zabbix 线路质量监控自定义python模块,集成ICMP/TCP/UDP探测,批量监控线路质量自定义阈值联动mtr保存线路故障日志并发送至noc邮箱
互联网故障一般表现为丢包和时延增大,持续性故障不难排查,难的是间歇性或凌晨故障,后者往往来不及等我们测试就已经恢复正常,得不到异常时的mtr无法判断故障点在哪里 故此有了根据丢包率和时延变换联动mtr ...
- 【mq】从零开始实现 mq-11-消费者消息回执添加分组信息 pull message ack groupName
前景回顾 [mq]从零开始实现 mq-01-生产者.消费者启动 [mq]从零开始实现 mq-02-如何实现生产者调用消费者? [mq]从零开始实现 mq-03-引入 broker 中间人 [mq]从零 ...
- Redis设计与实现2.2:数据持久化
数据持久化 这是<Redis设计与实现>系列的文章,系列导航:Redis设计与实现笔记 RDB持久化 RDB 持久化功能所生成的 RDB 文件是一个经过压缩的二进制文件,通过该文件可以还原 ...
- [C++STL] set 容器的入门
set 容器的入门 unordered_set:另外头文件,乱序排放,使用哈希表(便于查找) multiset:可以重复存在的集合.用count()读取个数 创建set的几种方式 常规 set< ...
- 协议层安全相关《http请求走私与CTF利用》
0x00 前言 最近刷题的时候多次遇到HTTP请求走私相关的题目,但之前都没怎么接触到相关的知识点,只是在GKCTF2021--hackme中使用到了 CVE-2019-20372(Nginx< ...
- mongoDB 命令大全
每日一句 There should be a better way to start a day than waking up every morning. 应该有更好的方式开始新一天, 而不是千篇一 ...