Circuit Breaker(断路器)模式

关于断路器模式是在微服务架构/远程调用环境下经常被使用到的一个模式。它的作用一言以蔽之就是提高系统的可用性,在出现的问题通过服务降级的手段来保证系统的整体可用,而不至于因为部分问题导致整个系统不可用。

用下面这张图可以很好的说明它能够解决的问题:



图片引用自参考资料1。

其中从client和supplier可以分别理解成调用者和远程方法。在没有Circuit Breaker这个组件之前,两者是直接发生交互的,因此当远程方法不可用时,调用者这边可能会阻塞或者失败。由于在微服务架构/远程调用环境下,方法调用之间往往都有依赖性,因此当本次方法调动失败后有可能会影响到后续的业务,从而层层失败(Cascading Failures)导致整个系统的不可用。

通过引入断路器模式(即图中的Circuit Breaker组件),让它负责Client和远程资源的调用和协调。当调用正常的时候,并不会感觉到断路器的存在,然而当调用发生异常,比如连续性的Timeouts,这个时候断路器会被触发(也就是图中的trip),被触发后的断路器处于打开(Open)的状态,此时由Client发起的调用请求会被断路器拒绝,完成服务的降级。

降级后的返回值因应用场景而异,如果能够有默认值的情况可以返回给调用方默认值。比如在一个购物网站中,会根据用户的浏览记录动态地推荐相关产品,如果这个动态推荐的服务暂时不可用,那么可以考虑推荐一些默认的畅销产品,这些结果一般会存放在缓存中,因此也不需要消耗什么计算资源。如果没有的话可以提示调用者流量过大,请稍后重试。就像每逢双11零点的时候经常会被各大购物网站拒绝访问一样。

希望通过以上的解释,能够大概说明Circuit Breaker模式的意图。更多信息可以查看参考资料的1和2。同时,在业界也有一些厂商针对这个模式有一些开源工具,比如Netflix的Hystrix项目,这个项目也被Spring整合到其Spring Cloud微服务技术栈中。

和Retry的区别

在上一篇文章中我们讨论了如何使用Retry机制来处理调用中可能出现的失败,Retry和Circuit Breaker尤其共通之处:

  • 都涉及到对于目标方法的多次调用
  • 都有阈值的概念(重试次数vs断路前的失败次数)

但是Retry机制尤其自身的问题,比如:

  • 当服务不可用时容易堆积大量调用
  • 服务再次可用的时候容易被大量的堆积请求再次弄崩
  • 策略上不够灵活

以上问题的症结在于重试机制没有办法去区分服务是暂时不可用(随机性的网络异常)还是真的不可用(服务挂了),也许通过区分异常类型可以判断,但是多个调用线程的重试是彼此独立的,并没有一个统一的管控方(比如Circuit Breaker)进行协调。这就导致在服务确实不可用的时候,调用还是会发起请求,哪怕重试的次数因为异常类型的缘故不那么多。

而使用断路器时,它能够根据情况服务状况调整请求数量,比如在服务不可用的时候能够大量地减少请求数量。并且断路器本身会根据业务性质实现一些恢复策略,比如断路器开启30秒后进行重试,如果调用成功则关闭断路器等等。

下面,我们就来看看如何通过AOP实现Circuit Breaker模式。

Aspect实现

目标业务方法(可以考虑成远程调用)

@Service
@Scope("prototype")
public class CircuitBreakerService {

  private int counter = 0;

  @CircuitBreaker
  public int service() {
    if (counter++ < 1) {
      throw new RuntimeException("服务不可用");
    }

    return 1;
  }

}

以上的service方法便是目标业务方法了,里面一般会包含远程调用。这里为了模拟远程调用出现的问题,在初次调用的时候会抛出RuntimeException,第二次调用的时候返回正常结果。

标记注解

@CircuitBreaker注解用来标注业务方法作为Pointcut的定位方式,目前注解只是一个Marker Annotation:

@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface CircuitBreaker {

}

Aspect

@Component
@Scope("prototype")
@Aspect("perthis(com.rxjiang.aop.custom.cb.CircuitBreakerAspect.circuitBreakerTargets())")
public class CircuitBreakerAspect {

  @Pointcut("execution(@com.rxjiang.aop.custom.cb.CircuitBreaker * *(..))")
  public void circuitBreakerTargets() {}

  private AtomicInteger counter = new AtomicInteger(0);
  private Throwable throwable;

