关于后端:云音乐服务端应用启动时间下降40实践分享

45次阅读

共计 6763 个字符,预计需要花费 17 分钟才能阅读完成。

本文作者:喵内

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!

正文完
 0