1. 程式人生 > 其它 >JVM SandBox 的技術原理與應用分析

JVM SandBox 的技術原理與應用分析

https://www.infoq.cn/article/tsy4lgjvsfweuxebw*gp

https://blog.csdn.net/qq_40378034/article/details/116255652

一、前言

在開始之前,我們先來模擬一下以下的場景:

小李:“小明,你的介面沒有返回資料,麻煩幫忙看一下?”

小明:“我這邊的資料也是從別人的伺服器中拿到的,但是我不確定是因為邏輯處理有問題導致沒有結果,還是因為我依賴的服務有問題而沒有返回結果,我需要確認一下。”

小明:“哎呀,線上沒有日誌,我需要加個日誌上個線。”

30 分鐘之後……

小明:“不好意思,日誌加錯地方了……稍等……”

接來下隆重登場的就是本文的主角 JVM SandBox 了。基於 JVM SandBox,我們可以很容易地做到在不重新部署應用的情況下,給指定的某些類的某些方法加上日誌功能。當然,動態加日誌僅僅是 JVM SandBox 可以應用的一個小小的場景,JVM SandBox 的威力遠不在於此。那麼,JVM SandBox 是什麼?JVM SandBox 從哪裡來?JVM SandBox 怎麼用?本文在第二章會回答這幾個問題,如果你跟我一樣對 JVM SandBox 的底層實現原理感興趣,特別是 JVM 相關部分,那麼第三章有相關的內容;如果你只想瞭解 JVM SandBox 自身具有哪些特性,以及 JVM SandBox 是如何設計實現的,那麼可以跳過第三章,直接閱讀第四章;最後,在第五章會簡單地介紹其他兩個可以應用 JVM SandBox 的場景。

二、JVM SandBox 簡介

2.1 AOP

在介紹 JVM SandBox 之前,我們先來回顧一下 AOP 技術。

AOP(面向切面程式設計,Aspect Oriented Programming)技術已被業界廣泛應用,其思想是面向業務處理過程的某個步驟或階段進行程式設計,這個步驟或階段被稱為切面,其目的是降低業務邏輯的各部分之間的耦合,常見的 AOP 實現基本原理有兩種:代理和行為注入。

1)代理模式

在代理模式下,我們會建立一個代理物件來代理原物件的行為,代理物件擁有原物件行為執行的控制權,在這種模式下,我們基於代理物件在原物件行為執行的前後插入程式碼來實現 AOP。

圖 2-1 代理模式

2)行為注入模式

在行為注入模式下,我們不會建立一個新的物件,而是修改原物件,在原物件行為的執行前後注入程式碼來實現 AOP。

圖 2-2 行為注入模式

2.2 JVM SandBox

JVM SandBox 是阿里開源的一款 JVM 平臺非侵入式執行期 AOP 解決方案,本質上是一種 AOP 落地形式。那麼可能有同學會問:已有成熟的 Spring AOP 解決方案,阿里巴巴為什麼還要“重複造輪子”?這個問題要回到 JVM SandBox 誕生的背景中來回答。在 2016 年中,天貓雙十一催動了阿里巴巴內部大量業務系統的改動,恰逢徐冬晨(阿里巴巴測試開發專家)所在的團隊調整,測試資源保障嚴重不足,迫使他們必須考慮更精準、更便捷的老業務測試迴歸驗證方案。開發團隊面臨的是新接手的老系統,老的業務程式碼架構難以滿足可測性的要求,很多現有測試框架也無法應用到老的業務系統架構中,於是需要新的測試思路和測試框架。

為什麼不採用 Spring AOP 方案呢?Spring AOP 方案的痛點在於不是所有業務程式碼都託管在 Spring 容器中,而且更底層的中介軟體程式碼、三方包程式碼無法納入到迴歸測試範圍,更糟糕的是測試框架會引入自身所依賴的類庫,經常與業務程式碼的類庫產生衝突,因此,JVM SandBox 應運而生。

