
作者 | 葉飛、穹谷
導讀:總以為混沌工程離你很遠?但發生故障的那一刻不是由你來選擇的,而是那一刻來選擇你,你能做的就是為之做好準備。混沌工程在阿裡内部已經應用多年,而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 請求延遲為例,詳細介紹故障注入的過程。
- ChaosBlade 下發挂載指令,挂載 Sandbox 到應用程序,激活 Java Agent,例如
blade p jvm --pid 888
- 挂載 Sandbox 後加載 chaosblade-exec-jvm 子產品,加載插件,如 ServletPlugin、DubboPlugin 等。
- 比對 ServletPlugin 插件的切點、注冊事件監聽,HttpServlet 的 doPost、doGet 方法。
- ChaosBlade 下發故障規則指令
blade create servlet --requestpath=/topic delay --time=3000 --method=post
- 比對故障規則,如 --requestpath=/topic ,通路 http://127.0.0.1/topic 規則比對成功。
- 比對故障規則成功後,觸發故障,如延遲故障、自定義異常抛出等。
- 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 等技術領域、聚焦雲原生流行技術趨勢、雲原生大規模的落地實踐,做最懂雲原生開發者的公衆号。”