1. 程式人生 > 實用技巧 >如果故障選擇了你……

如果故障選擇了你……

作者 | 葉飛、穹谷

導讀:總以為混沌工程離你很遠?但發生故障的那一刻不是由你來選擇的,而是那一刻來選擇你,你能做的就是為之做好準備。混沌工程在阿里內部已經應用多年,而ChaosBlade這個開源專案是阿里多年來通過注入故障來對抗故障的經驗結晶。為使大家更深入的瞭解其實現原理以及如何擴充套件自己所需要的元件故障注入,我們準備了一個系列對其做詳細技術剖析:架構篇、模型篇、協議篇、位元組碼篇、外掛篇以及實戰篇。

原文標題《技術剖析 Java 場景混沌工程實現系列(一)| 架構篇》

前言

在分散式系統架構下,服務間的依賴日益複雜,很難評估單個服務故障對整個系統的影響,並且請求鏈路長,監控告警的不完善導致發現問題、定位問題難度增大,同時業務和技術迭代快,如何持續保障系統的穩定性和高可用性受到很大的挑戰。

我們知道發生故障的那一刻不是由你來選擇的,而是那一刻來選擇你,你能做的就是為之做好準備。所以構建穩定性系統很重要的一環是混沌工程,在可控範圍或環境下,通過故障注入,來持續提升系統的穩定性和高可用能力。

ChaosBlade(Github 地址:https://github.com/chaosblade-io/chaosblade) 是一款遵循混沌工程實驗原理,提供豐富故障場景實現,幫助分散式系統提升容錯性和可恢復性的混沌工程工具,可實現底層故障的注入,特點是操作簡潔、無侵入、擴充套件性強。 其中 chaosblade-exec-jvm (Github 地址:https://github.com/chaosblade-io/chaosblade-exec-jvm

)專案實現了零成本對 Java 應用服務故障注入。其不僅支援主流的框架元件,如 Dubbo、Servlet、RocketMQ 等,還支援指定任意類和方法注入延遲、異常以及通過編寫 Java 和 Groovy 指令碼來實現複雜的實驗場景。

為使大家更深入的瞭解其實現原理以及如何擴充套件自己所需要的元件故障注入,分為六篇文章對其做詳細技術剖析:架構篇、模型篇、協議篇、位元組碼篇、外掛篇以及實戰篇。本文將詳細介紹 chaosblade-exec-jvm 的整體架構設計,使使用者對 chaosblade-exec-jvm 有一定的瞭解。

系統設計

Chaosblade-exec-jvm 基於 JVM-Sanbox 做位元組碼修改,執行 ChaosBlade 工具可實現將故障注入的 Java Agent 掛載到指定的應用程序中。Java Agent 遵循混沌實驗模型設計,通過外掛的可拔插設計來擴充套件對不同 Java 元件的支援,可以很方便的擴充套件外掛來支援更多的故障場景,外掛基於 AOP 的設計定義通知Advice

、增強類Enhancer、切點PointCut,同時結合混沌實驗模型定模型ModelSpec、實驗靶點Target、匹配方式Matcher、攻擊動作Action

Chaosblade-exec-jvm 在由make build編譯打包時下載 JVM-Sanbox relase 包,編譯打包後 chaosblade-exec-jvm 做為 JVM-Sandbox 的模組。在載入 Agent 後,同時監聽 JVM-Sanbox 的事件來管理整個混沌實驗流程,通過Java Agent 技術來實現類的 transform 注入故障。

原理剖析

在日常後臺應用開發中,我們經常需要提供 API 介面給客戶端,而這些 API 介面不可避免的由於網路、系統負載等原因存在超時、異常等情況。使用 Java 語言時,HTTP 協議我們通常使用 Servlet 來提供 API 介面,chaosblade-exec-jvm 支援 Servlet 外掛,注入超時、自定義異常等故障能力。本篇將通過給 Servlet API 介面 注入延遲故障能力為例,分析 chaosblade-exec-jvm 故障注入的流程。

