快放开那些捣乱的猴子!

本文介绍混沌工程的概念及其在分布式系统中的应用,特别是通过Netflix的Chaos Monkey工具实施混沌测试的方法。文章详细解析了Chaos Monkey的工作原理及配置,并展示了如何利用Spring Boot集成Chaos Monkey进行系统稳定性测试。

粗看标题你可能感觉莫名其妙,什么「捣乱的猴子」,还要放开。不急,且听我说说为什么不光要放开这些捣乱的猴子,还要欢迎他们。

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.latencyRangeStartchaos.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();
      }
    }
  }

chaosMonkeyRequestScope.callChaosMonkey(createSignature(signature))

这里是 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.

感谢阅读,我喜欢鼓捣源码,追根究底,希望对你有所帮助。

如果方便,请点个「在看」和转发支持我吧,给我写下去的动力!   多谢多谢!

相关阅读

Sentinel 是怎样拦截异常流量的?

嵌套事务、挂起事务,Spring 是怎样给事务又实现传播特性的?

写代码效率不高?放过Ctrl C 和 V,让AI来能帮你写代码吧

不用Jar 包的Agent?几行代码实现运行时增强?

MySQL: 喂,别走,听我解释一下好吗?

多表查询用什么联接?别信感觉,用数据说话

一个数据库SQL查询的数次轮回

数据库是咋工作的?(一)

凭什么让日志先写?

怎样阅读源代码?

源码|实战|成长|职场

这里是「Tomcat那些事儿」

请留下你的足迹

我们一起「终身成长」

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值