[AOP] 7. 一些自定义的Aspect - Circuit Breaker

Posted dm_vincent

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了[AOP] 7. 一些自定义的Aspect - Circuit Breaker相关的知识,希望对你有一定的参考价值。

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的主要内容,如果未能解决你的问题,请参考以下文章

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

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

aop配合自定义的注解使用,静态类获取request

关于srping的AOP事务管理问题,自定义切面是否导致事务控制失效

spring的aop实现与自定义Advisor

AOP