Sonic是美团外部研发设计的一款用于热部署的IDEA插件,本文其实现原理及落地的一些技术细节。在浏览本文之前,倡议大家先相熟一下Spring源码、Spring MVC 源码 、Spring Boot源码 、Agent字节码加强、Javassist、Classloader等相干常识。

1 前言

1.1 什么是热部署

所谓热部署,就是在利用正在运行时降级软件,却不须要重新启动利用。对于Java应用程序来说,热部署就是在运行时更新Java类文件,同时触发Spring以及其余罕用第三方框架的一系列从新加载的过程。在这个过程中不须要重新启动,并且批改的代码实时失效,好比是战斗机在地面实现加油,不须要战斗机熄火起飞,一系列操作都在“运行”状态来实现。

1.2 为什么咱们须要热部署

据理解,美团外部很多工程师每天本地重启服务高达5~12次,单次大略3~8分钟,每天向Cargo(美团内部测试环境管理工具)部署3~5次,单次时长20~45分钟,部署频繁频次高、耗时长,重大影响了零碎上线的效率。而插件提供的本地和近程热部署性能,可让将代码变更“秒级”失效。一般而言,开发者日常工作次要分为开发自测和联调两个场景,上面将别离介绍热部署在每个场景中施展的作用。

1.2.1 开发自测场景

一般来讲,在用插件之前,开发者批改完代码还需期待3~8分钟启动工夫,而后手动结构申请或协调上游发申请,耗时且费劲。在应用完热部署插件后,批改完代码能够一键增量部署,让变更“秒级”失效,可能做到疾速自测。而对于那些无奈本地启动我的项目,也能够通过近程热部署性能使代码变更“秒级”失效。

1.2.2 联调场景

通常状况下,在应用插件之前,开发者批改代码通过20~35分钟的漫长部署,须要分割上游联调开发者发动申请,始终要等到近程服务器查看日志,能力确认代码失效。在应用热部署插件之后,开发者批改代码近程热部署可能秒级(2~10s)失效,开发者间接发动服务调用,能够节俭大量的碎片化工夫(热部署插件还具备流量回放、近程调用、近程反编译等性能,可配合进行应用)。

所以,热部署插件心愿解决的痛点是:在可控的条件内,帮忙开发者缩小频繁编译部署的次数,节俭碎片化的工夫。最终为开发者每天节约出一定量的编码工夫

1.3 热部署难在哪

为什么业界目前没有好用的开源工具?因为热部署不等同于热重启,像Tomcat或者Spring Boot DevTools此类热重启模式须要从新加载我的项目,性能较差。增量热部署难度较大,须要兼容罕用的中间件版本,须要深刻启动销毁加载流程。以美团为例,咱们须要对JPDA(Java Platform Debugger Architecture)、Java Agent、ASM字节码加强、Classloader、Spring框架、Spring Boot框架、MyBatis框架、Mtthrift(美团RPC框架)、Zebra(美团长久层框架)、Pigeon(美团RPC框架),MDP(美团疾速开发框架)、XFrame(美团疾速开发脚手架)、Crane(美团分布式任务调度框架)等泛滥框架和技术原理深刻理解能力做到全面的兼容和反对。另外,还须要IDEA插件开发能力,造成整体的产品解决方案闭环,美团的热部署插件Sonic正是在这种背景下应运而生。

1.4 Sonic能够做什么

Sonic是美团外部研发设计的一款IDEA插件,旨在通过低代码开发辅助近程/本地热部署,解决Coding、单测编写执行、自测联调等阶段的效率问题,进步开发者的编码产出效率。数据统计表明,开发者日常大略有35%工夫用于编码的产出。如果想进步研发效率,要么扩充编码产出的工夫占比,要么进步编码阶段的产出效率,而Sonic则聚焦进步编码阶段的产出效率。

目前,应用Sonic热部署能够解决大部分代码反复构建的问题。Sonic能够使用户在本地编写代码一键部署到近程环境,批改代码、部署、联调申请、查看日志,循环反复。如果不思考代码批改工夫,通常一个循环须要20~35分钟,而应用Sonic能够把整个时长缩短至5~10秒,而且可能给开发者带来高效沉迷式的开发体验。在理论编码工作中,多文件批改是粗茶淡饭,Sonic对多文件的热部署能力尤为突出,它能够通过依赖剖析等伎俩来对多文件批量进行近程热部署,并且反对Spring Bean Class、一般Class、Spring XML、MyBatis XML等多类型文件混合热部署。

