服务端架构治理|闲鱼如何有效提高应用的编译和启动速度

简介: 闲鱼服务端治理系列文章

作者:闲鱼技术——泊垚

背景

应用的发布是一件非常耗时的事情,尤其是当应用迭代了比较长的时间之后,一次预发的部署就可能需要花费十几分钟,其中服务启动一次,就可能花费五六分钟。如此漫长的发布可能带来两方面的问题:

  1. 开发过程中,在使用测试环境的时候发布验证的时候,我们往往希望快速迭代,快速验证,但是每次改完一个feature发布验证都要经过十几分钟的话,效率是非常低下的。
  2. 在线上发布过程中,如果遇到机器数量非常多的应用,那么一次发布,一批一批的部署下来,耗费的时间非常长,很容易超出发布窗口,带来线上风险。

针对应用发布耗时长的问题,笔者结合对手头应用idle-local的耗时分析,参考和尝试了多数的方案之后,制定了一套实施方案,能够有效提高应用的编译和启动速度。同时也着手优化和沉淀了一个启动加速工具(在其他同学的项目上迭代得到),能够针对整个过程中最耗时的启动阶段进行加速,有不错的效果。

构建部署耗时分析

idle-local项目耗时统计

image.png
注:

  1. mtop是我们项目的api层
  2. hsf是阿里的RPC框架
  3. pandora是一个的轻量级的隔离容器,用来隔离Webapp和中间件的依赖

重点优化分析

根据统计得到的应用编译部署耗时情况,以及理论上的加速空间,我们制定了以下几项优化的重点:

  1. 部署过程中,启动应用是最主要的耗时项,也是最容易随着应用迭代膨胀的部分,其中启动应用的主要耗时是bean的初始化。
  2. 构建过程中,代码编译是主要的耗时项,理论上存在较大的优化空间。
  3. 镜像中最后一层打包内容的大小会影响镜像push和pull的耗时,如果能够将变动范围分层优化,会有较大的收益。
  4. 停止应用的过程中,为了处理RPC服务HSF优雅下线和应用优雅关闭,花了比较多的时间,在非生产环境可以省略

构建部署速度治理方案

应用启动加速-Spring Bean异步初始化的原理与落地

加速效果:☆☆☆☆
配置简易:☆☆
推荐指数:☆☆☆☆
我们通过分析spring的初始化过程会发现,spring对于bean的创建,不论是通过遍历还是通过依赖触发,都是通过同步的方式对bean进行初始化的。
这就导致了当一个bean的初始化过程很久的时候,会严重阻塞后续bean的初始化,哪怕这两个bean之间完全没有相互依赖。
如果能够将bean的初始化过程放到异步线程中,则会大大提升bean的创建效率。
image.png
如图,在完成bean的实例化后,异步进行初始化,异步初始化实现的关键是保证bean在被使用之前初始化完成。
本节我们将从理论出发,逐步分析springBean的异步初始化办法:

孤立的Bean

我们将不被其他Bean依赖的Bean,定义为孤立的Bean。如图,beanA和beanB1两个bean,相互不依赖也不被其他bean依赖。
image.png
理论上,容器中存在孤立的Bean,这些Bean由于不被其他Bean依赖,在容器初始化过程中,这些Bean不会被其他bean使用,是可以自己异步进行初始化的,只需要容器初始化完成时,保障这些Bean已经被初始化完成即可。
然而完全孤立的Bean其实比较少,同时,找出孤立的Bean,需要遍历整个依赖树。我们需要一种覆盖范围更广,更容易定义的方式进行异步化。

暂时孤立的Bean

我们有这样的认知:

  1. 孤立的Bean一定是被spring通过遍历的方式创建的
  2. 被spring通过遍历的方式创建的bean不一定是孤立的Bean,他可能被后来的bean依赖并注入
  3. 被spring通过遍历的方式创建的bean有较大概率是孤立的Bean
  4. 被spring通过遍历的方式创建的bean的初始化距离它被其他的Bean依赖并注入,有一些时间差