  @Around("com.rxjiang.aop.custom.cb.CircuitBreakerAspect.circuitBreakerTargets()")
  public Object advice(ProceedingJoinPoint pjp) throws Throwable {
    try {
      if (counter.get() == 0 || counter.addAndGet(1) == 10) {
        Object result = pjp.proceed();
        counter.set(0);
        return result;
      }
    } catch (Throwable throwable) {
      this.throwable = throwable;
      counter.set(1);
    }

    throw this.throwable;
  }

}

上述代码由以下几个部门组成:

  • CircuitBreakerAspect的内部状态以及声明方式
  • Circuit Breaker的逻辑(advice方法)

下面我们分别来看看这两个部分的具体细节。

Prototype类型的Aspect

@Component
@Scope("prototype")
@Aspect("perthis(com.rxjiang.aop.custom.cb.CircuitBreakerAspect.circuitBreakerTargets())")
public class CircuitBreakerAspect {
  private AtomicInteger counter = new AtomicInteger(0);
  private Throwable throwable;

  // ...
}

由于该Aspect内部存在两个成员变量,即它是有状态的。因此在被多个Service使用的时候,需要使用不同的Aspect实例。因此也就有了上面的@Scope以及@Aspect中的perthis语法声明。关于perthis的作用,简而言之就是会为每个调用目标方法的Service对象都创建一个Aspect。更多信息请查看参考资料3。

Circuit Breaker的逻辑

大概逻辑是:

当初次调用或者调用次数积累到一定程度(这里设定的是10次),会尝试调用目标方法。调用目标方法的过程中如果发生了异常将异常记录为成员变量然后将计数器设置为1;如果没有发生异常则将计数器清零并且返回结果。那么当下次调用目标方法的时候,有两种情况:

  1. 之前发生过异常,此时的计数器值应该大于0,并且在没有累积一定次数之前会直接抛出异常;如果积累达到10次,那么再次尝试方法调用。
  2. 之前没有发生过异常,此时的计数器应该为0,那么会正常调用目标方法。

反映到代码中就是下面这样:

@Around("com.rxjiang.aop.custom.cb.CircuitBreakerAspect.circuitBreakerTargets()")
public Object advice(ProceedingJoinPoint pjp) throws Throwable {
  try {
    if (counter.get() == 0 || counter.addAndGet(1) == 10) {
      Object result = pjp.proceed();
      counter.set(0);
      return result;
    }
  } catch (Throwable throwable) {
    this.throwable = throwable;
    counter.set(1);
  }

  throw this.throwable;
}

测试方法

为了测试Aspect是否有多个实例,创建了两个服务(CircuitBreakerService本身也是prototype类型的):

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = {CustomAopConfiguration.class})
public class CircuitBreakerTest {

  @Autowired
  private CircuitBreakerService service1;

  @Autowired
  private CircuitBreakerService service2;

  @Test
  public void testCircuitBreakerService1() {
    coreCircuitBreakerService(service1);
  }

  @Test
  public void testCircuitBreakerService2() {
    coreCircuitBreakerService(service2);
  }

  public void coreCircuitBreakerService(CircuitBreakerService service) {
    for (int i = 0; i < 9; i++) {
      try {
        service.service();
        fail("不应该到这里");
      } catch (RuntimeException e) {

      }
    }

    assertEquals(1, service.service());
  }

}

相关扩展

仔细分析上面Circuit Breaker的逻辑部分,可以提炼出下面的通用结构:

@Around("com.rxjiang.aop.custom.cb.CircuitBreakerAspect.circuitBreakerTargets()")
public Object advice(ProceedingJoinPoint pjp) throws Throwable {
  try {
    if (cb.isClosed()) {
      Object result = pjp.proceed();
      cb.reset();
      return result;
    }
  } catch (Throwable throwable) {
    cb.catchedException(throwable)
  }

  return cb.process(pjp);
}

那么我们可以有一个具体的CircuitBreaker对象(上述代码中的cb对象)用来处理和断路器相关的逻辑,因此可以设计这样一个接口:

public interface ICircuitBreaker {

  boolean isClosed();

  void reset();

  void catchedException(Throwable throwable);

  Object process(ProceedingJoinPoint pjp) throws Throwable;

}

同时为了让断路器在打开的时候能够调用默认实现,可以向注解中添加一个属性:

@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface CircuitBreaker {

  String fallbackMethod() default "";

}

