本文作者:喵内

0. 前言

SpringBoot利用曾经作为Java开发中的首选形式,在云音乐中有着宽泛的利用。在云音乐的实际中,为了简化拉取新工程的老本,有一个脚手架作为工程的初始化模版。而随着业务的一直迭代,有一些脚手架的工程启动变地十分慢,重大影响研发效力,并当在须要重启线上集群来进行止血线上问题时,启动的耗时越长,可能造成的资损也就越大。基于此业务痛点,进行了脚手架利用的启动剖析与优化。此篇文章次要介绍了这个剖析和优化过程,并给出了一些SpringBoot利用的通用剖析与优化思路。

1. 我的项目背景

云音乐中局部利用启动速度慢,均匀在2min以上,局部大型工程启动甚至须要将近10min,如咱们的主利用iplay-server为例,上面为其在开发环境本地的启动工夫(此工夫的统计形式能够查看4.1节)。针对此类耗时,将导致阻塞研发流程,大大降低测试和开发人员的效率,并且当线上环境须要从新公布集群来进行线上问题止血时,启动的耗时越长,可能造成的资损也就越大。基于此痛点,咱们进行了脚手架利用的启动剖析,并针对于剖析失去的后果进行了相应优化。本我的项目的次要难点在于如何在集成了大量组件以及业务代码的利用中剖析并定位到优化点,并且优化应该对于业务代码来说应该尽可能无感知。

2. 脚手架在SpringBoot之上提供的能力

脚手架实质是是一个maven的archetype模板利用,整体构建在SpringBoot之上,除了提供了对立的依赖治理,并额定提供了云音乐相干业务中间件的starter,利用生命周期治理以及配置文件解析的能力

3. 脚手架利用启动整体流程

