编辑推荐: |
本文主要介绍了Spring并行初始化加速的思路和实践相关知识。希望能对遇到类似问题的同学有所帮助。
本文来自于微信公众号阿里开发者,由火龙果软件Linda编辑、推荐。 |
|
前言
之前的一篇文章《一些杂想:Java老矣,尚能饭否》里提到了一个rhino-boot-turbo组件:
Java应用启动慢,还有一个罪魁祸首是Spring的bean初始化,我之前写了个异步初始化Spring
Bean的starter rhino-boot-turbo,把串行改并行启动速度会快很多。
最近看到一篇文章《Java应用提速(速度与激情)-其他优化篇》里讲到了Spring异步加载,以及以《Spring
并行加载 Bean 的理想解决方案与不可为》开始的一系列文章,发现实现的思路和方案有很多类似之处,所以记录一下我当时的思考和实践。
我们现在基于Spring开发的Java应用,启动时长受bean初始化影响非常大,之前对经手过的应用做了一个粗略的统计,大部分启动过程花了50%+的时间在做bean的初始化,更有甚者会占到80%以上(主要是启动时去拉取配置)。
多说一句,这个组件是19年的时候搞的,起这个名字是当初受华为技术“GPU Turbo”的启发....
争议
并行初始化是Spring的老大难问题,而且已经成为“unresolved issue”中投票数最多的之一。其实很早(2011年)就有人向官方提过issue:
Parallel bean initialization during startup [SPR-8767]
#13410,时至今日该issue仍是open状态。官方的态度是:首先,对于大部分应用而言,启动时间并不存在大问题;其次,并行初始化虽然可能对一小部分应用的效果是显著的,但是会对每个Spring带来不可避免的bug,增加复杂性,以及难以预料的副作用。
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. |
我对于官方“启动时间并不存在大问题”的说法不敢苟同(在issue里也有很多人同样表示:启动性能对我来说非常重要!)为什么并行初始化会“带来不可避免的bug,增加复杂性,以及难以预料的副作用”?可以试着分析一下原因。
首先,如果bean是独立,也就是说没有和其他bean有依赖关系,那么对其独立做初始化是完全可行的,并不会任何问题。
当然,bean和bean之间的依赖是难以避免的。这种依赖关系可以用一个有向有环图表达,如下图:
有环图是很难处理的,一种朴素的想法就是将其转化为有向无环图(DAG)。图里的环(D - E)来自于循环依赖,我们要做的就是将循环依赖的bean当做是同一组。得到DAG后,就可以先加载下层的bean,然后同一层的bean做并行加载。在issue中也有人提出了这个想法。我个人理解,这个方案的难点在于:
DAG的分析很难,包括如何分析以及分析本身的耗时,特别是循环依赖的嵌套比较深的时候。
兼容目前的生态很难。打个比方,按照Spring目前的设计,有很多开放的扩展点可以修改bean的定义和依赖,比如BeanDefinitionRegistryPostProcessor、BeanFactoryPostProcessor等。如何兼容是个难题。
总结一下,Spring至今拖着没有支持并行初始化,最大的困难在于,需要重写大量底层流程逻辑,在重写的基础上,还要兼容目前大生态下的已经开放出去的扩展点。
既然构建DAG比较困难,是否有其他方式可以做到并行呢?
思路
背景知识
Spring初始化的流程中,关键的代码在org.springframework.context.support. AbstractApplicationContext#refresh。先借用一张网上的图,来看下Spring
Bean启动的生命周期:
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之间的依赖关系的话,只需要改造上面所述的两类初始化方法,将其异步初始化就行了。同时注册一个ApplicationListener<ContextRefreshedEvent>,在监听里等待所有异步初始化的bean完成。
现在考虑bean和bean之间有依赖的情况。既然DAG的构建很困难,我们可以退而求其次,对图中每一个没有依赖边的子图分别做并行初始化。如上面的示例图所示,可以将从顶层的A开始和从F开始的bean做并行初始化,这样是不会有副作用的。思路如下:
判断一个bean是不是顶层的bean,也就是没有其他的bean依赖它。
如果是顶层的bean,就单独起一个异步任务做初始化。
如果不是顶层的bean,那么就意味着肯定有其他bean依赖它,将其放到跟依赖它的bean同一个线程中做串行初始化。
关键是如何判断一个bean是不是顶层bean,或者说是否有bean依赖它呢?Spring是一个依赖注入框架,由容器统一管理所有bean,如果要用到另一个bean,需要从BeanFactory中获取。这时候就会调用到一个关键的方法:
org.springframework.beans.factory.support.AbstractBeanFactory#doGetBean
|
如果Bean A初始化依赖B,就肯定还在初始化过程中调用doGetBean去从容器中拿Bean
B的实例,拿的过程中完成Bean B的初始化,如果B又有其他依赖,也是同理类推。
所以可以考虑使用一个栈,记录每一次doGetBean的情况:当尝试获取一个bean时,把要获取的bean的beanName入栈,当获取返回后,再做出栈。
还是以上面的DAG示例图为例,栈的变化长这样:
如果doGetBean时,发现栈不为空,那么就表示当前栈顶的bean依赖当前要获取的bean,譬如上图获取bean
B时,栈不为空,栈顶元素为bean A,说明A依赖了B,A的初始化必须要等到B完成后才能返回。
细节点
实现这个方案有几个细节点值得说一下。
第一是如何替换默认的初始化逻辑?需要改造上面说的两类初始化方法的入口。
1、对于BeanFactory#invokeInitMethods方式调用的,需要替换默认的BeanFactory并重写这个方法:
@Override
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)
{
@Override
public void doInit() throws
Throwable {
AsyncInitBeanFactory.super.invokeInitMethods(beanName,
bean, mbd);
}
};
initTaskManager.init(task);
}
|
2、对于由@PostConstruct注解的方法,要替换默认的org.springframework.context.annotation.internal CommonAnnotationProcessor,并重写postProcessBeforeInitialization:
@Override
public Object postProcessBeforeInitialization(Object
bean, String beanName) throws BeansException
{
BaseBeanInitTask task = new
BaseBeanInitTask(beanName, canAsyncInit(beanName),
BeanInitTypeEnum.POST_CONSTRUCT_METHOD) {
@Override
public void doInit() {
AsyncInitAnnotationBeanPostProcessor.super.postProcessBeforeInitialization(bean,
beanName);
}
};
initTaskManager.init(task);
return bean;
}
|
第二是如何替换上面说的BeanFactory和internalCommonAnnotationProcessor?需要注册一个ApplicationContextInitializer,在refresh之前进行替换:
@Slf4j
@Order(Ordered.HIGHEST_PRECEDENCE)
public class AsyncInitApplicationContextInitializer
implements ApplicationContextInitializer<ConfigurableApplicationContext>
{
@Override
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);
}
}
|
最后是改造BeanFactory#doGetBean了:
@Override
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;
}
|
实现
整体代码800+行,实现一个简单的turbo加速器,测了几个应用,启动时间提升在一倍以上。(完整代码无法对外展示)
功能上支持两种加速模式:
自动挡:自动对大部分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.rhino-boot-turbo.auto-mode-enable=true),会自动对大部分存在第一类初始化方法(init-method)的Bean进行异步初始化加速。
一些例外:
Spring本身的Bean,例如class名称以org.springframework开头;
这类Bean一般耗时很短,也没有异步初始化的必要
一些Spring的生命周期回调Bean,例如ApplicationContextInitializer、 BeanFactoryPostProcessor、BeanPostProcessor
对这类Bean进行异步初始化可能会有意想不到的后果,如果确定可以异步,参考手动挡配置。
启动统计
在启动完成后,可以访问ip:managementport/bean-init页面(例如:localhost:7002/bean-init)来查看启动统计。
结果样例:
{
"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可能不能安全地进行异步初始化,但自动挡无法自动识别。举个例子:beanA依赖beanB,但是beanA是通过手动set的方式引用了beanB(而不是通过@Autowired、构造器注入等正常方式),在异步初始化beanA时不会等待beanB完成,导致运行异常。
针对以上各种情况,可以进行手动配置。
判断一个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上;
因此waitTime反映了由于这个bean初始化阻塞的时间,在优化时区分两类情况:
如果这个bean没有进行异步初始化(在启动统计中没有(async)后缀),考虑将其指定进行异步初始化;
如果这个bean已经进行了异步初始化(在启动统计中带有(async)后缀),考虑将其指定跳过阻塞等待,前提是确认其不会影响其他bean,几个参考判断条件:
依赖这个bean的其他bean不会在初始化时就调用必须在当前bean初始化完成后才能使用的功能,这时候跳过等待当前bean是OK的。一般来说,大部分bean是满足这个条件的,例如HSF的provider/consumer,metaq的的producer/consumer等等;
应用启动后,除非手动触发,不会立刻就调用必须初始化完成才能使用的功能;
指定异步初始化的bean
通过配置项spring.rhino-boot-turbo.include可以手动指定要异步初始化的bean。
spring.rhino-boot-turbo.include=beanA,beanB
|
指定跳过等待
通过配置项spring.rhino-boot-turbo.skip-wait可以手动指定不进行阻塞等待初始化完成的bean。
spring.rhino-boot-turbo.skip-wait=beanC
|
指定不要异步初始化的bean
通过配置项spring.rhino-boot-turbo.exclude可以手动排除自动档会进行异步初始化的bean。
spring.rhino-boot-turbo.exclude=beanA,beanB
|
已知问题
建议不要配置跳过等待dataSource,虽然TDDL初始化耗时很长,但跳过等待有几率会触发Pandora内部的死锁,目前无解。
题外
其实Spring官方也“辩解”了一下:大多数时候应用启动慢的元凶就是少数的一些bean,可以重点优化这些bean的初始化时长。具体如何优化?官方没有说,这里列一些可行的方法:
如无必要,不在bean初始化中做耗时的操作,包括:阻塞IO操作、远程接口调用等等。
依赖加载的方式,将初始化工作后置。
如果确实是要在bean初始化中执行耗时操作,考虑改造其初始化方法为异步。
|