那么跟业界现有的产品相比,Sonic有哪些优劣势呢?上面咱们尝试给出几种产品的比照,仅供大家参考:

个性JRebelSpring Boot DevToolsIDEA热加载Tomcat热加载Spring LoaderSonic
近程Debug基于Debug协定批改
批改办法体内容✅效率低✅效率低
新增办法体✅效率低✅效率低
Jar包变更✅效率低✅效率低
Spring MVC✅效率低✅效率低
多文件热部署✅效率低✅效率低
新增泛型办法✅效率低✅效率低
新增非动态字段✅效率低✅效率低
新增动态字段✅效率低✅效率低
新增批改继承类✅效率低✅效率低
新增批改接口办法✅效率低✅效率低
新增批改匿名外部类✅效率低✅效率低
减少批改动态块✅效率低✅效率低
FastJson✅效率低✅效率低
Cglib✅效率低✅效率低
MyBatis Annotation✅效率低✅效率低
MyBatis XML✅效率低✅效率低
Gson✅效率低✅效率低
Jackson✅效率低✅效率低
Jdk代理✅效率低✅效率低
Log4j✅效率低✅效率低
Slf4J✅效率低✅效率低
Logback✅效率低✅效率低
Spring Tx✅效率低✅效率低
Spring 新增Xml✅效率低✅效率低
Spring Bean✅效率低✅效率低
Spring Boot✅效率低✅效率低
Spring Validator✅效率低✅效率低
近程热部署配置繁琐
IDEA插件集成

上表未把Sofa-Ark、Osgi、Arthas列举,此类属于插件化、模块化利用框架,以及Java在线诊断工具,外围能力非热部署。值得注意的是,Spring Boot DevTools只能利用在Spring Boot我的项目中,并且它不是增量热部署,而是通过Classloader迭代的形式重启我的项目,对大我的项目而言,性能上是无奈承受的。尽管,JRebel反对三方插件较多,生态宏大,然而对于国产的插件不反对,例如FastJson等,同时它还存在近程热部署配置局限,对于公司外部的中间件须要个性化开发,并且是商业软件,整体的应用老本较高。

1.5 Sonic近程热部署落地推广的实践经验

置信大家都晓得,对于技术产品的推广,尤其是开发、测试阶段应用的产品,因为远离线上环境,推动力、执行力、产品性能闭环是否做好,是决定着该产品是否能在企业外部落地并失去大多数人认可的重要的一环。此外,因为很多开发者在开发、测试阶段已逐步造成了“固化动作”,如何扭转这些用户的行为,让他们拥抱新产品,也是Sonic面临的艰巨挑战之一。咱们从被动沟通、零老本(或极低成本)疾速接入、自动化脚本,以及产品主动诊断、收集反馈等方向登程,践行出了四条准则。

2 整体设计方案

2.1 Sonic构造

Sonic插件由4大部分组成,包含脚本端、插件端、Agent端,以及Sonic服务端。脚本端负责自动化构建Sonic启动参数、服务启动等集成工作;IDEA插件端集成环境为开发者提供更便捷的热部署服务;Agent端随我的项目启动负责热部署的性能实现;服务端则负责收集热部署信息、失败上报等统计工作。如下图所示:

2.2 走进Agent

2.2.1 Instrumentation类罕用API

