快放開那些搗亂的猴子!
粗看標題你可能感覺莫名其妙,什么「搗亂的猴子」,還要放開。不急,且聽我說說為什么不光要放開這些搗亂的猴子,還要歡迎他們。
0.背景信息
在構建高可用性軟件架構領域,有個詞叫「混沌工程」,對應的英文是Chaos Engineering,通過 Chaos 的測試,可以發現系統的潛在風險,特別對于分布式系統,找出脆弱的地方進行增強,提升可用性,避免系統間級聯影響。
混沌工程是在分布式系統上進行實驗的學科, 目的是建立對系統抵御生產環境中失控條件的能力以及信心。
大規模分布式軟件系統的發展正在改變軟件工程。作為一個行業,我們很快采用了提高開發靈活性和部署速度的實踐。緊隨著這些優點的一個迫切問題是:我們對投入生產的復雜系統有多少信心?
即使分布式系統中的所有單個服務都正常運行, 這些服務之間的交互也會導致不可預知的結果。 這些不可預知的結果, 由影響生產環境的罕見且破壞性的事件復合而成,令這些分布式系統存在內在的混沌。
https://principlesofchaos.org/zh/
后來Netflix 開源了其關于混沌工程的實現 ChaosMonkey,以猴子的形象來代表在系統里出其不意的破壞者。
比如
- 機器或者一個機房掛了
- 一部分網絡延遲嚴重
- CPU、內存占用嚴重
- 隨機讓某些服務異常或者響應延遲
再看Chaos 原則里提到的這些:
- 當服務不可用時的不正確回滾設置;
- 不當的超時設置導致的重試風暴;
- 由于下游依賴的流量過載導致的服務中斷;
- 單點故障時的級聯失敗等。
我們自己在代碼層面,在部署層面僅能關注應用的功能正常,但上述這些意想不到的出錯,是我們在代碼層面不太容易控制,也不易去測試的。
而ChaosMonkey 就是用來做這個的。所以,對于這些搗亂的猴子,我們是應該歡迎的,是不是像犀牛鳥之于犀牛?
關于ChaosMonkey,各個語言,各個公司也都有一些實現,其中Netflix的最出名。是go語言實現的。
在 Java Spring Boot 技術棧中,我發現一個容易理解和上手的實現。
https://github.com/codecentric/chaos-monkey-spring-boot
我們一起來看下如何上手以及它是怎樣實現的。
1. 上手
添加maven 依賴
- <dependency>
- <groupId>de.codecentric</groupId>
- <artifactId>chaos-monkey-spring-boot</artifactId>
- <version>2.3.0-SNAPSHOT</version>
- </dependency>
application.yml 中增加關于chaosmonkey的配置:
- chaos:
- monkey:
- enabled: true
- assaults:
- level: 1
- latencyRangeStart: 1000
- latencyRangeEnd: 10000
- exceptionsActive: true
- killApplicationActive: true
- watcher:
- repository: true
- controller: true
- # restController: true
- # service: true
應用啟動時,記得激活chaosmonkey的配置:
- java -jar your-app.jar --spring.profiles.active=chaos-monkey
再去請求你應用的controller,是不是發現異常產生了?這就是猴子在努力的搗亂中...
關于上面這些配置,再簡單解釋下:
你會發現chaos - monkey 配置下,除了 enabled,還有兩項比較大的配置項,一個是Assault,一個是Watcher。
其中Assault代表是搞什么破壞,比如破壞類型有超時、內存占用、殺死進程、拋出異常等等
- Latency Assault
- Exception Assault
- AppKiller Assault
- Memory Assault
而Watcher 表示都要在哪些地方搞破壞。一個是What,一個是Where。
Watcher支持多種類型,比如Spring 常用的組件:
- @Controller
- @RestController
- @Service
- @Repository
- @Component
那你說都 What 和 Where 了,怎么沒有When?還真有Level就是。
chaos.monkey.enabled 用來打開和關閉ChaosMonkey。對應的配置中,除了設置Assault之外,不同的Assault也可以設置攻擊的頻率,配置項是chaos.monkey.assaults.level比如1代表每次請求都攻擊,10代表每十次請求攻擊一次。
chaos.monkey.assaults.latencyRangeStart 和chaos.monkey.assaults.latencyRangeEnd 這兩個配置項用來配置LatencyAssault這個攻擊的延遲時間值范圍。
如下圖所示,實際部署之后,每個ChaosMonkey會藏身于各個服務中,出其不意進行攻擊。
這下子配置和使用就明白了。我們再來看看實現。
2.實現原理
aaa實際我們想一下,前面配置Watcher,后面決定進行攻擊,那必須得是Watcher把它攔下來再攻擊,所以在Spring 里攔截常用的,就是它:AOP。
原理如圖所示:
以Controller 的攔截為例
- /** @author Benjamin Wilms */
- @Aspect
- @AllArgsConstructor
- @Slf4j
- public class SpringControllerAspect extends ChaosMonkeyBaseAspect {
- private final ChaosMonkeyRequestScope chaosMonkeyRequestScope;
- private MetricEventPublisher metricEventPublisher;
- private WatcherProperties watcherProperties;
- @Pointcut("within(@org.springframework.stereotype.Controller *)")
- public void classAnnotatedWithControllerPointcut() {}
- @Around(
- "classAnnotatedWithControllerPointcut() && allPublicMethodPointcut() && !classInChaosMonkeyPackage()")
- public Object intercept(ProceedingJoinPoint pjp) throws Throwable {
- if (watcherProperties.isController()) {
- log.debug("Watching public method on controller class: {}", pjp.getSignature());
- if (metricEventPublisher != null) {
- metricEventPublisher.publishMetricEvent(
- calculatePointcut(pjp.toShortString()), MetricType.CONTROLLER);
- }
- MethodSignature signature = (MethodSignature) pjp.getSignature();
- chaosMonkeyRequestScope.callChaosMonkey(createSignature(signature));
- }
- return pjp.proceed();
- }
- public void callChaosMonkey(String simpleName) {
- if (isEnabled() && isTrouble()) {
- if (metricEventPublisher != null) {
- metricEventPublisher.publishMetricEvent(MetricType.APPLICATION_REQ_COUNT, "type", "total");
- }
- // Custom watched services can be defined at runtime, if there are any, only
- // these will be attacked!
- if (chaosMonkeySettings.getAssaultProperties().isWatchedCustomServicesActive()) {
- if (chaosMonkeySettings
- .getAssaultProperties()
- .getWatchedCustomServices()
- .contains(simpleName)) {
- // only all listed custom methods will be attacked
- chooseAndRunAttack();
- }
- } else {
- // default attack if no custom watched service is defined
- chooseAndRunAttack();
- }
- }
- }
這里是 Controller AOP的代碼,基本沒門檻。先判斷 Controller 的開關是否打開,然后再看是否需要事件通知,緊接著,就是重頭戲,召喚 Chaos Monkey 來搞破壞了。
注意這里,從激活的幾種攻擊方式里,選擇一種去調用。
- private void chooseAndRunAttack() {
- List<ChaosMonkeyAssault> activeAssaults =
- assaults.stream().filter(ChaosMonkeyAssault::isActive).collect(Collectors.toList());
- if (isEmpty(activeAssaults)) {
- return;
- }
- getRandomFrom(activeAssaults).attack(); // 注意這里,從激活的幾種攻擊方式里,選擇一種去調用。
- if (metricEventPublisher != null) {
- metricEventPublisher.publishMetricEvent(
- MetricType.APPLICATION_REQ_COUNT, "type", "assaulted");
- }
- }
延遲攻擊
比如LatencyAssault,就是要執行延遲攻擊,此時,會生成一個隨機的延遲時間
- public void attack() {
- Logger.debug("Chaos Monkey - timeout");
- atomicTimeoutGauge.set(determineLatency());
- // metrics
- if (metricEventPublisher != null) {
- metricEventPublisher.publishMetricEvent(MetricType.LATENCY_ASSAULT);
- metricEventPublisher.publishMetricEvent(MetricType.LATENCY_ASSAULT, atomicTimeoutGauge);
- }
- assaultExecutor.execute(atomicTimeoutGauge.get());
- }
然后把這個值傳在線程池中進行這個時間的
sleep。 assaultExecutor.execute(atomicTimeoutGauge.get());
- public class LatencyAssaultExecutor implements ChaosMonkeyLatencyAssaultExecutor {
- public void execute(long durationInMillis) {
- try {
- Thread.sleep(durationInMillis);
- } catch (InterruptedException e) {
- }
- }
- }
Exception攻擊
再來看Exception 攻擊,攻擊的時候,則是構造一個Exception 直接拋出
- @Override
- public void attack() {
- Logger.info("Chaos Monkey - exception");
- AssaultException assaultException = this.settings.getAssaultProperties().getException();
- assaultException.throwExceptionInstance();
- }
- @SneakyThrows
- public void throwExceptionInstance() {
- Exception instance;
- try {
- Class<? extends Exception> exceptionClass = getExceptionClass();
- if (arguments == null) {
- Constructor<? extends Exception> constructor = exceptionClass.getConstructor();
- instance = constructor.newInstance();
- } else {
- Constructor<? extends Exception> constructor =
- exceptionClass.getConstructor(this.getExceptionArgumentTypes().toArray(new Class[0]));
- instance =
- constructor.newInstance(this.getExceptionArgumentValues().toArray(new Object[0]));
- }
- } catch (ReflectiveOperationException e) {
- Logger.warn(
- "Cannot instantiate the class for provided type: {}. Fallback: Throw RuntimeException",
- type);
- instance = new RuntimeException("Chaos Monkey - RuntimeException");
- }
- throw instance; // 哈哈,直接拋出
- }
KillApp 就直接執行應用的退出操作,System.exit.
本文轉載自微信公眾號「 Tomcat那些事兒」,可以通過以下二維碼關注。轉載本文請聯系 Tomcat那些事兒公眾號。