JVM SandBox 本身是基於外掛化的設計思想,允許用於以“模組”的方式基於 JVM SandBox 提供的 AOP 能力開發新的功能。基於 JVM SandBox,我們不需要關心如何在 JVM 層實現 AOP 的技術細節,只需要通過 JVM SandBox 提供的程式設計結構告訴“沙箱”,我們希望對哪些類哪些方法進行 AOP,在切面點做什麼即可,JVM SandBox 模組功能編寫起來非常簡單。下面是一個示例模組程式碼:

 1 @MetaInfServices(Module.class)  
 2 @Information(id = "my-sandbox-module")//模組名  
 3 public class MySandBoxModule implements Module {  
 4     private Logger LOG = Logger.getLogger(MySandBoxModule.class.getName());  
 5     @Resource  
 6     private ModuleEventWatcher moduleEventWatcher;  
 7   
 8     @Command("addLog")//模組命令名  
 9     public void addLog() {  
10         new EventWatchBuilder(moduleEventWatcher)  
11                 .onClass("com.float.lu.DealGroupService")//想要對DealGroupService這個類進行切面  
12                 .onBehavior("loadDealGroup")//想要對上面類的loadDealGroup方法進行切面  
13                 .onWatch(new AdviceListener() {  
14                     @Override  
15                     protected void before(Advice advice) throws Throwable {  
16                         LOG.info("方法名: " + advice.getBehavior().getName());//在方法執行前列印方法的名字  
17                     }  
18                 });  
19     }  
20 }  

如上面程式碼所示,通過簡單常規的編碼即可實現對某個類的某個方法進行切面,不需要對底層技術有了解即可上手。上面的模組被 JVM SandBox 載入和初始化之後便可以被使用了。比如,只需要告訴 JVM SandBox 我們要執行 my-sandbox-module 這個模組的 addLog 這個方法,我們編寫的功能的呼叫就會被注入到目標地方。

JVM SandBox 使用起來非常很簡單,但是 JVM SandBox 背後所涉及到的底層技術原理、實現細節卻不簡單,比如 Java Agent、Attach、JVMTI、Instrument、Class 位元組碼修改、ClassLoader、程式碼鎖、事件驅動設計等等。如果要深究可能要究幾本書,但這不是本文的目的。本文僅僅概括性地介紹 JVM SandBox 實現涉及到的一些核心技術點,力求通過本文可以回答如 JVMTI 是什麼?Instrument 是什麼?Java Agent 是什麼?它們之間有什麼關係?他們和 JVM SandBox 又是什麼關係等問題。

三、JVM 核心技術

3.1 Java Agent

JVM SandBox 容器的啟動依賴 Java Agent,Java Agent(Java 代理)是 JDK 1.5 之後引入的技術。開發一個 Java Agent 有兩種方式,一種是實現一個 premain 方法,但是這種方式實現的 Java Agent 只能在 JVM 啟動的時候被載入;另一種是實現一個 agentmain 方法,這種方式實現的 Java Agent 可以在 JVM 啟動之後被載入。當然,兩種實現方法各有利弊、各有適用場景,這裡不再過多介紹,JVM SandBox Agent 對於這兩種方式都有實現,使用者可以自行選擇使用,因為在 JVM 層這兩種方式底層的實現原理大同小異,因此本文只選擇 agentmain 方式進行介紹,下文的脈絡也僅跟 agentmain 方式相關。下面先通過兩行程式碼,來看看基於 agentmain 方式實現的 Java Agent 是如何被載入的:

1 VirtualMachine vmObj = VirtualMachine.attach(targetJvmPid);//targetJvmPid為目標JVM的程序ID  
2 vmObj.loadAgent(agentJarPath, cfg);  // agentJarPath為agent jar包的路徑,cfg為傳遞給agent的引數  

在 Java Agent 被載入之後,JVM 會呼叫 Java Agent JAR 包中的 MANIFEST.MF 檔案中的 Agent-Class 引數指定的類中的 agentmain 方法。下面兩節會對這兩行程式碼的背後 JVM 實現技術進行探究。

3.2 Attach

1)Attach 工作機制

上面一節中第一行程式碼的背後,有一個重要的 JVM 支撐機制——Attach,為什麼說重要?比如大家最熟悉的 jstack 就是要依賴這個機制來工作,那麼,Attach 機制是什麼呢?我們先來看看 Attach 機制都做了什麼事兒。首先,Attach 機制對外提供了一種程序間的通訊能力,能讓一個程序傳遞命令給 JVM;其次,Attach 機制內建一些重要功能,可供外部程序呼叫。比如剛剛提到的 jstack,再比如上一節中提到的第二行程式碼:vmObj.loadAgent(agentJarPath, cfg); 這行程式碼實際上就是告訴 JVM 我們希望執行 load 命令,下面的程式碼片段可以更直觀地看到 load 命令對應的行為是:JvmtiExport::load_agent_library,這行程式碼的行為是對 agentJarPath 指定的 Java Agent 進行載入:

//來源:attachListener.cpp  
static AttachOperationFunctionInfo funcs[] = {  
  { "agentProperties",  get_agent_properties },  
  { "datadump",         data_dump },  
  { "dumpheap",         dump_heap },  
  { "load",             JvmtiExport::load_agent_library },  
  { "properties",       get_system_properties },  
  { "threaddump",       thread_dump },  
  { "inspectheap",      heap_inspection },  
  { "setflag",          set_flag },  
  { "printflag",        print_flag },  
  { "jcmd",             jcmd },  
  { NULL,               NULL }  
};  

那麼,JVM Attach 機制是如何工作的呢?Attach 機制的核心元件是 Attach Listener,顧名思義,Attach Listener 是 JVM 內部的一個執行緒,這個執行緒的主要工作是監聽和接收客戶端程序通過 Attach 提供的通訊機制發起的命令,如下圖所示:

圖 3-1 Attach Listener 工作機制

Attach Listener 執行緒的主要工作是串流程,流程步驟包括:接收客戶端命令、解析命令、查詢命令執行器、執行命令等等,下面附上相關程式碼片段:

片段一:AttachListener::init(啟動 AttachListener 執行緒):

//來源:attachListener.cpp  
{ MutexLocker mu(Threads_lock);  
    // 啟動執行緒  
    JavaThread* listener_thread = new JavaThread(&attach_listener_thread_entry);  
    // Check that thread and osthread were created  
    if (listener_thread == NULL || listener_thread->osthread() == NULL) {  
      vm_exit_during_initialization("java.lang.OutOfMemoryError",  
                                    "unable to create new native thread");  
    }  
    java_lang_Thread::set_thread(thread_oop(), listener_thread);  
    java_lang_Thread::set_daemon(thread_oop());  
  
    listener_thread->set_threadObj(thread_oop());  
    Threads::add(listener_thread);  
    Thread::start(listener_thread);  
  }  

片段二:attach_listener_thread_entry(輪詢佇列):

 1 //來源:attachListener.cpp  
 2 static void attach_listener_thread_entry(JavaThread* thread, TRAPS) {  
 3   os::set_priority(thread, NearMaxPriority);  
 4   
 5   thread->record_stack_base_and_size();  
 6   
 7   if (AttachListener::pd_init() != 0) {  
 8     return;  
 9   }  
10   AttachListener::set_initialized();  
11   for (;;) {  
12     AttachOperation* op = AttachListener::dequeue();// 展開  
13     if (op == NULL) {  
14       return;   // dequeue failed or shutdown  
15     }  

片段三:dequeue(讀取客戶端 socket 內容)

//來源:attachListener_bsd.cpp  
BsdAttachOperation* BsdAttachListener::dequeue() {  
  for (;;) {  
    int s;  
    // wait for client to connect  
    struct sockaddr addr;  
    socklen_t len = sizeof(addr);  
    RESTARTABLE(::accept(listener(), &addr, &len), s);  
    if (s == -1) {  
      return NULL;      // log a warning?  
    }  
    // 省略……  
    // peer credential look okay so we read the request  
    BsdAttachOperation* op = read_request(s);  
  }  
}  

2)載入 Agent

回到上層,我們再看看 vmObj.loadAgent(agentJarPath, cfg);這行 Java 程式碼程式碼是如何工作的?其實,這行程式碼背後主要做了一件事情:告訴 Attach 載入 instrument 庫,instrument 庫又是什麼?instrument 庫是基於 JVMTI 程式設計介面編寫的一個 JVMTI Agent,其表現形式是一個動態連結庫,下面上兩個程式碼片段:

//來源:HotSpotVirtualMachine.java  
//片段1  
loadAgentLibrary("instrument", args);  
//片段2   
InputStream in = execute("load",  
                                 agentLibrary,  
                                 isAbsolute ? "true" : "false",  
                                 options);  

Attach 接收到命令之後執行 load_agent_library 方法,主要做兩件事情:1)載入 instrument 動態庫;2)找到 instrument 動態庫中實現的 Agent_OnAttach 方法並呼叫。Attach 的工作到這裡就結束了,至於 Agent_OnAttach 這個方法做了什麼事情,我們會在 JVMTI 部分進行介紹。下面先解釋 Attach 相關的另外一個問題,Attach Listener 並不是在 JVM 啟動的時候被啟動的,而是基於一種懶啟動策略實現。

