簡介: 總覺得混沌工程離你很遠?但發生故障的那一刻不是由你來選擇的,而是那一刻來選擇你,你能作的就是爲之作好準備。混沌工程在阿里內部已經應用多年,而ChaosBlade這個開源項目是阿里多年來經過注入故障來對抗故障的經驗結晶。爲使你們更深刻的瞭解其實現原理以及如何擴展本身所須要的組件故障注入,咱們準備了一個系列對其作詳細技術剖析:架構篇、模型篇、協議篇、字節碼篇、插件篇以及實戰篇。java
做者 | 葉飛、穹谷git
導讀:總覺得混沌工程離你很遠?但發生故障的那一刻不是由你來選擇的,而是那一刻來選擇你,你能作的就是爲之作好準備。混沌工程在阿里內部已經應用多年,而ChaosBlade這個開源項目是阿里多年來經過注入故障來對抗故障的經驗結晶。爲使你們更深刻的瞭解其實現原理以及如何擴展本身所須要的組件故障注入,咱們準備了一個系列對其作詳細技術剖析:架構篇、模型篇、協議篇、字節碼篇、插件篇以及實戰篇。
原文標題《技術剖析 Java 場景混沌工程實現系列(一)| 架構篇》github
在分佈式系統架構下,服務間的依賴日益複雜,很難評估單個服務故障對整個系統的影響,而且請求鏈路長,監控告警的不完善致使發現問題、定位問題難度增大,同時業務和技術迭代快,如何持續保障系統的穩定性和高可用性受到很大的挑戰。數據庫
咱們知道發生故障的那一刻不是由你來選擇的,而是那一刻來選擇你,你能作的就是爲之作好準備。因此構建穩定性系統很重要的一環是混沌工程,在可控範圍或環境下,經過故障注入,來持續提高系統的穩定性和高可用能力。緩存
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
。app
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 故障注入的流程。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
如下經過 Servlet 請求延遲爲例,詳細介紹故障注入的過程。
blade p jvm --pid 888
。blade create servlet --requestpath=/topic delay --time=3000 --method=post
。blade revoke 98e792c9a9a5dfea
。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(); }
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); }
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(); ... }; }
若是已經加載插件,此時目標應用匹配能匹配到 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); }
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); } }
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); } } ...
若是 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 類觸發故障。
故障能力注入的方式,最終都是調用 ActionExecutor 執行故障能力。
DirectlyInjectionAction 直接注入不通過Enhancer參數包裝匹配直接到故障觸發 ActionExecutor 執行階段,若是是Injector 注入此時由於 StatusManager 已經註冊了實驗,當事件再次出發後ManagerFactory.getStatusManager().expExists(targetName) 的判斷不會被中斷,繼續往下走,到了自定義的 Enhancer ,在自定義的 Enhancer 裏面能夠拿到原方法的參數、類型等,甚至能夠反射調原類型的其餘方法,這樣作風險較大,通常在這裏每每是取一些成員變量或者 get 方法等,用於 Inject 階段參數匹配。
自定義的 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; }
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; } }
由 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); }
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); }
blade revoke 98e792c9a9a5dfea
該命令下發後,觸發 SandboxModule unload() 事件,同時插件卸載,徹底回收 Agent 建立的各類資源。
public void onUnload() throws Throwable { dispatchService.unload(); ManagerFactory.unload(); watchIds.clear(); }
本文以 Servlet 場景爲例,詳細介紹了 chaosblade-exec-jvm 項目架構設計和實現原理,後續將經過模型篇、協議篇、字節碼篇、插件篇以及實戰篇深刻介紹此項目,使讀者達到能夠快速擴展本身所需插件的目的。
ChaosBlade 項目做爲一個混沌工程實驗工具,不只使用簡潔,並且還支持豐富的實驗場景且擴展場景簡單,支持的場景領域以下:
ChaosBlade 社區歡迎各位加入,咱們一塊兒討論混沌工程領域實踐或者在使用 ChaosBlade 過程當中產生的任何想法和問題。
葉飛:Github @tiny-x,開源社區愛好者,ChaosBlade Committer,參與推進 ChaosBlade 混沌工程生態建設。
穹谷:Github @xcaspar,ChaosBlade 項目負責人,混沌工程佈道師。