去哪兒系統高可用之法:搭建故障演練平臺
王鵬, 2017年加入去哪兒機票事業部,主要從事後端研發工作,目前在機票事業部負責行程單和故障演練平臺以及公共服務ES、資料同步中介軟體等相關的研發工作。
去哪兒網2005年成立至今,隨著系統規模的逐步擴大,已經有成百上千個應用系統,這些系統之間的耦合度和鏈路的複雜度不斷加強,對於我們構建分散式高可用的系統架構具有極大挑戰。我們需要一個平臺在執行期自動注入故障,檢驗故障預案是否起效——故障演練平臺。
一、背景
這是某事業部的系統拓撲圖:
系統之間的依賴非常複雜、呼叫鏈路很深、服務之間沒有分層。 在這種複雜的依賴下,系統發生了幾起故障:
-
弱依賴掛掉,主流程掛掉,修改報銷憑證的支付狀態,下單主流程失敗;
-
核心服務呼叫量陡增,某服務超時引起相關聯的所有服務“雪崩”;
-
機房網路或者某些機器掛掉,不能提供核心服務。
三個故障原因:
-
系統強弱依賴混亂、弱依賴無降級;
-
系統流量陡增,系統容量不足,沒有限流熔斷機制;
-
硬體資源網路出現問題影響系統執行,沒有高可用的網路架構。
各種各樣的問題,在這種複雜的依賴結構下被放大,一個依賴30個SOA服務的系統,每個服務99.99%可用。99.99%的30次方≈99.7%。0.3%意味著一億次請求會有3,000,00次失敗,換算成時間大約每月有2個小時服務不穩定。隨著服務依賴數量的變多,服務不穩定的概率會 呈 指數 性提高,這些問題最後都會轉化為故障表現出來。
二、系統高可用的方法論
如何構建一個高可用的系統呢?首先要分析一下不可用的因素都有哪些:
高可用系統典型實踐
理論上來說,當圖中所有的事情都做完,我們就可以認為系統是一個真正的高可用系統。但真是這樣嗎?
那麼故障演練平臺就隆重登場了。當上述的高可用實踐都做完,利用故障演練平臺做一次真正的故障演練,在系統執行期動態地注入一些故障,從而來驗證下系統是否按照故障預案去執行相應的降級或者熔斷策略 。
三、故障演練平臺
故障演練平臺: 檢驗故障預案是否真正的起作用的平臺。
故障型別 : 主要包括執行期異常、超時等等。通過對系統某些服務動態地注入執行期異常來達到模擬故障的目的,系統按照預案執行相應的策略驗證系統是否是真正的高可用。
1、故障演練平臺的整體架構
故障演練平臺架構主要分為四部分:
-
前臺展示系統(WEB): 展示系統之間的拓撲關係以及每個AppCode對應的叢集和方法,可以選擇具體的方法進行故障的注入和解除;
-
釋出系統(Deploy): 這個系統主要用於將故障演練平臺的Agent和Binder包釋出到目標APP的機器上並且啟動執行。前臺展示系統會傳遞給釋出平臺要進行故障注入的AppCode以及目標APP的IP地址,通過這兩個引數釋出系統可以找到相應的機器進行Jar包的下載和啟動;
-
服務和命令分發系統(Server): 這個系統主要是用於命令的分發、注入故障的狀態記錄、故障注入和解除操作的邏輯、許可權校驗以及相關的Agent的返回資訊接收功能。前臺頁面已經接入QSSO會對當前人可以操作的IP列表做故障注入,防範風險。後端命令分發的模組會和部署在目標APP上的Agent進行通訊,將命令推送到Agent上執行位元組碼編織,Agent執行命令後返回的內容通過Server和Agent的長連線傳回Server端;
-
Agent和Binder程式: Agent負責對目標APP做代理並且做位元組碼增強,具體代理的方法可以通過傳輸的命令來控制,代理方法後對方法做動態的位元組碼增強,這種位元組碼增強具有無侵入、實時生效、動態可插拔的特點。Binder程式主要是通過釋出系統傳遞過來的AppCode和啟動埠(ServerPort)找到目標APP的JVM程序,之後執行動態繫結,完成執行期程式碼增強的功能。
2、 Agent整體架構
目前AOP的實現有兩種方式:
-
靜態編織: 靜態編織發生在位元組碼生成時根據一定框架的規則提前將AOP位元組碼插入到目標類和方法中;
-
動態編織: 在JVM執行期對指定的方法完成AOP位元組碼增強。常見的方法大多數採用重新命名原有方法,再新建一個同名方法做代理的工作模式來完成。
靜態編織的問題是如果想改變位元組碼必須重啟,這給開發和測試過程造成了很大的不便。動態的方式雖然可以在執行期注入位元組碼實現動態增強,但沒有統一的API很容易操作錯誤。基於此,我們採用動態編織的方式、規範的API來規範字節碼的生成——Agent元件。
Agent元件: 通過JDK所提供的Instrumentation-API實現了利用HotSwap技術在不重啟JVM的情況下實現對任意方法的增強,無論我們是做故障演練、呼叫鏈追蹤(QTrace)、流量錄製平臺(Ares)以及動態增加日誌輸出BTrace,都需要一個具有無侵入、實時生效、動態可插拔的位元組碼增強元件。
Agent的事件模型
如圖所示,事件模型主要可分為三類事件:
BEFORE在方法執行前事件、THROWS丟擲異常事件、RETURN返回事件。這三類事件可以在方法執行前、返回和丟擲異常這三種情況做位元組碼編織。
如下程式碼:
// BEFORE try { /* * do something... */ foo(); // RETURN return; } catch (Throwable e) { // THROWS }
事件模型可以完成三個功能:
-
在方法體執行之前直接返回自定義結果物件,原有方法程式碼將不會被執行;
-
在方法體返回之前重新構造新的結果物件,甚至可以改變為丟擲異常;
-
在方法體丟擲異常之後重新丟擲新的異常,甚至可以改變為正常返回。
Agent如何防止“類汙染”
在開發Agent的時候,第一個應用是故障演練平臺,那麼這個時候其實我們並不需要Agent執行的過程中有自定義結果物件的返回,所以第一個版本的Agent採用硬編碼的方式進行動態織入:
故障類載入模型
首先介紹下幾個類載入器:
-
BootstrapClassLoader引導類載入器載入的是JVM自身需要的類,這個類載入使用C++語言實現的,是虛擬機器自身的一部分;
-
ExtClassLoader它負責載入<JAVA_HOME>/lib/ext目錄下或者由系統變數-Djava.ext.dir指定位路徑中的類庫;
-
AppClassLoader它負責載入系統類路徑java-classpath或-D java.class.path指定路徑下的類庫,也就是我們經常用到的classpath路徑;
-
CommonClassLoader以及下邊的都是Tomcat定義的ClassLoader。
Agent和相關的lib會放到AppClassLoader這一層去載入,利用Javasist做位元組碼的織入,所以Javasist的載入器就是AppClassLoader。
但是想改變的是Tomcat WebClassLoader所載入的com.xxx.InvocationHandler這個類的Invoke方法,不同的ClassLoader之間的類是不能相互訪問的,做位元組碼的變換並不需要這個類的例項,也不需要返回結果,所以可以通過Instrument API拿到這個類載入器,並且可以根據類名稱獲取到這個類的位元組碼進行位元組碼變換。故障類Drill.class和變形後的com.xxx.InvocationHandler.class重新load到JVM中,完成了插樁操作。
以Dubbo為例說明下如何注入故障和解除故障:
Dubbo呼叫的注入過程
-
服務A呼叫服務B在Client端的Proxy層做AOP;
-
啟動Agent並且生成一個Drill類invoke方法,丟擲一個執行期異常;
-
位元組碼變形:在程式碼第一行之前增加Drill.invoke();
-
如果想變換異常型別,改變Drill類即可,換成Sleep 3s ClassRedifine之後會重新load到JVM完成故障型別的轉化或者清除。
遇到的問題
上邊的方式貌似很完美的解決了問題,但是隨著平臺的使用業務線要對很多介面和方法同時進行故障演練,那麼我們生成的Drill類裡面就會有各種:
if method==業務線定義方法
do xxx
而且很容易拼接出錯並且難以除錯,只能把生成的類輸出為檔案,檢視自己寫的位元組碼編譯成class檔案是否正確,簡直太痛苦了!
怎麼解決?
新的架構需要解決三個問題:
-
類隔離的問題:不要汙染原生APP;
-
事件的實現是可編譯的;
-
支援返回自定義的結果。
下一版本的Agent實現就產生了,把所有Agent的類和實現的功能抽象出來,放到一個自定義的AgentClassLoader裡面,位元組碼注入到目標APP後可以通過反射的方式來呼叫具體的事件實現。
類載入模型
-
在BootstrapClassLoader裡面注入Drill類作為通訊類;
-
Agent會接受命令,根據事件型別對InvocationHandler做位元組碼變形,注入到目標APP;
-
在目標APP呼叫的時候,呼叫Drill.invoke(targetJavaClass,targetJavaMethod, targetThis, args)傳遞過來幾個引數(目標類、方法、例項、本身引數等);
-
Drill類通過反射的方式呼叫AppClassLoader裡面的具體事件實現,比如BEFORE事件的執行程式碼,來完成注入後的邏輯執行。
Agent的整體架構
Agent的整體架構如圖所示:
-
支援不同的模組的加入,比如Mock、流量錄製、故障演練等;
-
支援QSSO的許可權驗證;
-
支援測試和模擬環境的無成本接入;
-
支援自動部署不需要人工介入;
-
支援各種故障命令的釋出和執行、 超時 、異常以及資料的返回;
-
支援方法級別的編織以及程式碼執行流程的編織;
-
支援在任意的Web容器執行Agent代理。
四、如何使用
使用的好處是很明顯的:
-
零成本接入,無需申請任何資源;
-
故障注入解除,無需重啟服務;
-
可以提供所有叢集的拓撲結構。
但是如何才能正確使用呢?如下圖所示:
使用方法
步驟一、輸入AppCode;
步驟二、選擇故障方法;
步驟三、指定機器;
步驟四、注入故障 。
五、總結
故障演練平臺最核心的就是Agent元件——位元組碼編織框架,這個框架是純Java的基於Instrumentation-API的AOP解決方案。它可以方便研發人員對於位元組碼插樁拆樁操作,可以很容易的實現故障演練、流量錄製以及其他的應用模組。
作者:王鵬 來源:Qunar技術沙龍訂閱號(ID:QunarTL) dbaplus社群歡迎廣大技術人員投稿,投稿郵箱:[email protected]