基于这样的认知,我们可以按照以下方案进行异步化:
image.png
如图所示,被spring通过遍历的方式创建的bean我们可以暂时认为他是孤立的Bean,当我们发现他不是的时候(被其他Bean调用了getBean),阻塞并等待他的初始化,那么期间的异步初始化过程,也能为我们节省时间;如果他始终没有被依赖,那么说明他就是孤立的Bean。
至于在实际实现中,如何判断一个bean是被spring遍历到还是被其他bean依赖导致的创建,有一个可行的方法是:定义一个全局的标记,用来记录当前spring遍历到的bean,当且仅当这个标记是null的时候,表示当前正在获取的bean是被spring遍历到的,然后立即将当前bean写入标记,并在bean返回前将标记清除,我们可以通过增强beanFactory的getBean方法实现这部分逻辑。
这个方案的优点是能够自动识别并尝试异步初始化,无需复杂的配置即可实现效果不错的加速。

暂时不被使用的Bean

上述的两种异步化中,我们通过bean是否被“依赖”来决定是否异步初始化。但还有很多Bean,不是被spring通过遍历的方式创建的,这就导致我们上面的方案覆盖不到一些耗时的bean。
但事实上,我们Bean的初始化过程,只要在Bean被“使用”前完成即可,被“依赖”这个条件,是过于严格的。如果我们将Bean对象的方法访问判定为被“使用”的入口,那么我们可以通过对Bean进行代理,拦截Bean的方法访问,在其被“使用”之前,等待他初始化完成。
image.png
这样我们可以指定任意的Bean进行异步初始化。但有一种情况是不安全的:这个Bean定义了公共的变量,如果在初始化之前被访问,是不能被代理拦截的。我们在实现bean的时候,要注意不要暴露内部变量,这是一个很重要的习惯。

FactoryBean的处理

然而上述的并行化方式,对于有一种类型的bean是不适用的,那就是FactoryBean。不论有没有刻意注意过,写java应用的同学应该都接触过FactoryBean,最常见的就是我们的Mapper。FactoryBean创建bean的过程比较特殊,他会先创建一个FactoryBean的实例,然后由这个FactoryBean实例如创建出我们最终想要的bean实例。因此FactoryBean初始化的不是最终得到的实例,而是生成这个实例的工厂,而这个工厂的初始化完成与否,大概率会影响到bean的生成,因此他不能简单的将初始化过程异步化。
image.png
如图是一个Bean的获取过程:

  1. 如果这个Bean是一个单例并且没有被创建过,那么就会进入createBean,并且在其中完成初始化。
  2. 如果这个Bean是一个FactoryBean,那么createBean返回的不是bean本身,而是一个factory,真正的bean要在之后的getInstance方法中获取。

在我们的项目中,存在着大量HSFSpringConsumerBean的实例,他们都是FactoryBean,而且这些bean的初始化还相当的耗时。(HSFSpringConsumerBean是我们RPC框架HSF的consumer的FactoryBean)
好在FactoryBean也不是完全不能异步初始化,我们分析一个Bean的get获取过程,会发现,他分为createBean和getInstance两个阶段,在FactoryBean的处理中,如果能够将两个阶段人为分开,先异步完成第一阶段的调用,再触发第二阶段,就可以实现我们的目标。
image.png
如图,实现对factoryBean的加速,我们需要在FactoryBean被getBean之前将需要加速的Bean一起找出来,先手动触发它们的异步初始化,但不触发getObject方法,等这些FactoryBean初始化完成后,再交由spring按照原来的创建顺序,去触发他们的getBean方法(此时singleton已经创建,会直接进入getObject调用)。但如果这些Bean对其他的bean有依赖,可能会导致在第一步的异步初始化中产生间接依赖而触发getBean。
好消息是HSFSpringConsumerBean不对其他的bean有依赖,而且项目中绝大多数耗时的FactoryBean都是HSFSpringConsumerBean。如果在Spring初始化所有Bean之前,我们可以一次性并行把所有HSFSpringConsumerBean初始化掉,也能够获得较大的提升。对于其他不产生依赖的FactoryBean,也可以按照一样的方式处理,比如我们比较常见的mapper。
对于可能对其他bean产生依赖的FactoryBean,理论上我们也可以通过去阻塞这些bean的getBean方法,等待我们第一阶段预初始化的完成。目前项目中这些bean的比例很小,因此这部分功能尚未着手实现。