對 Servlet API 介面/topic延遲3秒,步驟如下:

// 掛載 Agent
blade prepare jvm --pid 888
{"code":200,"success":true,"result":"98e792c9a9a5dfea"}

// 注入故障能力
blade create servlet --requestpath=/topic delay --time=3000 --method=post
{"code":200,"success":true,"result":"52a27bafc252beee"}

// 撤銷故障能力
blade destroy 52a27bafc252beee

// 解除安裝 Agent
blade revoke 98e792c9a9a5dfea

1. 執行過程

以下通過 Servlet 請求延遲為例,詳細介紹故障注入的過程。

  1. ChaosBlade 下發掛載命令,掛載 Sandbox 到應用程序,啟用 Java Agent,例如blade p jvm --pid 888
  2. 掛載 Sandbox 後加載 chaosblade-exec-jvm 模組,載入外掛,如 ServletPlugin、DubboPlugin 等。
  3. 匹配 ServletPlugin 外掛的切點、註冊事件監聽,HttpServlet 的 doPost、doGet 方法。
  4. ChaosBlade 下發故障規則命令blade create servlet --requestpath=/topic delay --time=3000 --method=post
  5. 匹配故障規則,如 --requestpath=/topic ,訪問 http://127.0.0.1/topic 規則匹配成功。
  6. 匹配故障規則成功後,觸發故障,如延遲故障、自定義異常丟擲等。
  7. ChaosBlade 下發命令解除安裝 JavaAgent,如blade revoke 98e792c9a9a5dfea

2. 程式碼剖析

1)掛載 Agent

blade p jvm --pid 888

該命令下發後,將在目標 Java 應用程序掛在 Agent ,觸發 SandboxModule onLoad() 事件,初始化           PluginLifecycleListener 來管理外掛的生命週期,同時也觸發 SandboxModule onActive() 事件,載入部分外掛,載入外掛對應的 ModelSpec。

// Agent 載入事件
public void onLoad() throws Throwable {
  ManagerFactory.getListenerManager().setPluginLifecycleListener(this);
  dispatchService.load();
  ManagerFactory.load();
}
// ChaosBlade 模組啟用實現
public void onActive() throws Throwable {
  loadPlugins();
}

2)載入 Plugin

Plugin 載入時,建立事件監聽器 SandboxEnhancerFactory.createAfterEventListener(plugin) ,監聽器會監聽感興趣的事件,如 BeforeAdvice、AfterAdvice 等,具體實現如下:

// 載入外掛
public void add(PluginBean plugin) {
    PointCut pointCut = plugin.getPointCut();
    if (pointCut == null) {
        return;
    }
    String enhancerName = plugin.getEnhancer().getClass().getSimpleName();
    // 建立filter PointCut匹配
    Filter filter = SandboxEnhancerFactory.createFilter(enhancerName, pointCut);
   
    // 事件監聽
    int watcherId = moduleEventWatcher.watch(filter, SandboxEnhancerFactory.createBeforeEventListener(plugin), Event.Type.BEFORE);
    watchIds.put(PluginUtil.getIdentifier(plugin), watcherId);
}

3)匹配 PointCut

SandboxModule onActive() 事件觸發 Plugin 載入後,SandboxEnhancerFactory 建立 Filter,Filter 內部通過 PointCut 的 ClassMatcher 和 MethodMatcher 過濾。

public static Filter createFilter(final String enhancerClassName, final PointCut pointCut) {
  return new Filter() {
	@Override
	public boolean doClassFilter(int access, String javaClassName, String superClassTypeJavaClassName,
								 String[] interfaceTypeJavaClassNameArray,
								 String[] annotationTypeJavaClassNameArray
								) {
	  // ClassMatcher 匹配
	  ClassMatcher classMatcher = pointCut.getClassMatcher();
	  ...
	}

	@Override
	public boolean doMethodFilter(int access, String javaMethodName,
								  String[] parameterTypeJavaClassNameArray,
								  String[] throwsTypeJavaClassNameArray,
								  String[] annotationTypeJavaClassNameArray) {
	   // MethodMatcher 匹配
	  MethodMatcher methodMatcher = pointCut.getMethodMatcher();
	  ...
  };
}

4)觸發 Enhancer