public interface Instrumentation {    //减少一个Class 文件的转换器,转换器用于扭转 Class 二进制流的数据,参数 canRetransform 设置是否容许从新转换。    void addTransformer(ClassFileTransformer transformer, boolean canRetransform);    //在类加载之前,从新定义 Class 文件,ClassDefinition 示意对一个类新的定义,    //如果在类加载之后,须要应用 retransformClasses 办法从新定义。addTransformer办法配置之后,后续的类加载都会被Transformer拦挡。    //对于曾经加载过的类,能够执行retransformClasses来从新触发这个Transformer的拦挡。类加载的字节码被批改后,除非再次被retransform,否则不会复原。    void addTransformer(ClassFileTransformer transformer);    //删除一个类转换器    boolean removeTransformer(ClassFileTransformer transformer);        //是否容许对class retransform    boolean isRetransformClassesSupported();    //在类加载之后,从新定义 Class。这个很重要,该办法是1.6 之后退出的,事实上,该办法是 update 了一个类。    void retransformClasses(Class<?>... classes) throws UnmodifiableClassException;       //是否容许对class从新定义    boolean isRedefineClassesSupported();    //此办法用于替换类的定义,而不援用现有的类文件字节,就像从源代码从新编译以进行修复和持续调试时所做的那样。    //在要转换现有类文件字节的中央(例如在字节码插装中),应该应用retransformClasses。    //该办法能够批改办法体、常量池和属性值,但不能新增、删除、重命名属性或办法,也不能批改办法的签名    void redefineClasses(ClassDefinition... definitions) throws  ClassNotFoundException, UnmodifiableClassException;    //获取曾经被JVM加载的class,有className可能反复(可能存在多个classloader)    @SuppressWarnings("rawtypes")    Class[] getAllLoadedClasses();}

2.2.2 Instrument简介

Instrument的底层实现依赖于JVMTI(JVM Tool Interface),它是JVM裸露进去的一些供用户扩大的接口汇合,JVMTI是基于事件驱动的,JVM每执行到肯定的逻辑就会调用一些事件的回调接口(如果存在),这些接口能够供开发者去扩大本人的逻辑。

JVMTIAgent是一个利用JVMTI裸露进去的接口提供了代理启动时加载(Agent On Load)、代理通过Attach模式加载(Agent On Attach)和代理卸载(Agent On Unload)性能的动静库。而Instrument Agent能够了解为一类JVMTIAgent动静库,别名是JPLISAgent(Java Programming Language Instrumentation Services Agent),也就是专门为Java语言编写的插桩服务提供反对的代理。

2.2.3 启动时和运行时加载Instrument Agent过程

2.3 那些年JVM和HotSwap之间的“相爱相杀”

围绕着Method Body的HotSwap JVM始终在进行改良。从1.4版本开始,JPDA引入HotSwap机制(JPDA Enhancements),实现Debug时的Method Body的动态性。大家可参考文档:enhancements1.4

1.5版本开始通过JVMTI实现的java.lang.instrument(Java Platform SE 8)的Premain形式,实现Agent形式的动态性(JVM启动时指定Agent)。大家可参考文档:package-summary。

1.6版本又减少Agentmain形式,实现运行时动态性(通过The Attach API 绑定到具体VM)。大家可参考文档:package-summary 。根本实现是通过JVMTI的retransformClass/redefineClass进行method、body级的字节码更新,ASM、CGLib根本都是围绕这些在做动态性。然而针对Class的HotSwap始终没有动作(比方Class增加method、增加field、批改继承关系等等),为什么会这样呢?因为简单度过高,且没有很高的回报。

2.4 Sonic如何解决Instrumentation的局限性

因为JVM限度,JDK 7和JDK 8都不容许改类构造,比方新增字段,新增办法和批改类的父类等,这对于Spring我的项目来说是致命的。比方开发同学想批改一个Spring Bean,新增一个@Autowired字段,此类场景在理论利用时很多,所以Sonic对此类场景的反对必不可少。

那么,具体是如何做到的呢?这里要提一下“赫赫有名”的Dcevm。Dcevm(DynamicCode Evolution Virtual Machine)是Java Hostspot的补丁(严格上来说是批改),容许(并非无限度)在运行环境下批改加载的类文件。以后虚拟机只容许批改办法体(Method,Body),而Decvm能够减少、删除类属性、办法,甚至扭转一个类的父类,Dcevm是一个开源我的项目,听从GPL 2.0协定。更多对于Dcevm的介绍,大家能够参考:Wuerthinger10a以及GitHub Decvm。

值得一提的是,在美团外部,针对Dcevm的装置,Sonic曾经买通HULK,集成公布镜像即可实现(本地热部署可联合插件性能实现一键装置热部署环境)。

3 Sonic热部署技术解析

3.1 Sonic整体架构模型

上一章节咱们次要介绍了Sonic的组成。下图具体介绍了Sonic在运行期间各个组成部分的工作职责,由它们造成一整套齐备的技术产品落地闭环计划:

3.2 Sonic性能流转

Sonic通过NIO监听本地文件变更,触发文件变更事件,例如Class新增、Class批改、Spring Bean重载等事件流程。下图展现了一次热部署单个文件的生命周期:

3.3 文件监听

Sonic首先会在本地和近程预约义两个目录,/var/tmp/sonic/extraClasspath/var/tmp/sonic/classes。extraClasspath为Sonic自定义的拓展Classpath URL,classes为Sonic监听的目录,当有文件变更时,通过IDEA插件来部署到近程/本地,触发Agent的监听目录,来持续上面的热加载逻辑:

为什么Sonic不间接替换用户ClassPath上面的资源文件呢?因为思考到业务方WAR包的API我的项目、Spring Boot、Tomcat我的项目、Jetty我的项目等,都是以JAR包来启动的,这样是无奈间接批改用户的Class文件的。即便是用户我的项目能够批改,间接操作用户的Class,也会带来一系列的平安问题。

所以,Sonic采纳拓展ClassPath URL门路来实现文件的批改和新增。并且存在这么一种场景,多个业务侧的我的项目引入雷同的JAR包,在JAR外面配置MyBatis的XML和注解。在此类情况下,Sonic没有方法间接来批改JAR包中源文件,通过拓展门路的形式能够不须要关注JAR包,来批改JAR包中某一文件和XML。同理,采纳此类办法能够进行整个JAR包的热替换。上面咱们简略介绍一下Sonic的外围监听器,如下图所示:

3.4 JVM Class Reload

JVM的字节码批量重载逻辑,通过新的字节码二进制流和旧的Class对象生成ClassDefinition定义,instrumentation.redefineClasses(definitions),来触发JVM重载,重载过后将触发初始化时Spring插件注册的Transfrom。接下来,咱们简略解说一下Spring是怎么重载的。

新增class Sonic如何保障能够加载到Classloader上下文中?因为我的项目在近程执行,所以运行环境简单,有可能是JAR包形式启动(Spring Boot),也有可能是一般我的项目,也有可能是War Web我的项目,针对此类情况Sonic做了一层Classloader URL拓展。

User ClassLoader是框架自定义的ClassLoader统称,例如Jetty我的项目是WebAppclassLoader。其中Urlclasspath为以后我的项目的lib文件件下,例如Spring Boot我的项目也是从以后我的项目BOOT-INF/lib/门路中加载CLass等等,不同框架的自定义地位稍有不同。所以针对此类情况,Agent必须拿到用户的自定义Classloader,如果是惯例形式启动的,比方一般Spring XML我的项目,借助Plus(美团外部服务公布平台)公布,此类没有自定义Classloader,是默认AppClassLoader,所以Agent在用户我的项目启动过程中,借助字节码加强的形式来获取到真正的用户Classloader。

找到用户应用的子Classloader之后,通过反射的形式来获取Classloader中的元素Classpath,其中ClassPath中的URL就是以后我的项目加载Class时须要的所有运行时Class环境,并且包含三方的JAR包依赖等。

Sonic获取到URL数组,把Sonic自定义的拓展Classpath目录退出到URL数组首位,这样当有新增Class时,Sonic只须要将Class文件复制到拓展Classpath对应的包目录上面即可,当有其余Bean依赖新增的Class时,会从当前目录上面查找类文件。

为什么不间接对Appclassloader进行增强?而是对框架的自定义Classloader进行增强?

思考这样一个场景,框架自定义类加载器中有ClassA,此时用户新增ClassB须要热加载,B Class外面有A的援用关系,如果加强AppClassLoader,初始化B实例时ClassLoader。loadclass首先从UserClassLoader开始加载ClassB的字节码,依附双亲委派准则,B被Appclassloader加载,因为B依赖类A,所以以后AppClassLoader加载B肯定是加载不到的,此时会抛出ClassNotFoundException异样。所以对类加载器拓展,肯定要拓展最上层的类加载器,这样才会达到使用者想要的成果。

3.5 Spring Bean重载

Spring Bean Reload过程中,Bean的销毁和重启流程,次要内容如下图展现:

首先当批改Java Class D时,通过Spring ClasspathScan扫描校验以后批改的Bean是否Sprin Bean(注解校验),而后触发销毁流程(BeanDefinitionRegistry.removeBeanDefinition),此办法会将以后Spring上下文中的Bean D和依赖Spring Bean D的Bean C一并销毁,然而作用范畴仅仅在以后Spring上下文。如果C被子上下文中的Bean B依赖,就无奈更新子上下文中的依赖关系,当有零碎申请时,Bean B中关联的Bean C还是热部署之前的对象,所以热部署失败。

因而,在Spring初始化过程中,须要保护父子上下文的对应关系,当子上下文变时若变更范畴波及到Bean B时,须要从新更新子上下文中的依赖关系,当有多上下文关联时须要保护多上下文环境,且以后上下文环境入口须要Reload。这里的入口是指:Spring MVC Controller、Mthrift和Pigeon,对不同的流量入口,采纳不同的Reload策略。RPC框架入口次要操作为解绑注册核心、从新注册、从新加载启动流程等等,对Spring MVC Controller,次要是解绑和注册URL Mappping来实现流量入口类的变动切换。

3.6 Spring XML重载

当用户批改/新增Spring XML时,须要对XML中所有Bean进行重载。

从新Reload之后,将Spring销毁后重启。须要留神的是:XML批改形式改变较大,可能波及到全局的AOP的配置以及前置和后置处理器相干的内容,影响范畴为全局,所以目前只放开一般的XML Bean标签的新增/批改,其余能力酌情逐渐放开。

3.7 MyBatis 热部署

Spring MyBatis热部署的次要解决流程是在启动期间获取所有Configuration门路,并保护它和Spring Context的对应关系,在热部署Class、XML时去匹配Configuration,从而从新加载Configuration以达到热部署的目标。

4 总结

4.1 热部署性能一览

上一章节次要讲述了Spring Bean、Spring MVC、MyBatis的重载流程,Sonic还反对其它罕用的开发框架,丰盛的框架反对和兼容能力是Sonic的基石,上面列举一些Sonic反对的罕用的第三方框架:

截止目前,Sonic曾经反对绝大部分罕用第三方框架的热加载,惯例业务开发简直无需重启服务。并且在美团外部的成功率曾经高达99.9%以上,真正地让热部署来代替惯例部署构建成为一种可能。

4.2 IDE插件集成

Sonic也提供了功能强大的IDEA插件,让用户进行沉迷式开发,近程热部署也变得更加便当。

4.3 推广应用状况

截止到发稿时,Sonic在美团应用人数3000+,利用我的项目数量2000+。该我的项目还取得了美团外部2020年下半年到家研发平台“最佳效率团队”奖。

5 作者简介

凯哥、占峰、李晗、龚炎、程骁、玉龙等,均来自美团/到家研发平台。

6 参考文章

  • [1] 基于Javassist和Javaagent实现动静切面
  • [2] Spring MVC 源码解析
  • [3] Spring IOC源码解析
  • [4] MyBatis源码解析
  • [5] Spring Boot源码解析
  • [6] Spring AOP源码解析
  • [7] Spring事务源码解析
  • [8] Cglib源码解析
  • [9] JDK Proxy源码解析
  • [10] Dcevm简介
  • [11] 字节码加强技术摸索
  • [12] Javassist API

浏览美团技术团队更多技术文章合集

前端 | 算法 | 后端 | 数据 | 平安 | 运维 | iOS | Android | 测试

| 在公众号菜单栏对话框回复【2021年货】、【2020年货】、【2019年货】、【2018年货】、【2017年货】等关键词,可查看美团技术团队历年技术文章合集。

| 本文系美团技术团队出品,著作权归属美团。欢送出于分享和交换等非商业目标转载或应用本文内容,敬请注明“内容转载自美团技术团队”。本文未经许可,不得进行商业性转载或者应用。任何商用行为,请发送邮件至tech@meituan.com申请受权。