Java系列 | 遠端熱部署在美團的落地實踐

美團技術團隊發表於2022-03-28
Sonic是美團內部研發設計的一款用於熱部署的IDEA外掛,本文其實現原理及落地的一些技術細節。在閱讀本文之前,建議大家先熟悉一下Spring原始碼Spring MVC 原始碼Spring Boot原始碼Agent位元組碼增強JavassistClassloader等相關知識。

1 前言

1.1 什麼是熱部署

所謂熱部署,就是在應用正在執行時升級軟體,卻不需要重新啟動應用。對於Java應用程式來說,熱部署就是在執行時更新Java類檔案,同時觸發Spring以及其他常用第三方框架的一系列重新載入的過程。在這個過程中不需要重新啟動,並且修改的程式碼實時生效,好比是戰鬥機在空中完成加油,不需要戰鬥機熄火降落,一系列操作都在“執行”狀態來完成。

1.2 為什麼我們需要熱部署

據瞭解,美團內部很多工程師每天本地重啟服務高達5~12次,單次大概3~8分鐘,每天向Cargo(美團內部測試環境管理工具)部署3~5次,單次時長20~45分鐘,部署頻繁頻次高、耗時長,嚴重影響了系統上線的效率。而外掛提供的本地和遠端熱部署功能,可讓將程式碼變更“秒級”生效。一般而言,開發者日常工作主要分為開發自測和聯調兩個場景,下面將分別介紹熱部署在每個場景中發揮的作用。

圖 1

1.2.1 開發自測場景

一般來講,在用外掛之前,開發者修改完程式碼還需等待3~8分鐘啟動時間,然後手動構造請求或協調上游發請求,耗時且費力。在使用完熱部署外掛後,修改完程式碼可以一鍵增量部署,讓變更“秒級”生效,能夠做到快速自測。而對於那些無法本地啟動專案,也可以通過遠端熱部署功能使程式碼變更“秒級”生效。

圖 2

1.2.2 聯調場景

通常情況下,在使用外掛之前,開發者修改程式碼經過20~35分鐘的漫長部署,需要聯絡上游聯調開發者發起請求,一直要等到遠端伺服器檢視日誌,才能確認程式碼生效。在使用熱部署外掛之後,開發者修改程式碼遠端熱部署能夠秒級(2~10s)生效,開發者直接發起服務呼叫,可以節省大量的碎片化時間(熱部署外掛還具備流量回放、遠端呼叫、遠端反編譯等功能,可配合進行使用)。

圖 3

所以,熱部署外掛希望解決的痛點是:在可控的條件內,幫助開發者減少頻繁編譯部署的次數,節省碎片化的時間。最終為開發者每天節約出一定量的編碼時間

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正是在這種背景下應運而生。

圖 4

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面臨的艱鉅挑戰之一。我們從主動溝通、零成本(或極低成本)快速接入、自動化指令碼,以及產品自動診斷、收集反饋等方向出發,踐行出了四條原則。

圖 6

2 整體設計方案

2.1 Sonic結構

Sonic外掛由4大部分組成,包括指令碼端、外掛端、Agent端,以及Sonic服務端。指令碼端負責自動化構建Sonic啟動引數、服務啟動等整合工作;IDEA外掛端整合環境為開發者提供更便捷的熱部署服務;Agent端隨專案啟動負責熱部署的功能實現;服務端則負責收集熱部署資訊、失敗上報等統計工作。如下圖所示:

圖 7

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過程

圖 8

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在執行期間各個組成部分的工作職責,由它們形成一整套完備的技術產品落地閉環方案:

圖 9

3.2 Sonic功能流轉

Sonic通過NIO監聽本地檔案變更,觸發檔案變更事件,例如Class新增、Class修改、Spring Bean過載等事件流程。下圖展示了一次熱部署單個檔案的生命週期:

圖 10

3.3 檔案監聽

Sonic首先會在本地和遠端預定義兩個目錄,/var/tmp/sonic/extraClasspath/var/tmp/sonic/classes。extraClasspath為Sonic自定義的擴充Classpath URL,classes為Sonic監聽的目錄,當有檔案變更時,通過IDEA外掛來部署到遠端/本地,觸發Agent的監聽目錄,來繼續下面的熱載入邏輯:

圖 11

為什麼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的核心監聽器,如下圖所示:

圖 12

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擴充。

圖 13

User ClassLoader是框架自定義的ClassLoader統稱,例如Jetty專案是WebAppclassLoader。其中Urlclasspath為當前專案的lib檔案件下,例如Spring Boot專案也是從當前專案BOOT-INF/lib/路徑中載入CLass等等,不同框架的自定義位置稍有不同。所以針對此類情況,Agent必須拿到使用者的自定義Classloader,如果是常規方式啟動的,比如普通Spring XML專案,藉助Plus(美團內部服務釋出平臺)釋出,此類沒有自定義Classloader,是預設AppClassLoader,所以Agent在使用者專案啟動過程中,藉助位元組碼增強的方式來獲取到真正的使用者Classloader。

圖 14

找到使用者使用的子Classloader之後,通過反射的方式來獲取Classloader中的元素Classpath,其中ClassPath中的URL就是當前專案載入Class時需要的所有執行時Class環境,並且包括三方的JAR包依賴等。

Sonic獲取到URL陣列,把Sonic自定義的擴充Classpath目錄加入到URL陣列首位,這樣當有新增Class時,Sonic只需要將Class檔案複製到擴充Classpath對應的包目錄下面即可,當有其他Bean依賴新增的Class時,會從當前目錄下面查詢類檔案。

為什麼不直接對Appclassloader進行加強?而是對框架的自定義Classloader進行加強?

圖 15

考慮這樣一個場景,框架自定義類載入器中有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的銷燬和重啟流程,主要內容如下圖展示:

圖 16

首先當修改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進行過載。

圖 17

重新Reload之後,將Spring銷燬後重啟。需要注意的是:XML修改方式改動較大,可能涉及到全域性的AOP的配置以及前置和後置處理器相關的內容,影響範圍為全域性,所以目前只放開普通的XML Bean標籤的新增/修改,其他能力酌情逐步放開。

3.7 MyBatis 熱部署

Spring MyBatis熱部署的主要處理流程是在啟動期間獲取所有Configuration路徑,並維護它和Spring Context的對應關係,在熱部署Class、XML時去匹配Configuration,從而重新載入Configuration以達到熱部署的目的。

圖 18

4 總結

4.1 熱部署功能一覽

上一章節主要講述了Spring Bean、Spring MVC、MyBatis的過載流程,Sonic還支援其它常用的開發框架,豐富的框架支援和相容能力是Sonic的基石,下面列舉一些Sonic支援的常用的第三方框架:

圖19 美團內部框架以及常用開源框架

截止目前,Sonic已經支援絕大部分常用第三方框架的熱載入,常規業務開發幾乎無需重啟服務。並且在美團內部的成功率已經高達99.9%以上,真正地讓熱部署來代替常規部署構建成為一種可能。

4.2 IDE外掛整合

Sonic也提供了功能強大的IDEA外掛,讓使用者進行沉浸式開發,遠端熱部署也變得更加便利。

圖 20

4.3 推廣使用情況

截止到發稿時,Sonic在美團使用人數3000+,應用專案數量2000+。該專案還獲得了美團內部2020年下半年到家研發平臺“最佳效率團隊”獎。

5 作者簡介

凱哥、佔峰、李晗、龔炎、程驍、玉龍等,均來自美團/到家研發平臺。

6 參考文章

閱讀美團技術團隊更多技術文章合集

前端 | 演算法 | 後端 | 資料 | 安全 | 運維 | iOS | Android | 測試

| 在公眾號選單欄對話方塊回覆【2021年貨】、【2020年貨】、【2019年貨】、【2018年貨】、【2017年貨】等關鍵詞,可檢視美團技術團隊歷年技術文章合集。

| 本文系美團技術團隊出品,著作權歸屬美團。歡迎出於分享和交流等非商業目的轉載或使用本文內容,敬請註明“內容轉載自美團技術團隊”。本文未經許可,不得進行商業性轉載或者使用。任何商用行為,請傳送郵件至tech@meituan.com申請授權。

相關文章