如果已經載入外掛,此時目標應用匹配能匹配到 Filter 後,EventListener 已經可以被觸發,但是 chaosblade-exec-jvm 內部通過 StatusManager 管理狀態,所以故障能力不會被觸發。

例如 BeforeEventListener 觸發呼叫 BeforeEnhancer 的 beforeAdvice() 方法,在ManagerFactory.getStatusManager().expExists(targetName) 判斷時候被中斷,具體的實現如下:

public void beforeAdvice(String targetName, 
						 ClassLoader classLoader, 
						 String className,
						 Object object,
						 Method method, 
						 Object[] methodArguments) throws Exception {

  // 判斷實驗的狀態
  if (!ManagerFactory.getStatusManager().expExists(targetName)) {
	return;
  }
  EnhancerModel model = doBeforeAdvice(classLoader, className, object, method, methodArguments);
  if (model == null) {
	return;
  }
  ...
  // 注入階段
  Injector.inject(model);
}

5)建立混沌實驗

blade create servlet --requestpath=/topic delay --time=3000

該命令下發後,觸發 SandboxModule @Http("/create") 註解標記的方法,將事件分發給 com.alibaba.chaosblade.exec.service.handler.CreateHandler 處理

在判斷必要的 uid、target、action、model 引數後呼叫 handleInjection,handleInjection 通過狀態管理器註冊本次實驗,如果外掛型別是 PreCreateInjectionModelHandler 型別,將預處理一些東西。同是如果 Action 型別是  DirectlyInjectionAction,那麼將直接進行故障能力注入,且不需要走 Enhancer,如 JVM OOM 故障能力等。

public Response handle(Request request) {
  if (unloaded) {
	return Response.ofFailure(Code.ILLEGAL_STATE, "the agent is uninstalling");
  }
  // 檢查 suid,suid 是一次實驗的上下文ID
  String suid = request.getParam("suid");
  ...
  return handleInjection(suid, model, modelSpec);
}

private Response handleInjection(String suid, Model model, ModelSpec modelSpec) {
  RegisterResult result = this.statusManager.registerExp(suid, model);
  if (result.isSuccess()) {
	// 判斷是否預建立
	applyPreInjectionModelHandler(suid, modelSpec, model);
  }
}

ModelSpec

  • com.alibaba.chaosblade.exec.common.model.handler.PreCreateInjectionModelHandler預建立
  • com.alibaba.chaosblade.exec.common.model.handler.PreDestroyInjectionModelHandler預銷燬
private void applyPreInjectionModelHandler(String suid, ModelSpec modelSpec, Model model)
  throws ExperimentException {
  if (modelSpec instanceof PreCreateInjectionModelHandler) {
	((PreCreateInjectionModelHandler)modelSpec).preCreate(suid, model);
  }
}
...

DirectlyInjectionAction

如果 ModelSpec 是 PreCreateInjectionModelHandler 型別,且 ActionSpec 的型別是 DirectlyInjectionAction 型別,將直接進行故障能力注入,比如 JvmOom 故障能力,ActionSpec 的型別不是 DirectlyInjectionAction 型別,將載入外掛。

private Response handleInjection(String suid, Model model, ModelSpec modelSpec) {
    // 註冊
    RegisterResult result = this.statusManager.registerExp(suid, model);
    if (result.isSuccess()) {
        // handle injection
        try {
            applyPreInjectionModelHandler(suid, modelSpec, model);
        } catch (ExperimentException ex) {
            this.statusManager.removeExp(suid);
            return Response.ofFailure(Response.Code.SERVER_ERROR, ex.getMessage());
        }

        return Response.ofSuccess(model.toString());
    }
    return Response.ofFailure(Response.Code.DUPLICATE_INJECTION, "the experiment exists");
}

