如果故障选择了你……
作者 | 葉飛、穹谷
**導讀:**總以為混沌工程離你很遠?但發生故障的那一刻不是由你來選擇的,而是那一刻來選擇你,你能做的就是為之做好準備。混沌工程在阿里內部已經應用多年,而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 98e792c9a9a5dfea1. 執行過程
以下通過 Servlet 請求延遲為例,詳細介紹故障注入的過程。
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() {@Overridepublic boolean doClassFilter(int access, String javaClassName, String superClassTypeJavaClassName,String[] interfaceTypeJavaClassNameArray,String[] annotationTypeJavaClassNameArray) {// ClassMatcher 匹配ClassMatcher classMatcher = pointCut.getClassMatcher();...}@Overridepublic 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 是一次實驗的上下文IDString 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預銷毀
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 injectiontry {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");...// 判斷 uidif (StringUtil.isBlank(uid)) {if (StringUtil.isBlank(target) || StringUtil.isBlank(action)) {return false;}// 注銷statusreturn 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 等技術領域、聚焦云原生流行技術趨勢、云原生大規模的落地實踐,做最懂云原生開發者的公眾號。”
總結
以上是生活随笔為你收集整理的如果故障选择了你……的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 教你 4 步搭建弹性可扩展的 WebAP
- 下一篇: 阿里云 OpenYurt 成为 CNCF