关于java:Java系列-远程热部署在美团的落地实践

62次阅读

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

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 有哪些优劣势呢?上面咱们尝试给出几种产品的比照,仅供大家参考:

个性 JRebel Spring Boot DevTools IDEA 热加载 Tomcat 热加载 Spring Loader Sonic
近程 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 申请受权。

正文完
 0