註冊成功後返回 uid,如果本階段直接進行故障能力注入了,或者自定義 Enhancer advice 返回 null,那麼後不通過Inject 類觸發故障。

6)注入故障能力

故障能力注入的方式,最終都是呼叫 ActionExecutor 執行故障能力。

  • 通過 Injector 注入;
  • DirectlyInjectionAction 直接注入,直接注入不進過 Inject 類呼叫階段,如果  JVM OOM 故障能力等。

DirectlyInjectionAction 直接注入不經過Enhancer引數包裝匹配直接到故障觸發 ActionExecutor 執行階段,如果是Injector 注入此時因為 StatusManager 已經註冊了實驗,當事件再次出發後ManagerFactory.getStatusManager().expExists(targetName) 的判斷不會被中斷,繼續往下走,到了自定義的 Enhancer ,在自定義的 Enhancer 裡面可以拿到原方法的引數、型別等,甚至可以反射調原型別的其他方法,這樣做風險較大,一般在這裡往往是取一些成員變數或者 get 方法等,用於 Inject 階段引數匹配。

7)包裝匹配引數

自定義的 Enhancer,如 ServletEnhancer,把一些需要與命令列匹配的引數 包裝在 MatcherMode 裡面,然後包裝 EnhancerModel 返回,比如  --requestpath = /index ,那麼requestpath 等於 requestURI;--querystring="name=xx" 做自定義匹配。引數包裝好後,在 Injector.inject(model) 階段判斷。

public EnhancerModel doBeforeAdvice(ClassLoader classLoader, String className, Object object,
                                    Method method, Object[] methodArguments)
        throws Exception {
    Object request = methodArguments[0];
    String requestURI = ReflectUtil.invokeMethod(request, ServletConstant.GET_REQUEST_URI, new Object[]{}, false);
    String requestMethod = ReflectUtil.invokeMethod(request, ServletConstant.GET_METHOD, new Object[]{}, false);

    MatcherModel matcherModel = new MatcherModel();
    matcherModel.add(ServletConstant.METHOD_KEY, requestMethod);
    matcherModel.add(ServletConstant.REQUEST_PATH_KEY, requestURI);

    Map<String, Object> queryString = getQueryString(requestMethod, request);

    EnhancerModel enhancerModel = new EnhancerModel(classLoader, matcherModel);
    // 自定義引數匹配
    enhancerModel.addCustomMatcher(ServletConstant.QUERY_STRING_KEY, queryString, ServletParamsMatcher.getInstance());
    return enhancerModel;
}

8)判斷前置條件

Inject 階段首先獲取 StatusManage 註冊的實驗,compare(model, enhancerModel) 做引數比對,比對失敗返回,limitAndIncrease(statusMetric) 判斷 --effect-count --effect-percent 來控制影響的次數和百分比

public static void inject(EnhancerModel enhancerModel) throws InterruptProcessException {
    String target = enhancerModel.getTarget();
    List<StatusMetric> statusMetrics = ManagerFactory.getStatusManager().getExpByTarget(
        target);
    for (StatusMetric statusMetric : statusMetrics) {
	  Model model = statusMetric.getModel();
	  // 匹配命令列輸入引數
	  if (!compare(model, enhancerModel)) {
		continue;
	  }
      // 累加攻擊次數和判斷攻擊次數是否到達 effect count 
	  boolean pass = limitAndIncrease(statusMetric);
	  if (!pass) {
		break;
	  }
	  enhancerModel.merge(model);
	  ModelSpec modelSpec = ManagerFactory.getModelSpecManager().getModelSpec(target);
	  ActionSpec actionSpec = modelSpec.getActionSpec(model.getActionName());
	  // ActionExecutor執行故障能力
	  actionSpec.getActionExecutor().run(enhancerModel);
      break;
    }
}

9)觸發故障能力