3)Attach Listener 懶啟動

為方便理解下面引入程式碼片段,這是從 JVM 啟動路徑上擷取的兩片程式碼:

//來源:thread.cpp  
// 片段1  
  os::signal_init();  
  if (!DisableAttachMechanism) {  
    AttachListener::vm_start();  
    if (StartAttachListener || AttachListener::init_at_startup()) {  
      AttachListener::init();  
    }  
  }  
// 片段2  
bool AttachListener::init_at_startup() {  
  if (ReduceSignalUsage) {  
    return true;  
  } else {  
    return false;  
  }  
}  

DisableAttachMechanism 這個引數預設是關閉的,也就是說 JVM 預設情況下啟用 Attach 機制,但是 StartAttachListener 和 ReduceSignalUsage 這兩個引數預設都是關閉的,因此 Attach Listener 執行緒預設並不會被初始化。那麼 Attach Listener 執行緒是在什麼時候被初始化的呢?這就有必要了解一下 Signal Dispatcher 元件了,Signal Dispatcher 本質上也是 JVM 提供的一種程序間通訊機制,只是這種機制是基於訊號量來實現的。

我們先從 Signal Dispatcher 的服務端角度,來看看 Signal Dispatcher 是如何工作的,不知道大家有沒有注意到上面的 os::signal_init();這麼一行程式碼,其作用是初始化和啟動 Signal Dispatcher 執行緒,Signal Dispatcher 執行緒啟動之後就會進入等待訊號狀態(os::signal_wait)。如下程式碼片段所示,SIGBREAK 訊號是 SIGQUIT 訊號的別名,Signal Dispatcher 接收到這個訊號之後會呼叫 AttachListener 的 is_init_trigger 的方法初始化和啟動 AttachListener 執行緒,同時會在 tmp 目錄下面建立/tmp/.attach_pid${pid}這樣的一個檔案,代表程序號為 pid 的 JVM 已經初始化了 AttachListener 元件了。

片段一:os::signal_init();(啟動 Signal Dispatcher 執行緒)

//來源:os.cpp  
{ MutexLocker mu(Threads_lock);  
      JavaThread* signal_thread = new JavaThread(&signal_thread_entry);//展開  
      if (signal_thread == NULL || signal_thread->osthread() == NULL) {  
        vm_exit_during_initialization("java.lang.OutOfMemoryError",  
                                      "unable to create new native thread");  
      }  
      java_lang_Thread::set_thread(thread_oop(), signal_thread);  
      java_lang_Thread::set_priority(thread_oop(), NearMaxPriority);  
      java_lang_Thread::set_daemon(thread_oop());  
  
      signal_thread->set_threadObj(thread_oop());  
      Threads::add(signal_thread);  
      Thread::start(signal_thread);  
    }  