然后在断路器实现中,可以通过反射的方式去检查指定的fallback方法是否存在,如果存在并且方法接受的参数类型以及返回值类型都一致的话,就会尝试去调用默认方法,而不是直接抛出异常。

铺垫了这么多,下面是一个基于计数器的断路器实现:

public class CounterCircuitBreaker implements ICircuitBreaker {

  private int threshold;
  private AtomicInteger counter = new AtomicInteger(0);
  private Throwable throwable;

  public CounterCircuitBreaker(int threshold) {
    this.threshold = threshold;
  }

  @Override
  public boolean isClosed() {
    return counter.get() == 0 || counter.addAndGet(1) == threshold;
  }

  @Override
  public void reset() {
    counter.set(0);
  }

  @Override
  public void catchedException(Throwable throwable) {
    this.throwable = throwable;
    this.counter.set(1);
  }

  @Override
  public Object process(ProceedingJoinPoint pjp) throws Throwable {
    // 获取被调用的对象以及CircuitBreaker注解对象
    MethodSignature signature = (MethodSignature) pjp.getSignature();
    String methodName = signature.getMethod().getName();
    Class<?>[] parameterTypes = signature.getMethod().getParameterTypes();
    CircuitBreaker cbAnno = pjp.getTarget().getClass().getMethod(methodName, parameterTypes)
        .getAnnotation(CircuitBreaker.class);

    String fallbackMethodName = cbAnno.fallbackMethod();
    if (StringUtils.isEmpty(fallbackMethodName)) {
      if (throwable != null) {
        throw throwable;
      }
    } else {
      if (fallbackExistsAndSignatureCorrect(pjp, fallbackMethodName)) {
        Method fallbackMethod = pjp.getTarget().getClass().getMethod(fallbackMethodName);
        return fallbackMethod.invoke(pjp.getTarget(), pjp.getArgs());
      } else {
        throw new IllegalArgumentException("指定的fallback方法不存在或者参数签名/返回值与目标方法不同");
      }
    }

    throw new IllegalArgumentException("被调对象或者方法为空");
  }

  private boolean fallbackExistsAndSignatureCorrect(ProceedingJoinPoint pjp,
      String fallbackMethodName) throws Throwable {
    MethodSignature signature = (MethodSignature) pjp.getSignature();
    Method fallbackMethod;
    try {
      fallbackMethod =
          pjp.getTarget().getClass().getMethod(fallbackMethodName, signature.getParameterTypes());
    } catch (NoSuchMethodException e) {
      return false;
    }

    if (fallbackMethod == null) {
      return false;
    }

    // 校验方法参数以及返回值是否一致
    String fbReturnType = fallbackMethod.getReturnType().getCanonicalName();
    String targetReturnType = signature.getReturnType().getCanonicalName();
    if (StringUtils.isEmpty(fbReturnType) || StringUtils.isEmpty(targetReturnType)
        || !fbReturnType.equalsIgnoreCase(targetReturnType)) {
      return false;
    }

    Class<?>[] fbParamTypes = fallbackMethod.getParameterTypes();
    Class<?>[] targetParamTypes = signature.getParameterTypes();
    if (fbParamTypes.length != targetParamTypes.length) {
      return false;
    }
    for (int i = 0; i < fbParamTypes.length; i++) {
      if (!fbParamTypes[i].getCanonicalName().equals(targetParamTypes[i].getCanonicalName())) {
        return false;
      }
    }

    return true;
  }

}

主体结构还是非常清晰的,细节部分主要是和反射相关的一些处理工作。

相应的,在Service中定义一个fallback方法以及一个使用它的目标业务方法:

@CircuitBreaker(fallbackMethod = "fallbackService")
public int serviceWithFallback() {
  if (counter++ < 1) {
    throw new RuntimeException("服务不可用");
  }

  return 1;
}

public int fallbackService() {
  return 2;
}

相关测试:

@Test
public void testCircuitBreakerServiceWithFallback() {
  assertEquals(2, service1.serviceWithFallback());
}

参考资料

  1. Circuit Breaker by Martin Fowler
  2. MSDN Circuit Breaker
  3. Spring AOP Doc - Instantiation Models
  4. Hystrix

