当前位置: 首页 > 后端技术 > Java

Java系列-美团远程热部署实践

时间:2023-04-01 14:15:25 Java

Sonic是美团开发设计的一款IDEA热部署插件。本文介绍了它的实现原理和实现的一些技术细节。在阅读本文之前,建议您熟悉Spring源码、SpringMVC源码、SpringBoot源码、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或者SpringBootDevTools这样的热重启方式需要重新加载项目,性能较差。增量热部署难度较大,需要兼容常用的中间件版本,需要深入启动销毁和加载过程。以美团为例,我们需要实现JPDA(JavaPlatformDebuggerArchitecture)、JavaAgent、ASM字节码增强、Classloader、Spring框架、SpringBoot框架、MyBatis框架、Mtthrift(美团RPC框架)、Zebra(美团持久层框架))、Pigeon(美团RPC框架)、MDP(美团快速开发框架)、XFrame(美团快速开发脚手架)、Crane(美团分布式任务调度框架)等众多框架和技术原理可以深入理解,实现全面兼容和支持.此外,还需要IDEA的插件开发能力,形成闭环的产品整体解决方案。正是在这样的背景下,美团的热部署插件Sonic应运而生。1.4Sonic可以做什么Sonic是美团点评开发设计的IDEA插件。开发人员编码生产力。统计数据显示,开发人员每天大约35%的时间花在编码输出上。要想提高研发效率,要么提高编码输出的时间比,要么提高编码阶段的输出效率,而索尼克则侧重于提高编码阶段的输出效率。目前使用Sonic热部署可以解决大部分代码重复构建的问题。Sonic允许用户在本地编写代码,一键部署到远程环境,修改代码,部署,加入调试请求,查看日志,如此循环往复。如果不考虑代码修改时间,一个周期通常需要20-35分钟,但使用Sonic可以将整个时间缩短到5-10秒,能够为开发者带来高效沉浸式的开发体验。在实际编码工作中,多文件修改是家常便饭,而Sonic的多文件热部署能力尤为突出。可以通过依赖分析等方式对多个文件进行批量远程热部署,支持SpringBeanClass、普通Class、SpringMixedXML、MyBatisXML等多种类型文件的热部署。那么与业内现有产品相比,Sonic有哪些优缺点呢?下面我们尝试给出几款产品的对比,仅供大家参考:特性JRebelSpringBootDevToolsIDEA热加载Tomcat热加载SpringLoaderSonic基于调试协议修改远程调试?????修改方法体内容??低效率??低效率??新方法体??低效率??低效率??改Jar包??低效率??低效率??SpringMVC??低效率??低效率??多文件热部署??低效率??低效率??新泛型方法??低效率??低效率??新非静态字段??低效率??低效率??添加静态字段??低效率?修改?低效率NEW?并添加继承类??低效率??低效率??新修改接口方法??低效率??低效率??新修改匿名内部类??低效率??低效率??增加修改静态块?低效率??低效率??FastJson??低效率??低效率??Cglib??低效率??低效率??MyBatisAnnotation??低效率??低效率??MyBatis高效XML??低效率?低???Gson??低效率??低效率??Jackson??低效率??低效率??Jdk代理??低效率??低效率??Log4j??低效率??低效率??Slf4J低效率??低效率??Logback??低效率??低效率??SpringTx??低效率??低效率??SpringAddedXml??低效率???效率低?效率低??效率低??SpringBoot??效率低??效率低??SpringValidator??效率低??效率低??远程热部署配置繁琐????IDEA插件集成??????Sofa-方舟、奥斯吉、阿尔萨斯不在上表中。这些是插件化、模块化的应用框架和Java在线诊断工具,它们的核心能力不是热部署。值得注意的是,SpringBootDevTools只能应用于SpringBoot项目,并不是增量热部署,而是通过Classloader迭代重启项目。对于大型项目,性能是不可接受的。JRebel虽然支持众多第三方插件,生态庞大,但不支持国产插件,如FastJson等,同时也存在远程热部署配置的局限性。内部中间件需要个性化开发,属于商业软件。综合使用成本较高。1.5Sonic远程热部署推广实践经验。相信大家都知道,对于技术产品的推广,尤其是开发测试阶段使用的产品,因为远离线上环境,能否做好驱动力、执行力、产品功能的闭环,是决定产品能否在企业内部落地并被大多数人认可的重要一环。此外,由于很多开发者在开发测试阶段已经逐渐形成“固定动作”,如何改变这些用户的行为,让他们拥抱新产品,也是索尼克面临的严峻挑战之一。从主动沟通、零成本(或极低成本)快速接入、自动化脚本、产品自动诊断、反馈收集入手,我们践行了四大原则。2总体设计方案2.1Sonic结构Sonic插件由脚本端、插件端、Agent端、Sonic服务器端4部分组成。脚本端负责Sonic启动参数的自动构建、服务启动等集成工作;IDEA插件端集成环境,为开发者提供更便捷的热部署服务;Agent端负责在项目启动时实现热部署功能;服务器端负责收集热部署信息和故障报告等统计工作。如下图所示:2.2进入Agent2.2.1Instrumentation类常用APIpublicinterfaceInstrumentation{//添加类文件转换器,转换器用于改变Class二进制流的数据,参数canRetransform设置是否允许再改造。voidaddTransformer(ClassFileTransformer转换器,布尔canRetransform);//类加载前,重新定义Class文件,ClassDefinition代表新定义一个类,//如果类加载后,需要使用retransformClasses方法重新定义。配置addTransformer方法后,后续的类加载会被Transformer拦截。//对于已经加载的类,可以执行retransformClasses重新触发Transformer的拦截。类加载的字节码修改后,除非再次重新转换,否则不会恢复。voidaddTransformer(ClassFileTransformer转换器);//删除一个类transformerbooleanremoveTransformer(ClassFileTransformertransformer);//是否允许classretransformbooleanisRetransformClassesSupported();//类加载完成后,重新定义Class。这个非常重要。这个方法是1.6之后加入的。实际上,这个方法更新了一个类。voidretransformClasses(Class...classes)抛出UnmodifiableClassException;//是否允许重定义类booleanisRedefineClassesSupported();//此方法用于在不引用现有类文件字节的情况下替换类的定义,就像您从源代码重新编译以修复并继续调试时所做的那样。//如果你想转换现有类文件的字节(例如在字节码检测中),你应该使用retransformClasses。//该方法可以修改方法体、常量池和属性值,但不能增删改名属性或方法,也不能修改方法签名voidredefineClasses(ClassDefinition...definitions)throwsClassNotFoundException,UnmodifiableClassException;//获取JVM加载过的class,className可能重复(可能有多个classloader)@SuppressWarnings("rawtypes")Class[]getAllLoadedClasses();}2.2.2Instrument简介底层实现Instrument的实现依赖于JVMTI(JVMToolInterface),JVMTI是JVM暴露给用户扩展的一组接口。JVMTI是事件驱动的。JVM每次执行某个逻辑时,都会调用一些事件回调接口(如果存在的话)。这些接口可供开发人员使用。扩展你自己的逻辑。JVMTIAgent是一个动态库,它使用JVMTI暴露的接口提供AgentOnLoad、AgentOnAttach和AgentOnUnload的功能。InstrumentAgent可以理解为一类JVMTIAgent动态库,别名为JPLISAgent(JavaProgrammingLanguageInstrumentationServicesAgent),是一种为Java语言编写的仪器服务提供支持的代理。2.2.3启动和运行时加载InstrumentAgent的过程2.3那些年,JVM和HotSwap的“相爱相杀”围绕着MethodBody的HotSwapJVM不断完善。从1.4版本开始,JPDA引入了HotSwap机制(JPDAEnhancements)来实现Debug时MethodBody的动态化。可以参考文档:enhancements1.4。从1.5版本开始,JVMTI实现的java.lang.instrument(JavaPlatformSE8)的premain方法实现了Agent方法的动态化(在JVM启动时指定Agent)。可以参考文档:package-summary。1.6版本添加了Agentmain方法来实现运行时动态(通过AttachAPI绑定到特定的VM)。可以参考文档:package-summary。基本实现是通过JVMTI的retransformClass/redefineClass更新method和body级字节码。ASM和CGLib基本上围绕这些动态。但是HotSwapforClass并没有对Classes进行任何动作(比如添加方法、添加字段、修改继承关系等)。为什么会这样?因为复杂度太高,而且没有高回报。2.4Sonic如何解决Instrumentation的局限性由于JVM的局限性,JDK7和JDK8不允许改变类结构,比如增加字段,增加方法,修改类的父类等,这就是对于Spring项目来说是致命的。例如,开发人员想要修改一个SpringBean并添加一个@Autowired字段。实际应用中有很多这样的场景,所以Sonic对这类场景的支持是必不可少的。那么,它是如何完成的呢?这里要提一下“大名鼎鼎”的Dcevm。Dcevm(DynamicCodeEvolutionVirtualMachine)是JavaHostspot的补丁(严格来说是修改),允许(不是无限制)修改运行环境中加载的类文件。现在的虚拟机只允许修改方法体(Method,Body),而Decvm可以增删类的属性、方法,甚至可以改变一个类的父类。Decvm是一个开源项目,遵守GPL2.0协议。关于Decvm的更多信息可以参考:Wuerthinger10a和GitHubDecvm。值得一提的是,在美团内部,Sonic为Dcevm的安装开放了HULK,集成release镜像即可完成(本地热部署可结合插件功能实现一键安装Dcevm)热部署环境)。3Sonic热部署技术分析3.1Sonic整体架构模型在上一章中,我们主要介绍了Sonic的组成。下图详细介绍了Sonic在运行过程中各组件的工作职责,形成了一套完整的技术产品落地闭环解决方案:3.2Sonic功能流程Sonic通过NIO监听本地文件变化,并触发文件变化事件,如新增Class、Class修改、SpringBean重载等事件流程。下图是单个文件热部署的生命周期:3.3文件监控Sonic会先在本地和远程预定义两个目录,/var/tmp/sonic/extraClasspath和/var/tmp/sonic/classes。extraClasspath是Sonic自定义扩展的ClasspathURL,classes是Sonic监控的目录。当有文件变化时,通过IDEA插件部署到remote/local,触发Agent的监控目录继续下面的热加载逻辑:为什么Sonic不直接替换用户ClassPath下的资源文件呢?因为考虑到业务方的WAR包的API项目、SpringBoot、Tomcat项目、Jetty项目等都是以JAR包启动的,所以不可能直接修改用户的Class文件。即使用户项目可以修改,直接操作用户的Class也会带来一系列的安全问题。因此,Sonic使用扩展的ClassPathURL路径来实现文件的修改和添加。并且有这样一种场景,多个业务端项目导入同一个JAR包,在JAR中配置MyBatisXML和注解。这种情况下,Sonic没有办法直接修改JAR包中的源文件。通过扩展路径,可以在不关注JAR包的情况下修改JAR包中的某个文件和XML。同理,也可以使用该方法热替换整个JAR包。下面简单介绍一下Sonic的核心监听器,如下图所示:3.4JVMClassReloadJVM的字节码批量重载逻辑通过新的字节码二进制流和旧的Class对象instrumentation.redefineClasses(definitions)生成ClassDefinition定义来触发JVMreloading,会触发Spring插件在初始化时注册的Transfrom。接下来简单说明一下Spring是如何重载的。如何将新类Sonic加载到类加载器上下文中?由于项目远程执行,操作环境复杂。它可能以JAR包(SpringBoot)的形式启动,也可能是一个普通的项目,也可能是一个WarWeb项目。针对这种情况,Sonic做了一层Classloader的URL扩展。UserClassLoader是框架自定义的ClassLoader的统称。例如Jetty项目是WebAppclassLoader。其中,Urlclasspath在当前项目的lib文件下。比如SpringBoot项目也是从当前项目的BOOT-INF/lib/路径加载Class,不同框架的自定义位置略有不同。所以对于这种情况,Agent必须拿到用户自定义的Classloader。如果以常规方式启动,比如普通的SpringXML项目,则借助Plus(美团内部服务发布平台)进行发布。该类没有自定义的Classloader,是默认的AppClassLoader,所以Agent在用户项目的启动过程中通过字节码增强的方式获取真实的用户Classloader。找到用户使用的子Classloader后,通过反射获取Classloader中的元素Classpath。ClassPath中的URL为当前项目加载Class所需的所有运行时Class环境,包括三方JAR包依赖等。Sonic获取URL数组,将Sonic自定义扩展的Classpath目录添加到第一个URL数组中,这样当有新的Class时,Sonic只需要将Class文件复制到扩展Classpath对应的包目录下即可。当有其他Beans依赖新添加的Class时,它会从当前目录中搜索class文件。为什么不直接增强Appclassloader呢?而是加强框架的自定义Classloader?考虑这样一个场景。框架的自定义类加载器中有ClassA。此时用户添加了ClassB,需要热加载。B类中存在A的引用关系,如果增强了AppClassLoader,在初始化实例B的同时也初始化了ClassLoader。loadclass首先从UserClassLoader加载ClassB的字节码。依靠双亲委托的原理,B由AppClassLoader加载。因为B依赖于A类,所以当前的AppClassLoader肯定加载不了B,这时候就会抛出ClassNotFoundException。因此,要扩展类加载器,就需要扩展顶层类加载器,这样才能达到用户想要的效果。3.5SpringBeanReloadingSpringBeanReload过程中的Bean销毁和重启过程,主要内容如下图所示:首先,在修改JavaClassD时,使用SpringClasspathScan扫描验证当前修改的Bean是否为一个SprinBean(注解验证),然后触发销毁过程(BeanDefinitionRegistry.removeBeanDefinition),该方法会销毁当前Spring上下文中的BeanD和依赖SpringBeanD的BeanC,但作用范围仅限于当前的Spring上下文。如果C在子上下文中依赖于BeanB,则无法更新子上下文中的依赖项。当有系统请求时,BeanB中关联的BeanC还是热部署前的对象,所以热部署失败。因此,在Spring初始化过程中,需要维护父子上下文的对应关系。当子上下文发生变化,且变化范围涉及到BeanB时,需要再次更新子上下文中的依赖关系。当关联多个上下文时,需要维护多个上下文。环境,需要重新加载当前上下文环境条目。这里的入口是指:SpringMVCController、Mthrift和Pigeon,针对不同的流量入口采用不同的Reload策略。RPC框架入口主要操作是解绑注册中心,重新注册,重新加载启动进程等,对于SpringMVCController来说,主要是解绑和注册URLMapping,实现流量的变化和切换入学班。3.6SpringXML重载当用户修改/添加SpringXML时,需要重载XML中的所有bean。重新加载后,销毁后重启Spring。需要注意的是,XML的修改方式变化比较大,可能会涉及到全局AOP的配置和前后处理器相关的内容。影响范围是全局的,所以目前只发布添加普通的XMLBean标签。/修改,视情况逐步释放其他能力。3.7MyBatis热部署SpringMyBatis热部署的主要处理流程是在启动时获取所有Configuration路径,并维护它与SpringContext的对应关系,在热部署Class和XML时匹配Configuration,从而重新加载Configuration来实现热部署的目标。4小结4.1热部署功能概述上一章主要讲述了SpringBean、SpringMVC、MyBatis的重载过程。Sonic还支持其他常用的开发框架。丰富的框架支持和兼容性是Sonic的基石。下面列出其中的一些Sonic支持的常用第三方框架:到目前为止,Sonic已经支持大部分常用第三方框架的热加载,日常业务开发几乎不需要重启服务。并且美团内部的成功率已经达到99.9%以上,真正让热部署替代常规部署成为可能。4.2IDE插件集成Sonic还提供了强大的IDEA插件,让用户沉浸式开发,远程热部署变得更加方便。4.3推广使用截至发稿,美团Sonic用户数3000+,应用项目数2000+。该项目还获得了2020年下半年美团到家研发平台“最佳效率团队”奖。5作者简介凯歌、展峰、李涵、龚艳、程晓、宇龙等均来自美团/到家研发平台。6篇参考文章[1]基于Javassist和Javaagent实现动态切面[2]SpringMVC源码分析[3]SpringIOC源码分析[4]MyBatis源码分析[5]SpringBoot源码分析[6]SpringAOP源码分析[7]SpringTransaction源码分析[8]Cglib源码分析[9]JDKProxy源码分析[10]Dcevm简介[11]字节码增强技术探索[12]JavassistAPI阅读更多技术文章合集美团技术团队前端|算法|Backend|Data|Security|O&M|iOS|Android|Test|在公众号菜单栏对话框回复【2021货】、【2020货】、【2019货】、【2018货】、【2017货】等box关键字,可以查看美团技术团队历年技术文章合集。|本文由美团技术团队制作,版权归美团所有。欢迎转载或将本文内容用于分享、交流等非商业用途,转载请注明“内容由美团技术团队转载”。本文未经许可不得转载或用于商业用途。任何商业活动,请发邮件至tech@meituan.com申请授权。