脚手架利用启动的整体流程,脚手架利用的启动流程实质上就是SpringBoot利用的启动流程,其中次要流程包含:

  • 创立并初始化Environment:创立并初始化应用环境Environment对象
  • props文件解析:依据应用环境,占位符等配置信息,通过Scala语言解析props文件(能够了解为properties文件的变种)中kv值put到Spring的Enviroiment中
  • 创立并初始化ApplicationContext:创立并初始化利用上下文对象
  • 加载BeanDefinition:加载应用程序中的Bean定义信息
  • 刷新ApplicationContext:IOC容器外围启动流程,包含bean的创立,初始化,依赖注入等
  • 下发Context刷新实现事件:下发ApplicationContext刷新实现事件
  • 利用健康检查:对利用中各种组件(如db,redis等)进行流量测试,校验是否失常work
  • 下发衰弱检测实现事件:利用健康检查实现事件下发
  • 组件online:组件进行online操作,实现服务注册,如api注册到网关zk,rpc服务注册到zk等

    4. 启动耗时分析阶段

    4.1 阶段指标与验证工具

  • 指标:通过日志打点形式,剖析启动过程中,脚手架中各流程的启动耗时并确认优化指标。
  • 验证工具:如上文所述,脚手架利用的启动流程实质上就是SpringBoot利用的启动流程,所以咱们实质上就是须要剖析SpringBoot利用的各个启动阶段耗时,所以能够通过SpringBoot提供的扩大点SpringApplicationRunListener进行启动过程中各阶段的耗时统计,此扩大点会在SpringBoot利用启动过程中一些重要阶段进行回调,具体见下图:

    如须要统计利用启动的总耗时,只须要starting和finished回调中进行日志打点即可,外围实现代码:

    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);    }}

    4.2 剖析过程

    以下剖析过程以咱们的主利用iplay-server为例

    4.2.1 运行时Scala解析props文件解析耗时

    props文件解析流程:

    因为解析的SDK中曾经在解析流程前后进行了工夫戳记录,并将耗时工夫的日志信息默认输入在过程的规范谬误流中,所以无需再进行额定的采集工作

    4.2.2 各类bean初始化耗时占比

    首先,依据脚手架中的组件,对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这两个组件,即后续须要优化的重点指标

    4.2.3 如何在简单的代码中疾速定位耗时逻辑

    从上一阶段的剖析后果中确认了优化的重点指标是RpcBuilder和RpcService,接下来须要剖析定位到耗时的起因。其中RpcBuilder的次要逻辑是在初始化与注册核心的网络连接,这里就不在赘述。而RpcService的初始化逻辑较简单,单纯地通过查阅代码并不能定位到耗时逻辑,此时就须要利用profiler工具来进行剖析,咱们这里应用了Arthas的profiler工具来生成利用启动时的火焰图,帮助进行剖析。具体操作流程:

  • 在利用启动的JVM参数中,增加debugger参数,留神其中的suspend参数须要设为y,示意在debugger连贯之前,程序会进行阻塞期待。

    -agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=10000

    启动程序之后,JVM会期待debugger的连贯:

  • 启动Arthas并连贯对应的JVM过程,留神因为咱们的过程在阻塞期待debugger,还未开始运行,程序的main class信息是获取不到的,所以咱们须要连贯的是无main class信息的JVM过程

    连贯实现之后进入Arthas的交互界面:
  • 启动profiler,因为咱们须要剖析启动过程的耗时阶段,所以咱们须要指定跟踪采集的事件为wall

    profiler -e wall start
  • 利用jdb(jdk自带的debugger工具)连贯对应的JVM过程,将此利用run起来

    jdb -attach localhost:10000

    连贯实现后,执行cont命令,让程序运行起来

    cont
  • 利用启动实现后,进行profiler的stop,并通过file参数指定生成火焰图的门路

    profiler stop --file /tmp/server.html

    最终咱们失去利用启动过程中的火焰图:

    通常咱们应用程序启动本身的堆栈是最高的,所以从中找到高度最高的堆栈,点击进入

    对于火焰图,针对以后场景来说,堆栈中宽度越宽的栈帧,代表在采样工夫中,占用cpu的比例越大。通过火焰图左上角自带的搜寻,咱们找到须要进行剖析的组件初始化流程的堆栈(满足搜寻条件的栈帧会被标为紫色):


    从中,咱们就看到了RpcService在初始化流程的堆栈中,宽度最宽即耗时占比最大的栈帧为3次DefaultConfigClient.getConfigValue的http申请读取配置核心的配置值。

    4.2.4 再来看看没有具体分类的other

  • 因为举例利用中,other分类的bean数量达2900+,故上图中只截取了局部剖析数据
  • 未进行分类的bean中,大部分为业务代码定义的bean,此处在不变更业务代码的前提下,这部分bean对框架层来说应该是无感知的。而因为这部分bean次要是业务逻辑相干,咱们能够应用Spring的懒加载实现bean的按需加载,而不是启动过程中全量加载。

    4.3 后果剖析

  • 从4.2.1的剖析数据来看,每次利用启动时,都须要走一遍Scala解析props配置文件的流程,通常状况下,咱们利用公布时都是按批次进行灰度公布,将会导致解析的流程走屡次,比方分了3批次,那咱们整个公布流程的耗时中就蕴含了3次解析props文件的耗时。针对此,咱们能够通过maven插件,将解析流程提前到构建编译阶段,这样整个公布流程中只须要解析一次即可
  • 从4.2.2和4.2.3的剖析后果来看,RpcBuilder和RpcService的耗时逻辑次要集中在IO操作,所以能够采纳异步初始化的形式来解决。将IO相干的操作独立到独自的线程中去实现。
  • 从4.2.4的剖析后果中,针对大量的业务逻辑bean时,采纳开启懒加载的形式。

    5. 优化落地阶段

    5.1 props文件解析的maven插件

    通过maven插件,将props文件的解析提前到编译期,提前生成相应的scala文件,而后在运行时进行加载。

    实现原理:

    5.2 RpcBuilder和RpcService

    采纳独立的线程池,将建设网络连接的流程异步化,并在利用健康检查之前,期待所有的异步化工作实现并销毁线程池,相似于一个CountDownLatch的逻辑,此处不再赘述。

    5.3 懒加载

    5.3.1 懒加载落地中的思考

    懒加载带来启动减速成果的同时,于此带来的最显著的副作用就是第一次申请拜访时rt会变高,而对于有些rt敏感的利用来说,这个副作用是不可承受的。所以综合思考之后,最终抉择仅在测试环境(包含开发环境)开启懒加载,次要起因有:

  • 保障框架层的适用性,尽量实用于所有类型的利用
  • 测试环境更适配懒加载的个性,因为绝大多数状况下测试环境只是测利用中的局部性能,而非全量性能,在未开启懒加载之前,须要期待与待测试性能无关的其余bean初始化,这部分工夫是毫无意义的。
  • 测试环境的重启公布频率远高于线上,懒加载带来的收益更显著。

如此一来,既能保障框架层的适配性,又可基于懒加载的个性带来研发效力中的晋升。

5.3.2 懒加载的实现落地

因为目前咱们应用的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);}

5.4 优化后果

优化后各分类bean耗时:


优化后开发环境本地总启动耗时:

整体优化成果从最后的452s,降落到276s,整体启动工夫降落了大概40%

6. 总结

本文总体形容了云音乐服务端脚手架利用从剖析定位,到优化落地的整体过程。之后依据底层组件的个性,总结了一些能够用于后续的编程实际,并给出了一些SpringBoot利用的通用剖析与优化思路。

7. 思考扩大

Spring框架自身也是一大问题,大量应用反射技术进行BeanDefiniton和Bean初始化,也是影响利用启动工夫的重要起因。同时,当初有一些Compile Dependency Inject模式的框架很无效的解决这类问题,比方micronaut,依据简略的demo测试后果,利用启动工夫大概只须要SpringBoot的1/3。

8. 参考资料

  1. Arthas Profiler工具:https://arthas.aliyun.com/doc/profiler.html
  2. Arthas Profiler命令参数:https://www.dounaite.com/article/6264549c7b5653d739b0bb74.html
  3. SpringBoot懒加载:https://spring.io/blog/2019/03/14/lazy-initialization-in-spri...
本文公布自网易云音乐技术团队,文章未经受权禁止任何模式的转载。咱们长年招收各类技术岗位,如果你筹备换工作,又恰好喜爱云音乐,那就退出咱们 grp.music-fe(at)corp.netease.com!