片段二:signal_thread_entry(監聽訊號)

 1 //來源:os.cpp  
 2 static void signal_thread_entry(JavaThread* thread, TRAPS) {  
 3   os::set_priority(thread, NearMaxPriority);  
 4   while (true) {  
 5     int sig;  
 6     {  
 7       sig = os::signal_wait();  
 8     }  
 9     switch (sig) {  
10       case SIGBREAK: {  
11         // Check if the signal is a trigger to start the Attach Listener - in that  
12         // case don't print stack traces.  
13         if (!DisableAttachMechanism && AttachListener::is_init_trigger()) {//展開  
14           continue;  
15         }  

片段三:is_init_trigger(啟動 AttachListener)

//來源:attachListener_bsd.cpp  
bool AttachListener::is_init_trigger() {  
  char path[PATH_MAX + 1];  
  int ret;  
  struct stat st;  
  snprintf(path, PATH_MAX + 1, "%s/.attach_pid%d",os::get_temp_directory(), os::current_process_id());  
  RESTARTABLE(::stat(path, &st), ret);  
  if (ret == 0) {  
    if (st.st_uid == geteuid()) {  
      init();//初始化Attach Listener  
      return true;  
    }  
  }  
  return false;  
}  

我們再從客戶端角度,來看看客戶端是如何通過 Signal Dispatcher 來啟動 AttachListener 執行緒的,這要又要回到 VirtualMachine.attach(pid)這行程式碼,這行程式碼的背後會執行具體 VirtualMachine 的初始化工作,我們拿 Linux 平臺下的 LinuxVirtualMachine 實現來看,下面是 LinuxVirtualMachine 初始化的核心程式碼:

//來源:LinuxVirtualMachine.java  
//檢查目標JVM對否存在標識檔案  
path = findSocketFile(pid);  
if (path == null) {  
  File f = createAttachFile(pid);  
  try {  
    mpid = getLinuxThreadsManager(pid);  
    sendQuitToChildrenOf(mpid);  

上面提到目標 JVM 一旦啟動 attach 元件之後,會在/tmp 目錄下建立名為.java_pid${pid}的檔案。因此,客戶端在每次初始化 LinuxVirtualMachine 物件的時候,會先檢視目標 JVM 的這個檔案是否存在,如果不存在則需要通過 SIGQUIT 訊號來將 attach 元件拉起來。具體操作是進入 try 區域後,找到指定 pid 程序的父程序(Linux 平臺下執行緒是通過程序實現的),給父程序的所有子程序都發送一個 SIGQUIT 訊號,而 Signal Dispatcher 元件恰好在監聽這個訊號。

3.3 JVMTI

JVMTI(Java Virtual Machine Tool Interface)是一套由 Java 虛擬機器提供的,為 JVM 相關的工具提供的本地程式設計介面集合。JVMTI 是從 Java SE 5 開始引入,整合和取代了以前使用的 Java Virtual Machine Profiler Interface (JVMPI) 和 the Java Virtual Machine Debug Interface (JVMDI),而在 Java SE 6 中,JVMPI 和 JVMDI 已經消失了。JVMTI 提供了一套“代理”程式機制,可以支援第三方工具程式以代理的方式連線和訪問 JVM,並利用 JVMTI 提供的豐富的程式設計介面,完成很多跟 JVM 相關的功能。JVMTI 的功能非常豐富,包括虛擬機器中執行緒、記憶體/堆/棧,類/方法/變數,事件/定時器處理等等。使用 JVMTI 一個基本的方式就是設定回撥函式,在某些事件發生的時候觸發並作出相應的動作,這些事件包括虛擬機器初始化、開始執行、結束,類的載入,方法出入,執行緒始末等等。如果想對這些事件進行處理,需要首先為該事件寫一個函式,然後在 jvmtiEventCallbacks 這個結構中指定相應的函式指標。

上面提到的 Instrument 就是一個基於 JVMTI 介面的,以代理方式連線和訪問 JVM 的一個 Agent,Instrument 庫被載入之後 JVM 會呼叫其 Agent_OnAttach 方法,如下程式碼片段:

//來源:InvocationAdapter.c  
//片段1:建立Instrument物件  
success = createInstrumentationImpl(jni_env, agent);  
//片段2:監聽ClassFileLoadHook事件並設定回撥函式為eventHandlerClassFileLoadHook  
callbacks.ClassFileLoadHook = &eventHandlerClassFileLoadHook;  
jvmtierror = (*jvmtienv)->SetEventCallbacks(jvmtienv, &callbacks, sizeof(callbacks));  
//片段3:呼叫java類的agentmain方法  
success = startJavaAgent(agent, jni_env, agentClass, options, agent->mAgentmainCaller); 

Agent_OnAttach 方法被呼叫的時候主要做了幾件事情:1)建立 Instrument 物件,這個物件就是 Java Agent 中通過 agentmain 方法拿到的 Instrument 物件;2)通過 JVMTI 監聽 JVM 的 ClassFileLoadHook 事件並設定回撥函式 eventHandlerClassFileLoadHook;3)呼叫 Java Agent 的 agentmain 方法,並將第 1)步建立的 Instrument 物件傳入。通過上面的內容可以知道,在 JVM 進行類載入的都會回撥 eventHandlerClassFileLoadHook 方法,我們可以猜到 eventHandlerClassFileLoadHook 方法做的事情就是呼叫 Java Agent 內部傳入的 Instrument 的 ClassFileTransformer 的實現:

//來源Instrumentation.java  
void addTransformer(ClassFileTransformer transformer);  

通過 JVMTI 的事件回撥機制,Instrument 可以捕捉到每個類的載入事件,從而呼叫使用者實現的 ClassFileTransformer 來對類進行轉換,那麼已經被載入的類怎麼辦呢?為解決這個問題,Instrument 提供了 retransformClasses 介面用於對已經載入的類進行轉換:

//來源Instrumentation.java  
void retransformClasses(Class<?>... classes) throws UnmodifiableClassException;

Instrument 底層的實現實際上也是呼叫 JVMTI 提供的 RetransformClasses 介面,RetransformClasses 實現對已經載入的類進行重新定義(redefine),而重新定義類也會觸發 ClassFileLoadHook 事件,Instrument 同樣會監聽到這個事件並對被載入的類進行處理。到這裡,JVM SandBox 底層依賴 JVM 的核心機制已經介紹完了,下面通過一張時序圖將一個 JavaAgent 的載入過程涉及到的相關元件及行為串起來:

圖 3-2 Java Agent 載入流程

四、JVM SandBox 設計與實現

4.1 可插拔

本文理解的 JVM SandBox 可插拔至少有兩層含義:一層是 JVM 沙箱本身是可以被插拔的,可被動態地掛載到指定 JVM 程序上和可以被動態地解除安裝;另一層是 JVM 沙箱內部的模組是可以被插拔的,在沙箱啟動期間,被載入的模組可以被動態地啟用和解除安裝。

一個典型的沙箱使用流程如下:

$./sandbox.sh -p 33342 #將沙箱掛載到程序號為33342的JVM程序上  
$./sandbox.sh -p 33342 -d 'my-sandbox-module/addLog' #執行指定模組, 模組功能生效  
$./sandbox.sh -p 33342 -S #解除安裝沙箱  

JVM 沙箱可以被動態地掛載到某個正在執行的目標 JVM 程序之上(前提是目標 JVM 沒有禁止 attach 功能),沙箱工作完之後還可以被動態地從目標 JVM 程序解除安裝掉,沙箱被解除安裝之後,沙箱對對目標 JVM 程序產生的影響會隨即消失(這是沙箱的一個重要特性),沙箱工作示意圖如下:

圖 4-1 沙箱工作示意圖

客戶端通過 Attach 將沙箱掛載到目標 JVM 程序上,沙箱的啟動實際上是依賴 Java Agent,上文已經介紹過,啟動之後沙箱會一直維護著 Instrument 物件引用,在沙箱中 Instrument 物件是一個非常重要的角色,它是沙箱訪問和操作 JVM 的唯一通道,後續修改位元組碼和重定義類都要經過 Instrument。另外,沙箱啟動之後同時會啟動一個內部的 Jetty 伺服器,這個伺服器用於外部程序和沙箱進行通訊,上面看到的./sandbox.sh -p 33342 -d ‘my-sandbox-module/addLog’ 這行程式碼,實際上就是通過 HTTP 協議來告訴沙箱執行 my-sanbox-module 這個模組的 addLog 這個功能的。

4.2 無侵入

沙箱內部定義了一個 Spy 類,該類被稱為“間諜類”,所有的沙箱模組功能都會通過這個間諜類驅動執行。下面給出一張示意圖將業務程式碼、間諜類和模組程式碼串起來來幫助理解:

圖 4-2 沙箱無侵入核心實現

上圖是沙箱 AOP 核心實現的虛擬碼,實際實現會比上圖更復雜一些,沙箱內部通過修改和重定義業務類來實現上述功能的。在介面設計方面,沙箱通過事件驅動的方式,讓模組開發者可以監聽到方法執行的某個事件並設定回撥邏輯,這一切都可以通過實現 AdviceListener 介面來做到,通過 AdviceListener 介面定義的行為,我們可以瞭解沙箱支援的監聽事件如下:

4.3 隔離

JVM 沙箱有自己的工作程式碼類,而這些程式碼類在沙箱被掛在到目標 JVM 之後,其涉及到的相關功能實現類都要被載入到目標 JVM 中,沙箱程式碼和業務程式碼共享 JVM 程序,這裡有兩個問題:1)如何避免沙箱程式碼和業務程式碼之間產生衝突;2)如何避免不同沙箱模組之間的程式碼產生衝突。為解決這兩個問題,JVM SandBox 定義了自己的類載入器,嚴格控制類的載入,沙箱的核心類載入器有兩個:SandBoxClassLoader 和 ModuleJarClassLoader。SandBoxClassLoader 用於載入沙箱自身的工作類,ModuleJarClassLoader 用於載入三方自己開發的模組功能類,如上面的 MySandBoxModule 類。在沙箱中類載入器繼承關係如下圖所示:

圖 4-3 沙箱類載入器繼承體系

通過類載入器,沙箱將沙箱程式碼和業務程式碼以及不同沙箱模組之間的程式碼隔離開來。

4.4 多租戶

JVM 沙箱提供的隔離機制也有兩層含義,一層是沙箱容器和業務程式碼之間隔離以及沙箱內部模組之間隔離;另一層是不同使用者的沙箱之間的隔離,這一層隔離用來支援多租戶特性,也就是支援多個使用者對同一個 JVM 同時使用沙箱功能且他們之間互不影響。沙箱的這種機制是通過支援建立多個 SandBoxClassLoader 的方式來實現的,每個 SandBoxClassLoader 關聯唯一一個名稱空間(namespace)用於標識不同的使用者,示意圖如下所示:

圖 4-4 多租戶實現示意圖

五、JVM Sandbox 應用場景分析

JVM SandBox 讓動態無侵入地對業務程式碼進行 AOP 這個事情實現起來非常容易,但是這個事情做起來非常容易只是前提條件,更重要的是我們基於 JVM SandBox 能做什麼?可以做的很多,比如:故障模擬、動態黑名單,動態日誌、動態開關、系統流控、熱修復,方法請求錄製和結果回放、動態去依賴、依賴超時時間動態修改、甚至是修改 JDK 基礎類的功能等等,當然不限於此,這裡大家可以開啟腦洞,天馬行空地思考一下,下面再給出兩個 JVM SandBox 應用場景的實現思路。