编译加速-module依赖关系优化

加速效果:☆☆☆
配置简易:☆
推荐指数:☆☆☆
目前项目使用的多mudule结构,往往含有start,mtop(接口层),service等层,其中module之间又相互依赖,导致一个低层module依赖的中间件,又会继续被上层模块解析。而往往上层模块自身非常薄,却因为依赖了低层模块,不得不反复解析庞大的依赖树,导致编译时间非常长。调整module的方式,是一种解决方案,但会破坏项目的module,失去来原来多module的优势。
我们针对含有start,mtop,service的项目,提出一种改动较小的优化方式:
image.png
如图所示:

  1. mtop层在依赖service层的时候,排除service层的所有间接依赖,仅针对mtop层自身也需要依赖的内容进行手动引入。
  2. start层依赖mtop和service,这里不能再做排除,因为项目是在start层进行打包的,如果排除,则会导致依赖包没有被正常打包到项目中而无法启动。
  3. 按照这种方式优化,能够节省在mtop层解析service依赖树的开销,往往有几十秒之多。

我们同时也提出约定,在使用这种module结构的时候,控制好mtop和service的边界:

  1. mtop层是对service提供的服务进行mtop接口级别的封装,尽量仅依赖应用内service层和common定义的服务和对象,不处理复杂的中间件逻辑
  2. 对于外部服务的使用和中间件定义的服务和对象的使用,尽量在service层封装,同时不宜将外部服务和中间件定义的对象直接透给mtop层,造成依赖扩散
  3. 这约定之后,清晰mtop层与service层的边界,mtop层就是操作service层定义的方法和对象来完成接口,涉及外部定义的方法和对象的,收口到service。

镜像治理-分层构建

加速效果:☆☆☆
配置简易:☆☆☆
推荐指数:☆☆☆☆
image.png
如图,我们使用docker进行构建时,push/pull image 的时候, 如果某一层的镜像已经存在了, 就会直接使用缓存, 跳过重复的推送和拉取过程。而正常情况下,应用打出的包 (一般是 tgz 包) 是一个整体, 即使用户只修改了一行代码, 也要打出一个完整的包,包含所有依赖的jar文件, 导致每次push和pull的时候,都要传输所有的jar包,导致效率低下。
如果在打包的时候将jar包和项目代码分到不同的层里面,在绝大多数构建中,jar包不发生变化,则需要被更新的内容大大减小,进而提升push和pull的速度。同时,在应用启动过程中,tgz包解压需要花费一定的时间,在分层打包的改造中,去除了压缩解压过程,使得速度进一步提升。
image.png
该方案对镜像构建、镜像拉取、应用启动三个过程均有提速,在idle-local中,综合收益超过30秒(一次构建加一次部署)。

应用停止过程加速

加速效果:☆☆☆☆
配置简易:☆☆
推荐指数:☆☆☆☆
停止应用过程中,比较耗时的有两个步骤:1、hsf优雅下线;2、应用停止。其中,hsf优雅下线过程,会先通知应用进行hsf provider下线,然后等待一个比较安全的时间,对于生产环境来说,15秒是相对安全的值,对于预发环境来说,可以不等待。应用停止是通过kill -0 信号进行应用停止,应用会进行一些停止前的操作,不同应用不尽相同,如果停止失败,则使用kill -9强制退出;对于已经进行HSF优雅下线并且没有其他关键退出动作的应用来说,可以直接关闭应用,可以加速停止的耗时,对于非线上环境环境来说,是比较适用的。

效果及展望

image.png
经过一系列的优化措施,我们可以看到我们的系统编译和启动过程得到了不错的优化。其中紫色部分的耗时基本可以忽略,红色部分的耗时减少了一倍。
image.png
到目前为止,我们落地了一套有效的加速方案,在idle-local上取得了不错的效果。后续我们将继续完善这一系列方案,一方面,我们将着力建设一个基于监控的长效管控方案,能够让应用长期保持较好的状态;另一方面,我们会将上述内容抽象成一个一站式落地方案,支持在其他项目快速的配置落地。