由 Inject 觸發,或者由 DirectlyInjectionAction 直接觸發,最後呼叫自定義的 ActionExecutor 生成故障,如  DefaultDelayExecutor ,此時故障能力已經生效了。

public void run(EnhancerModel enhancerModel) throws Exception {
    String time = enhancerModel.getActionFlag(timeFlagSpec.getName());
    Integer sleepTimeInMillis = Integer.valueOf(time);
 	// 觸發延遲
    TimeUnit.MILLISECONDS.sleep(sleepTimeInMillis);
}

3. 銷燬實驗

blade destroy 52a27bafc252beee

該命令下發後,觸發 SandboxModule @Http("/destory") 註解標記的方法,將事件分發給 com.alibaba.chaosblade.exec.service.handler.DestroyHandler 處理,登出本次故障的狀態,此時再次觸發 Enchaner 後,StatusManger判定實驗狀態已經銷燬,不會在進行故障能力注入

// StatusManger 判斷實驗狀態
if (!ManagerFactory.getStatusManager().expExists(targetName)) {
	return;
}

如果外掛的 ModelSpec 是 PreDestroyInjectionModelHandler 型別,且 ActionSpec 的型別是 DirectlyInjectionAction 型別,停止故障能力注入,ActionSpec 的型別不是 DirectlyInjectionAction 型別,將解除安裝外掛。

// DestroyHandler 登出實驗狀態
public Response handle(Request request) {
    String uid = request.getParam("suid");
    ...
	// 判斷 uid
    if (StringUtil.isBlank(uid)) {
        if (StringUtil.isBlank(target) || StringUtil.isBlank(action)) {
            return false;
        }
        // 登出status
        return destroy(target, action);
    }
    return destroy(uid);
}

4. 解除安裝 Agent

blade revoke 98e792c9a9a5dfea

該命令下發後,觸發 SandboxModule unload() 事件,同時外掛解除安裝,完全回收 Agent 建立的各種資源。

public void onUnload() throws Throwable {
    dispatchService.unload();
    ManagerFactory.unload();
    watchIds.clear();
}

總結

本文以 Servlet 場景為例,詳細介紹了 chaosblade-exec-jvm 專案架構設計和實現原理,後續將通過模型篇、協議篇、位元組碼篇、外掛篇以及實戰篇深入介紹此專案,使讀者達到可以快速擴充套件自己所需外掛的目的。

ChaosBlade 專案作為一個混沌工程實驗工具,不僅使用簡潔,而且還支援豐富的實驗場景且擴充套件場景簡單,支援的場景領域如下:

  • 基礎資源:比如 CPU、記憶體、網路、磁碟、程序等實驗場景;
  • Java 應用:比如資料庫、快取、訊息、JVM 本身、微服務等,還可以指定任意類方法注入各種複雜的實驗場景;
  • C++ 應用:比如指定任意方法或某行程式碼注入延遲、變數和返回值篡改等實驗場景;
  • Docker 容器:比如殺容器、容器內 CPU、記憶體、網路、磁碟、程序等實驗場景;
  • Kubernetes 平臺:比如節點上 CPU、記憶體、網路、磁碟、程序實驗場景,Pod 網路和 Pod 本身實驗場景如殺 Pod,Pod IO 異常,容器的實驗場景如上述的 Docker 容器實驗場景;
  • 雲資源:比如阿里雲 ECS 宕機等實驗場景。

ChaosBlade 社群歡迎各位加入,我們一起討論混沌工程領域實踐或者在使用 ChaosBlade 過程中產生的任何想法和問題。

作者簡介

葉飛:Github @tiny-x,開源社群愛好者,ChaosBlade Committer,參與推動 ChaosBlade 混沌工程生態建設。
穹谷:Github @xcaspar,ChaosBlade 專案負責人,混沌工程佈道師。

阿里巴巴雲原生關注微服務、Serverless、容器、Service Mesh 等技術領域、聚焦雲原生流行技術趨勢、雲原生大規模的落地實踐,做最懂雲原生開發者的公眾號。”