背景
线上项目checkHealth(超时时间5min)超时,然后显示服务启动失败。
其实项目启动时间已经做过几次时间调整了,从1-3-5min。
启动时间长问题会影响到:(1) 故障发生时如果需要应用回滚、扩容或者重启,启动时间长会拖慢故障恢复时间;(2) 上线时间长也是一个很大的痛点,当服务器数量比较多时,服务启动时间的影响会更明显。
设计方案
项目的耗时由那些组成:容器耗时 + 初始化Bean + 初始化任务
其中比较好把我的是初始化任务和初始化Bean 这部分,我们本文主要聊一聊这一点
方案是:
- 对于耗时长的任务进行手动优化,是否存在不合理的启动点
- 比如初始化Cache中查询MySQL,是否耗时过长,查询步长可以增大等
- Bean是否必须启动时进行初始化动作,是否可以懒加载,懒处理
- 并行初始化
- 如果单任务耗时问题已解决,就可以针对必须长耗时任务进行Bean并行初始化
- 耗时预计 从原本串行 Sum(taskTimeOut) => Max(taskTimeOut)
Spring Bean 启动耗时查询
耗时长任务统计
解决的是 @PostConstruct、InitializingBean(afterPropertiesSet)、initMethod方法
有目标或者指标才知道我们要如何优化,怎么优化
在Spring Bean中有注入点:BeanPostProcessor接口,实现postProcessorBeforeInitialization、postProcessAfterInitialization方法,对bean的总耗时进行打印,找到其中耗时较长的bean,进行针对性优化
@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() > 100) {
log.warn("slowSpringbean => :",entry.getKey());
}
}
}
}
}
耗时长任务并行初始化
为什么不能自动:全局自动配置完成所有的bean需要解决相互依赖的问题,这个问题不是很好解决,原因在于循环依赖和Spring提供了大量的扩展能力,这样导致我们无法得到一个spring bean的全局依赖图。因此无法通过自动配置的手段来解决spring bean单线程加载的问题。
手动如何实现:
定义全局线程池,重写bean的initmethod,将任务提交到线程池中处理,最终判断线程池任务全部结束,则并行初始化完成。
异步初始化线程池一般设定多大:
当前机器CPu核数+1(线程在服务启动后自动销毁)