SpringBoot应用已经作为Java开发中的首选方式,在云音乐中有着广泛的应用。在云音乐的实践中,为了简化拉取新工程的成本,有一个脚手架作为工程的初始化模版。而随着业务的不断迭代,有一些脚手架的工程启动变地非常慢,严重影响研发效能,并当在需要重启线上集群来进行止血线上问题时,启动的耗时越长,可能造成的资损也就越大。基于此业务痛点,进行了脚手架应用的启动分析与优化。此篇文章主要介绍了这个分析和优化过程,并给出了一些SpringBoot应用的通用分析与优化思路。
云音乐中部分应用启动速度慢,平均在2min以上,部分大型工程启动甚至需要将近10min,如我们的主应用iplay-server为例,下面为其在开发环境本地的启动时间(此时间的统计方式可以查看4.1节)。针对此类耗时,将导致阻塞研发流程,大大降低测试和开发人员的效率,并且当线上环境需要重新发布集群来进行线上问题止血时,启动的耗时越长,可能造成的资损也就越大。基于此痛点,我们进行了脚手架应用的启动分析,并针对于分析得到的结果进行了相应优化。本项目的主要难点在于如何在集成了大量组件以及业务代码的应用中分析并定位到优化点,并且优化应该对于业务代码来说应该尽可能无感知。
脚手架本质是是一个maven的archetype模板应用,整体构建在SpringBoot之上,除了提供了统一的依赖管理,并额外提供了云音乐相关业务中间件的starter,应用生命周期管理以及配置文件解析的能力
脚手架应用启动的整体流程,脚手架应用的启动流程本质上就是SpringBoot应用的启动流程,其中主要流程包括:
public class LifecycleAnalysisSpringApplicationRunListener implements SpringApplicationRunListener {
private long originStartTime;
@Override
public void starting() {
long now = System.currentTimeMillis();
originStartTime = now;
}
@Override
public void finished(ConfigurableApplicationContext context, Throwable exception) {
long now = System.currentTimeMillis();
DefaultTimeAnalysis.getInstance().logCost(getApplicationName() + ": 容器启动完成耗时",now - originStartTime);
}
}
以下分析过程以我们的主应用iplay-server为例
props文件解析流程:由于解析的SDK中已经在解析流程前后进行了时间戳记录,并将耗时时间的日志信息默认输出在进程的标准错误流中,所以无需再进行额外的采集工作
首先,根据脚手架中的组件,对bean进行类别划分,得到如下类别:
public enum BeanClassifierEnums {
/**
* rpc builder类型
* 注:rpc builder类型是rpc key(可理解为集群的一个标识)维度的bean,主要建立与注册中心的网络连接,后续用于构建此集群关联的rpc service
*/
RPC_BUILDER,
/**
* rpc service类型
注:rpc service类型是interface维度的bean,封装了rpc调用相关细节的动态代理
*/
RPC_SERVICE,
/**
* nydus类型 (云音乐MQ)
*/
NYDUS,
/**
* redis类型
*/
REDIS,
/**
* memcached类型
*/
MC,
/**
* SqlManager类型 (云音乐dao框架中负责与DB进行通信的组件)
*/
SQL_MANAGER,
/**
* dao层实现类类型 (业务层dao的实现类,可理解为业务层在SqlManager之上的封装层)
*/
SQL_IMPL,
/**
* 未具体分类的类型,其中主要包括业务代码逻辑中定义的bean以及其他未细化的脚手架组件bean
*/
OTHER
;
}
接下来需要对各个bean进行分类并进行初始化时间的打点,这时需要使用到Spring的扩展点BeanPostProcessor,在bean初始化的前后进行打点处理,采集初始化耗时时间。具体实现方案:根据此扩展点,得到以下数据:上图中分析数据单位为ms,将上面的数据可视化:从中,可以看到除了other之外,耗时占比最高的两个为RpcBuilder和RpcService这两个组件,即后续需要优化的重点目标
从上一阶段的分析结果中确认了优化的重点目标是RpcBuilder和RpcService,接下来需要分析定位到耗时的原因。其中RpcBuilder的主要逻辑是在初始化与注册中心的网络连接,这里就不在赘述。而RpcService的初始化逻辑较复杂,单纯地通过查阅代码并不能定位到耗时逻辑,此时就需要利用profiler工具来进行分析,我们这里使用了Arthas的profiler工具来生成应用启动时的火焰图,协助进行分析。具体操作流程:
-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=10000
启动程序之后,JVM会等待debugger的连接:
![](https://p5.music.126.net/obj/wo3DlcOGw6DClTvDisK1/18978190506/76d0/9631/4ce4/50b8130e0e6b4bbe37525c109e8e0ae0.png)
profiler -e wall start
jdb -attach localhost:10000
连接完成后,执行cont命令,让程序运行起来
cont
profiler stop --file /tmp/server.html
最终我们得到应用启动过程中的火焰图:通常我们应用程序启动自身的堆栈是最高的,所以从中找到高度最高的堆栈,点击进入关于火焰图,针对当前场景来说,堆栈中宽度越宽的栈帧,代表在采样时间中,占用cpu的比例越大。通过火焰图左上角自带的搜索,我们找到需要进行分析的组件初始化流程的堆栈(满足搜索条件的栈帧会被标为紫色):从中,我们就看到了RpcService在初始化流程的堆栈中,宽度最宽即耗时占比最大的栈帧为3次DefaultConfigClient.getConfigValue的http请求读取配置中心的配置值。
通过maven插件,将props文件的解析提前到编译期,提前生成相应的scala文件,然后在运行时进行加载。实现原理:
采用独立的线程池,将建立网络连接的流程异步化,并在应用健康检查之前,等待所有的异步化任务完成并销毁线程池,类似于一个CountDownLatch的逻辑,此处不再赘述。
懒加载带来启动加速效果的同时,于此带来的最明显的副作用就是第一次请求访问时rt会变高,而对于有些rt敏感的应用来说,这个副作用是不可接受的。所以综合考虑之后,最终选择仅在测试环境(包括开发环境)开启懒加载,主要原因有:
如此一来,既能保证框架层的适配性,又可基于懒加载的特性带来研发效能中的提升。
由于目前我们使用的SpringBoot版本为1.x,并未支持spring.main.lazy-initialization配置,所以需要我们自己来实现这个逻辑。这时需要使用到Spring另外的一个扩展点BeanDefinitionRegistryPostProcessor,这个扩展点主要作用于IOC容器收集完bean定义信息BeanDefinition之后的后置处理。通过此扩展点遍历所有的BeanDefinition,过滤出非Configuration的bean(部分配置类懒加载后不生效),通过BeanDefinition的api开启懒加载,核心实现代码:
public class LazyInitPostProcessor implements BeanDefinitionRegistryPostProcessor {
@Override
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException {
// 非测试环境不开启
if (! isTestEnv() && ! isDevEnv()) {
return;
}
for ( String name : registry.getBeanDefinitionNames() ) {
BeanDefinition beanDefinition = registry.getBeanDefinition(name);
String beanClassName = beanDefinition.getBeanClassName();
// 如果是@Configuration标识的bean不能设为lazyinit
if (null != beanClassName) {
try {
Class<?> beanClazz = Class.forName(beanClassName);
Configuration annotation = AnnotationUtils.findAnnotation(beanClazz, Configuration.class);
if (null != annotation) {
continue;
}
} catch (ClassNotFoundException e) {
log.warn("class not found,class -> {}",beanClassName);
}
}
// 设置为懒加载
registry.getBeanDefinition( name ).setLazyInit(true);
}
}
}
public interface BeanDefinition extends AttributeAccessor, BeanMetadataElement {
// 省略其他无关的api
/**
* Set whether this bean should be lazily initialized.
* <p>If {@code false}, the bean will get instantiated on startup by bean
* factories that perform eager initialization of singletons.
*/
void setLazyInit(boolean lazyInit);
}
优化后各分类bean耗时:优化后开发环境本地总启动耗时:整体优化效果从最初的452s,下降到276s,整体启动时间下降了大约40%
本文总体描述了云音乐服务端脚手架应用从分析定位,到优化落地的整体过程。之后根据底层组件的特性,总结了一些可以用于后续的编程实践,并给出了一些SpringBoot应用的通用分析与优化思路。
Spring框架本身也是一大问题,大量使用反射技术进行BeanDefiniton和Bean初始化,也是影响应用启动时间的重要原因。同时,现在有一些Compile Dependency Inject模式的框架很有效的解决这类问题,比如micronaut,根据简单的demo测试结果,应用启动时间大约只需要SpringBoot的1/3。
本文发布自网易云音乐技术团队,文章未经授权禁止任何形式的转载。我们常年招收各类技术岗位,如果你准备换工作,又恰好喜欢云音乐,那就加入我们 grp.music-fe(at)corp.netease.com!