[AOP] 7. 一些自定义的Aspect - Circuit Breaker的更多相关文章

  1. [AOP] 6. 一些自定义的Aspect - 方法的重试(Retry)

    前面的一系列文章介绍了AOP的方方面面: AOP的由来以及快速上手 AOP的两种实现-Spring AOP以及AspectJ Spring AOP中提供的种种Aspects - Tracing相关 S ...

  2. Springboot学习06-Spring AOP封装接口自定义校验

    Springboot学习06-Spring AOP封装接口自定义校验 关键字 BindingResult.Spring AOP.自定义注解.自定义异常处理.ConstraintValidator 前言 ...

  3. Circuit Breaker Pattern(断路器模式)

    Handle faults that may take a variable amount of time to rectify when connecting to a remote service ...

  4. Circuit Breaker Features

    Better to use a circuit breaker which supports the following set of features: Automatically time-out ...

  5. 谈谈Circuit Breaker在.NET Core中的简单应用

    前言 由于微服务的盛行,不少公司都将原来细粒度比较大的服务拆分成多个小的服务,让每个小服务做好自己的事即可. 经过拆分之后,就避免不了服务之间的相互调用问题!如果调用没有处理好,就有可能造成整个系统的 ...

  6. Circuit Breaker模式

    Circuit Breaker模式会处理一些需要一定时间来重连远程服务和远端资源的错误.该模式可以提高一个应用的稳定性和弹性. 问题 在类似于云的分布式环境中,当一个应用需要执行一些访问远程资源或者是 ...

  7. Akka之Circuit Breaker

    这周在项目中遇到了一个错误,就是Circuit Breaker time out.以前没有接触过,因此学习了下akka的断路器. 一.为什么使用Circuit Breaker 断路器是为了防止分布式系 ...

  8. .NET Core中Circuit Breaker

    谈谈Circuit Breaker在.NET Core中的简单应用 前言 由于微服务的盛行,不少公司都将原来细粒度比较大的服务拆分成多个小的服务,让每个小服务做好自己的事即可. 经过拆分之后,就避免不 ...

  9. 55.fielddata内存控制以及circuit breaker断路器

    课程大纲 fielddata加载 fielddata内存限制 监控fielddata内存使用 circuit breaker 一.fielddata加载 fielddata加载到内存的过程是lazy加 ...

随机推荐

  1. [转]C语言 gets()和scanf()函数的区别

    scanf( )函数和gets( )函数都可用于输入字符串,但在功能上有区别.若想从键盘上输入字符串"hi hello",则应该使用__gets__函数. gets可以接收空格:而 ...

  2. Jquery2 基础核心

    学习要点: 1.代码风格 2.加载模式 3.对象互换 4.多个库之间的冲突 本节简单的介绍一下jQuery 一些核心的问题. 一.代码风格 在jQuery程序中,不管是页面元素的选择.内置的功能函数, ...

  3. 【转载】showModalDialog returnValue is undefined in Google Chrome

    showModalDialog returnValue is undefined in Google Chrome Posted on August 22, 2012by briancaos For ...

  4. Centos为mysql开启binlog

    1.查询mysql配置文件所在位置 2.编辑配置文件/etc/my.cnf 在文件尾部添加: log-bin=/var/lib/mysql/mysql-bin server-id=123454  (5 ...

  5. Graph_Master(连通分量_D_Trajan缩点+dfs)

    hdu_2242 题目大意:求将一张无向图(n个点,m条边)移除一条边分为不连通两部分,使得两部分的点权和最接近,若无法分为两部分,则输出impossible. 题解:拿到题面还算清晰,就是先tarj ...

  6. Union 和Union all的区别

    Union:对两个结果集进行并集操作,不包括重复行,同时进行默认规则的排序: Union All:对两个结果集进行并集操作,包括重复行,不进行排序: 例如: select employee_id,jo ...

  7. valgrind的memchk和callgrind

    一.安装valgrind 安装valgrind,正常的三部曲configure/make/make install就行. 二.memchk使用 1.执行命令 [root@10g-host4 tools ...

  8. url拼接

    在做网页抓取的时候经常会遇到一个问题就是页面中的链接是相对链接,这个时候就需要对链接进行url拼接,才能得到绝对链接. url严格按照一定的格式构成,一般为如下5个字段: 详细可参考RFC:http: ...

  9. LightOJ 1341 Aladdin and the Flying Carpet(唯一分解定理)

    http://lightoj.com/volume_showproblem.php?problem=1341 题意:给你矩形的面积(矩形的边长都是正整数),让你求最小的边大于等于b的矩形的个数. 思路 ...

  10. node.js 之 http 架设

    Node.js 安装配置 下载node.js安装mis 打开:cmd cd到node.js安装目录下 输入nodejs --version 显示版本号,证明安装成功 在其根目录下建server.js ...