5.1 故障模擬

我們可以開發一個沙箱模組,通過和前臺頁面的互動,我們可以對任意業務類的任意方法注入故障來達到故障模擬的效果,使用者互動示意圖如下:

圖 5-1 故障模擬互動示意圖

使用者通過簡單的介面操作即可完成故障注入,應用程式碼不需要提前埋點。

5.2 動態黑名單

我們還可以開發一個沙箱模組實現 IP 黑名單功能,針對指定 IP 的客戶端,服務直接返回空結果,使用者互動示意圖如下:

圖 5-2 動態黑名單互動示意圖

引用 JVM SandBox 官網的一句話:“JVM-SANDBOX 還能幫助你做很多很多,取決於你的腦洞有多大了。”

總結

JVM SandBox 是一種無侵入,可動態插拔,JVM 層的 AOP 解決方案,基於 JVM SandBox 我們可以很容易地開發出很多有意思的工具,這完全歸功於 JVM SandBox 為我們遮蔽了底層技術細節和實現複雜性。JVM SandBox 很強大,這裡需要感謝 JVM SandBox 的作者。除了無侵入,可動態插拔這兩個優勢之外,JVM SandBox 在 JVM 層支援 AOP 這件事情本身就是一個絕對優勢,因為我們開發的 AOP 能力不再依賴應用層所使用的容器,比如不管你使用的是 Spring 容器還是 Plexus 容器,不管你的 Web 容器是 Tomcat 還是 Jetty、統統都沒有關係。

回顧一下本文的內容:

  • 回顧 AOP 技術;

  • 介紹 JVM SandBox 是什麼、來自哪裡、怎麼用;

  • 通過 Java Agent 的載入介紹涉及到的 JVM 相關核心技術如:Attach 機制、JVMTI、Instrument 等;

  • 介紹 JVM SandBox 的核心特性的設計與實現如:可插拔、無侵入、隔離、多租戶;

  • 介紹 JVM SandBox 可被應用的場景以及兩個小例子。

參考文件

【1】http://developer.51cto.com/art/201803/568224.htm

【2】https://github.com/alibaba/jvm-sandbox

【3】https://www.jianshu.com/p/b72f66da679f

作者簡介

陸晨,就職於美團點評,從事到店事業群業務後端研發工作,平時關注後端服務架構、領域驅動設計、海量運營等方面技術,個人對計算機基礎技術興趣較為濃厚,喜歡探究底層技術原理和同時關注技術應用價值。