探索|Spring并行初始化加速的思路和实践
阿里妹导读
前言
Java应用启动慢,还有一个罪魁祸首是Spring的bean初始化,我之前写了个异步初始化Spring Bean的starter rhino-boot-turbo,把串行改并行启动速度会快很多。
争议
The upside of parallelizing bean initialization in the Spring container could be significant for a minority of applications using Spring, while the downsides - the inevitable bugs, added complexity and unintended side effects - would affect potentially every application using Spring. Not an attractive outlook, I'm afraid.
有环图是很难处理的,一种朴素的想法就是将其转化为有向无环图(DAG)。图里的环(D - E)来自于循环依赖,我们要做的就是将循环依赖的bean当做是同一组。得到DAG后,就可以先加载下层的bean,然后同一层的bean做并行加载。在issue中也有人提出了这个想法。我个人理解,这个方案的难点在于:
DAG的分析很难,包括如何分析以及分析本身的耗时,特别是循环依赖的嵌套比较深的时候。
兼容目前的生态很难。打个比方,按照Spring目前的设计,有很多开放的扩展点可以修改bean的定义和依赖,比如BeanDefinitionRegistryPostProcessor、BeanFactoryPostProcessor等。如何兼容是个难题。
思路
背景知识
Spring会在主线程串行地对所有Bean进行初始化,在一个Bean的生命周期中,有两类初始化方法会被调用:
Init-method: 包括手动指定的init-method和实现InitializingBean时写的afterPropertiesSet,在构造、属性赋值后由BeanFactory调用;
@PostConstruct标记的方法,在构造、属性赋值后由CommonAnnotationBeanPostProcessor调用;
另外,Spring提供了两个扩展点,后面会用到:
ApplicationContextInitializer:在ApplicationContext做refresh之前会调用,可以对ConfiurableApplicationContext实例对象做处理。
在所有bean初始化完成后,refresh方法的最后一步会publish一个ContextRefreshedEvent,可以注册一个ApplicationListener<ContextRefreshedEvent>来监听该事件。
方案
判断一个bean是不是顶层的bean,也就是没有其他的bean依赖它。
如果是顶层的bean,就单独起一个异步任务做初始化。
如果不是顶层的bean,那么就意味着肯定有其他bean依赖它,将其放到跟依赖它的bean同一个线程中做串行初始化。
org.springframework.beans.factory.support.AbstractBeanFactory#doGetBean
如果Bean A初始化依赖B,就肯定还在初始化过程中调用doGetBean去从容器中拿Bean B的实例,拿的过程中完成Bean B的初始化,如果B又有其他依赖,也是同理类推。
如果doGetBean时,发现栈不为空,那么就表示当前栈顶的bean依赖当前要获取的bean,譬如上图获取bean B时,栈不为空,栈顶元素为bean A,说明A依赖了B,A的初始化必须要等到B完成后才能返回。
细节点
protected void invokeInitMethods(String beanName, Object bean, RootBeanDefinition mbd) throws Throwable {
// 构建bean初始化任务,提交给taskManager执行
BaseBeanInitTask task = new BaseBeanInitTask(beanName, canAsyncInit(beanName, bean, mbd),
BeanInitTypeEnum.INIT_METHOD) {
public void doInit() throws Throwable {
AsyncInitBeanFactory.super.invokeInitMethods(beanName, bean, mbd);
}
};
initTaskManager.init(task);
}
2、对于由@PostConstruct注解的方法,要替换默认的org.springframework.context.annotation.internalCommonAnnotationProcessor,并重写postProcessBeforeInitialization:
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
BaseBeanInitTask task = new BaseBeanInitTask(beanName, canAsyncInit(beanName), BeanInitTypeEnum.POST_CONSTRUCT_METHOD) {
public void doInit() {
AsyncInitAnnotationBeanPostProcessor.super.postProcessBeforeInitialization(bean, beanName);
}
};
initTaskManager.init(task);
return bean;
}
第二是如何替换上面说的BeanFactory和internalCommonAnnotationProcessor?需要注册一个ApplicationContextInitializer,在refresh之前进行替换:
4j
(Ordered.HIGHEST_PRECEDENCE)
public class AsyncInitApplicationContextInitializer
implements ApplicationContextInitializer<ConfigurableApplicationContext> {
public void initialize(ConfigurableApplicationContext context) {
if (context instanceof GenericApplicationContext) {
attach((GenericApplicationContext)context);
}
}
private void attach(GenericApplicationContext context) {
// 省略
InitTaskManager initTaskManager = new InitTaskManager(config);
// 替换BeanFactory
AsyncInitBeanFactory beanFactory = new AsyncInitBeanFactory(context.getBeanFactory(), config, initTaskManager);
replaceBeanFactory(context, beanFactory);
// 注入用于处理@PostConstruct注解初始化的BeanPostProcessor
AsyncInitAnnotationBeanPostProcessor annotationBeanPostProcessor = new AsyncInitAnnotationBeanPostProcessor(
config, initTaskManager);
annotationBeanPostProcessor.setBeanFactory(beanFactory);
beanFactory.registerSingleton(AsyncInitBeanFactoryPostProcessor.NAME,
new AsyncInitBeanFactoryPostProcessor(annotationBeanPostProcessor));
context.addApplicationListener(initTaskManager);
// 省略
}
private void replaceBeanFactory(GenericApplicationContext context, AsyncInitBeanFactory beanFactory) {
Field field = ReflectionUtils.findField(context.getClass(), "beanFactory");
field.setAccessible(true);
ReflectionUtils.setField(field, context, beanFactory);
}
}
protected <T> T doGetBean(String name, Class<T> requiredType, Object[] args, boolean typeCheckOnly)
throws BeansException {
if (initTaskManager.isStarted()) {
return super.doGetBean(name, requiredType, args, typeCheckOnly);
}
LinkedList<String> stack = stackLocal.get();
if (stack == null) {
return super.doGetBean(name, requiredType, args, typeCheckOnly);
}
String peek = stack.peek();
stack.push(name);
T bean = super.doGetBean(name, requiredType, args, typeCheckOnly);
if (peek != null) {
// 栈不为空,表示栈顶的bean依赖当前bean,等待当前bean初始化完成
initTaskManager.waitInitDone(name);
}
stack.pop();
return bean;
}
实现
功能上支持两种加速模式:
自动挡:自动对大部分Bean进行异步初始化加速,适合新手。 手动挡:手动进行配置,比如指定需要和不需要加速的bean,适合老司机。
配置
# 全局启动开关
# 默认值:默认为false,需要手动启用
spring.rhino-boot-turbo.global-enable=true
# [可选] 是否启动自动档加速
# 默认值:true
spring.rhino-boot-turbo.auto-mode-enable=true
# [可选] 异步初始化线程池大小,如果超出线程池大小,初始化任务会放到主线程执行
# 默认值:Runtime.getRuntime().availableProcessors() * 2
spring.rhino-boot-turbo.pool-size=20
# [可选] 最大等待超时(单位:秒),如果超出该值一个bean还没初始化完成,则会报错
# 默认值:60,如果有bean初始化特别久,可以考虑增加超时时间
spring.rhino-boot-turbo.wait-timeout=60
# [可选] 需要被异步加载的bean名称列表,如何配置参考手动挡说明
spring.rhino-boot-turbo.include=beanA,beanB
# [可选] 不想被异步加载的bean名称列表,如何配置参考手动挡说明
spring.rhino-boot-turbo.exclude=beanA,beanB
# [可选] 需要跳过等待的bean名称,如何配置参考手动挡说明
spring.rhino-boot-turbo.skip-wait=beanC
为了保证线上环境的绝对安全,组件只会在本地、项目和日常环境生效,即启动项-Dspring.profiles.active为其中之一:test、project、default。
自动挡
Spring本身的Bean,例如class名称以org.springframework开头; 这类Bean一般耗时很短,也没有异步初始化的必要 一些Spring的生命周期回调Bean,例如ApplicationContextInitializer、BeanFactoryPostProcessor、BeanPostProcessor 对这类Bean进行异步初始化可能会有意想不到的后果,如果确定可以异步,参考手动挡配置。
启动统计
{
"beanCount": 698, # bean数目
"taskCount": 1377, # bean初始化任务数量,包括同步和异步的init-method和@PostConstruct
"totalInitTime": 176658, # bean初始化时长总计,单位ms,包括异步和同步任务
"totalWaitTime": 27282, # 等待时长总计,包括异步和同步任务
"totalSyncInitTime": 14048, # 同步任务初始化时长总计
"totalAsyncInitTime": 162610, # 异步任务初始化时长总计
"totalSyncWaitTime": 14048, # 异步任务等待时长总计
"totalAsyncWaitTime": 13234, # 异步任务等待时长总计
"tasks": [ # 所有初始化任务列表
{
"beanName": "beanA", # bean名称
"type": "POST_CONSTRUCT_METHOD", # 初始化类型,分POST_CONSTRUCT_METHOD和INIT_METHOD
"initTime": 13007, # 初始化用时
"waitTime": 13007 # 等待用时
},
{
"beanName": "beanB (async)", # 标记了(async)的表示会进行异步初始化
"type": "INIT_METHOD",
"initTime": 13125,
"waitTime": 13001
},
....
}]
}
手动挡
适合对代码熟悉,追求更快启动速度的老司机。
不会对特定类型的Bean进行异步初始化,参考自动档说明;
对@PostConstruct初始化方式的bean,目前自动档不会自动进行异步初始化,这一点主要是考虑到在class中反射获取@PostConstruct注解方法在极端情况下可能会比较耗时; Spring本身会对找到的@PostConstruct注解方法进行缓存,但放在private内部类中,无法hook拿到; 如果一个bean在初始化时发现依赖了其他的bean,默认会阻塞等待这个被依赖的bean初始化完成;
判断一个bean是否要异步初始化
如何判断一个bean是否有必要进行异步初始化?可以参考初始化任务列表中的waitTime字段(启动统计的任务列表会根据waitTime等待时长进行排序),该字段表示阻塞等待该bean完成初始化的耗时,包括两部分:
在初始化其他bean时,发现依赖了当前的bean,为了保证安全,需要阻塞等待该bean初始化完成; 例如,beanA依赖beanB,beanB依赖beanC,依赖链beanA -> beanB -> beanC,都进行异步初始化:
在初始化beanA时,发现beanB还未完成初始化,则会等待beanB完成初始化,等待时间会加到beanB的waitTime上;
在初始化beanB时,发现beanC还未完成初始化,则会等待beanC完成初始化,等待时间会加到beanC的waitTime上,同时也会影响beanB的waitTime;
Spring完成了启动,发出了ContextRefreshedEvent事件,但实际上有的bean还未异步初始化完成,也需要阻塞等待其完成,这部分时间也会加到waitTime上;
如果这个bean没有进行异步初始化(在启动统计中没有(async)后缀),考虑将其指定进行异步初始化;
如果这个bean已经进行了异步初始化(在启动统计中带有(async)后缀),考虑将其指定跳过阻塞等待,前提是确认其不会影响其他bean,几个参考判断条件:
依赖这个bean的其他bean不会在初始化时就调用必须在当前bean初始化完成后才能使用的功能,这时候跳过等待当前bean是OK的。一般来说,大部分bean是满足这个条件的,例如HSF的provider/consumer,metaq的的producer/consumer等等;
应用启动后,除非手动触发,不会立刻就调用必须初始化完成才能使用的功能;
spring.rhino-boot-turbo.include=beanA,beanB
spring.rhino-boot-turbo.skip-wait=beanC
spring.rhino-boot-turbo.exclude=beanA,beanB
已知问题
建议不要配置跳过等待dataSource,虽然TDDL初始化耗时很长,但跳过等待有几率会触发Pandora内部的死锁,目前无解。
题外
如无必要,不在bean初始化中做耗时的操作,包括:阻塞IO操作、远程接口调用等等。
依赖加载的方式,将初始化工作后置。
如果确实是要在bean初始化中执行耗时操作,考虑改造其初始化方法为异步。
阿里云开发者社区,千万开发者的选择
阿里云开发者社区,百万精品技术内容、千节免费系统课程、丰富的体验场景、活跃的社群活动、行业专家分享交流,欢迎点击【阅读原文】加入我们。
微信扫码关注该文公众号作者