项目 优化效果 配置成本
Spring Bean异步初始化(自动) 10秒*n,随代码规格提升效果更明显 引入实现了加速的jar包
Spring Bean异步初始化(手动) 20秒*n,随代码规格提升效果更明显 引入实现了加速的jar包并配置bean
HSFSpringConsumerBean优化 10秒*n,随代码规格提升效果更明显 引入实现了加速的jar包
module依赖关系优化 40秒 需要调整pom并处理依赖
docker镜像分层 30秒 修改打包脚本
停止过程加速 40秒(非线上环境) 修改停止脚本
相关文章
|
12天前
|
机器学习/深度学习 API 语音技术
|
1月前
|
Cloud Native Devops 持续交付
构建未来:云原生架构在现代企业中的应用与挑战
【2月更文挑战第31天】 随着数字化转型的加速,云原生技术已经成为推动企业IT架构现代化的关键力量。本文深入探讨了云原生架构的核心组件、实施策略以及面临的主要挑战。通过分析容器化、微服务、DevOps和持续集成/持续部署(CI/CD)等关键技术,揭示了如何利用这些技术实现敏捷性、可扩展性和弹性。同时,文章还讨论了企业在采纳云原生实践中可能遇到的安全性、复杂性和文化适应性问题,并提供了解决这些问题的策略和建议。
|
1月前
|
数据库 Android开发 开发者
构建高性能微服务架构:从理论到实践构建高效Android应用:探究Kotlin协程的优势
【2月更文挑战第16天】 在当今快速迭代和竞争激烈的软件市场中,微服务架构以其灵活性、可扩展性和独立部署能力而受到企业的青睐。本文将深入探讨如何构建一个高性能的微服务系统,涵盖从理论基础到具体实现的各个方面。我们将重点讨论服务拆分策略、通信机制、数据一致性以及性能优化等关键主题,为读者提供一个清晰、实用的指南,以便在复杂多变的业务环境中构建和维护健壮的微服务体系结构。 【2月更文挑战第16天】 在移动开发领域,性能优化和流畅的用户体验是至关重要的。随着技术的不断进步,Kotlin作为一种现代编程语言,在Android开发中被广泛采用,尤其是其协程特性为异步编程带来了革命性的改进。本文旨在深入
240 5
|
1月前
Web应用基本架构
Web应用基本架构。
38 6
|
2月前
|
运维 监控 数据管理
Apollo与微服务架构:构建可扩展的应用程序
Apollo与微服务架构:构建可扩展的应用程序
|
7天前
|
人工智能 Serverless 数据处理
利用阿里云函数计算实现 Serverless 架构的应用
阿里云函数计算是事件驱动的Serverless服务,免服务器管理,自动扩展资源。它降低了基础设施成本,提高了开发效率,支持Web应用、数据处理、AI和定时任务等多种场景。通过实例展示了如何用Python实现图片压缩应用,通过OSS触发函数自动执行。阿里云函数计算在云计算时代助力企业实现快速迭代和高效运营。
43 0
|
11天前
|
运维 监控 自动驾驶
构建可扩展的应用程序:Apollo与微服务架构的完美结合
构建可扩展的应用程序:Apollo与微服务架构的完美结合
32 10
|
12天前
|
机器学习/深度学习 PyTorch API
|
12天前
|
机器学习/深度学习 语音技术 算法框架/工具
|
13天前
|
运维 Cloud Native 持续交付
构建未来:云原生架构在现代企业中的应用与挑战
【4月更文挑战第10天】 随着数字化转型的不断深入,企业对信息技术基础设施的要求日益提高。云原生架构作为一种新兴的设计理念和技术集合,以其灵活性、可扩展性和容错性,正在成为推动企业技术革新的关键力量。本文将探讨云原生技术的核心组件、实施策略以及面临的主要挑战,并分析如何通过采纳云原生架构来优化业务流程和提升服务效率。