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

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

作者:闲鱼技术——泊垚

背景

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

  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秒(非线上环境) 修改停止脚本
相关文章
|
9天前
|
运维 持续交付 开发工具
深入浅出:GitOps在微服务架构中的应用
【10月更文挑战第26天】本文深入探讨了GitOps在微服务架构中的应用,介绍了其核心理念、自动化部署流程和增强的可观测性。通过实例展示了GitOps如何简化服务部署、配置管理和故障恢复,并推荐了一些实用工具和开发技巧。
|
1天前
|
Go 数据处理 API
Go语言在微服务架构中的应用与优势
本文摘要采用问答形式,以期提供更直接的信息获取方式。 Q1: 为什么选择Go语言进行微服务开发? A1: Go语言的并发模型、简洁的语法和高效的编译速度使其成为微服务架构的理想选择。 Q2: Go语言在微服务架构中有哪些优势? A2: 主要优势包括高性能、高并发处理能力、简洁的代码和强大的标准库。 Q3: 文章将如何展示Go语言在微服务中的应用? A3: 通过对比其他语言和展示Go语言在实际项目中的应用案例,来说明其在微服务架构中的优势。
|
7天前
|
机器学习/深度学习 人工智能 自然语言处理
医疗行业的语音识别技术解析:AI多模态能力平台的应用与架构
AI多模态能力平台通过语音识别技术,实现实时转录医患对话,自动生成结构化数据,提高医疗效率。平台具备强大的环境降噪、语音分离及自然语言处理能力,支持与医院系统无缝集成,广泛应用于门诊记录、多学科会诊和急诊场景,显著提升工作效率和数据准确性。
|
7天前
|
JavaScript 持续交付 Docker
解锁新技能:Docker容器化部署在微服务架构中的应用
【10月更文挑战第29天】在数字化转型中,微服务架构因灵活性和可扩展性成为企业首选。Docker容器化技术为微服务的部署和管理带来革命性变化。本文探讨Docker在微服务架构中的应用,包括隔离性、可移植性、扩展性、版本控制等方面,并提供代码示例。
39 1
|
9天前
|
运维 Serverless 数据处理
Serverless架构通过提供更快的研发交付速度、降低成本、简化运维、优化资源利用、提供自动扩展能力、支持实时数据处理和快速原型开发等优势,为图像处理等计算密集型应用提供了一个高效、灵活且成本效益高的解决方案。
Serverless架构通过提供更快的研发交付速度、降低成本、简化运维、优化资源利用、提供自动扩展能力、支持实时数据处理和快速原型开发等优势,为图像处理等计算密集型应用提供了一个高效、灵活且成本效益高的解决方案。
37 1
|
12天前
|
前端开发 API UED
深入理解微前端架构:构建灵活、高效的前端应用
【10月更文挑战第23天】微前端架构是一种将前端应用分解为多个小型、独立、可复用的服务的方法。每个服务独立开发和部署,但共同提供一致的用户体验。本文探讨了微前端架构的核心概念、优势及实施方法,包括定义服务边界、建立通信机制、共享UI组件库和版本控制等。通过实际案例和职业心得,帮助读者更好地理解和应用微前端架构。
|
13天前
|
运维 监控 Serverless
Serverless架构在图像处理等计算密集型应用中展现了显著的优势
Serverless架构在图像处理等计算密集型应用中展现了显著的优势
26 1
|
19天前
|
运维 Cloud Native 持续交付
云原生技术在现代IT架构中的深度应用与挑战####
【10月更文挑战第17天】 本文深入剖析了云原生技术的精髓,探讨其在现代IT架构转型中的核心作用与面临的挑战。云原生不仅是一种技术实现,更是企业数字化转型的重要推手,通过容器化、微服务、持续集成/持续部署(CI/CD)等关键要素,重塑软件开发、部署与运维模式。文章首先概述了云原生的基本原则与核心组件,随后分析了其如何促进企业敏捷性、可扩展性和资源利用率的提升,同时也指出了在安全性、复杂性管理及人才技能匹配等方面存在的挑战,并提出了相应的对策建议。 ####
57 6
|
20天前
|
Cloud Native Go API
Go语言在微服务架构中的创新应用与实践
本文深入探讨了Go语言在构建高效、可扩展的微服务架构中的应用。Go语言以其轻量级协程(goroutine)和强大的并发处理能力,成为微服务开发的首选语言之一。通过实际案例分析,本文展示了如何利用Go语言的特性优化微服务的设计与实现,提高系统的响应速度和稳定性。文章还讨论了Go语言在微服务生态中的角色,以及面临的挑战和未来发展趋势。
|
18天前
|
前端开发 API UED
拥抱微前端架构:构建灵活、高效的前端应用
【10月更文挑战第17天】微前端架构是一种将前端应用拆分为多个小型、独立、可复用的服务的方法,每个服务可以独立开发、部署和维护。本文介绍了微前端架构的核心概念、优势及实施步骤,并分享了业界应用案例和职业心得,帮助读者理解和应用这一新兴架构模式。
下一篇
无影云桌面