[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;如果没有发生异常则将计数器清零并且返回结果。那么当下次调用目标方法的时候,有两种情况:
- 之前发生过异常,此时的计数器值应该大于0,并且在没有累积一定次数之前会直接抛出异常;如果积累达到10次,那么再次尝试方法调用。
- 之前没有发生过异常,此时的计数器应该为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());
}
参考资料
[AOP] 7. 一些自定义的Aspect - Circuit Breaker的更多相关文章
- [AOP] 6. 一些自定义的Aspect - 方法的重试(Retry)
前面的一系列文章介绍了AOP的方方面面: AOP的由来以及快速上手 AOP的两种实现-Spring AOP以及AspectJ Spring AOP中提供的种种Aspects - Tracing相关 S ...
- Springboot学习06-Spring AOP封装接口自定义校验
Springboot学习06-Spring AOP封装接口自定义校验 关键字 BindingResult.Spring AOP.自定义注解.自定义异常处理.ConstraintValidator 前言 ...
- Circuit Breaker Pattern(断路器模式)
Handle faults that may take a variable amount of time to rectify when connecting to a remote service ...
- Circuit Breaker Features
Better to use a circuit breaker which supports the following set of features: Automatically time-out ...
- 谈谈Circuit Breaker在.NET Core中的简单应用
前言 由于微服务的盛行,不少公司都将原来细粒度比较大的服务拆分成多个小的服务,让每个小服务做好自己的事即可. 经过拆分之后,就避免不了服务之间的相互调用问题!如果调用没有处理好,就有可能造成整个系统的 ...
- Circuit Breaker模式
Circuit Breaker模式会处理一些需要一定时间来重连远程服务和远端资源的错误.该模式可以提高一个应用的稳定性和弹性. 问题 在类似于云的分布式环境中,当一个应用需要执行一些访问远程资源或者是 ...
- Akka之Circuit Breaker
这周在项目中遇到了一个错误,就是Circuit Breaker time out.以前没有接触过,因此学习了下akka的断路器. 一.为什么使用Circuit Breaker 断路器是为了防止分布式系 ...
- .NET Core中Circuit Breaker
谈谈Circuit Breaker在.NET Core中的简单应用 前言 由于微服务的盛行,不少公司都将原来细粒度比较大的服务拆分成多个小的服务,让每个小服务做好自己的事即可. 经过拆分之后,就避免不 ...
- 55.fielddata内存控制以及circuit breaker断路器
课程大纲 fielddata加载 fielddata内存限制 监控fielddata内存使用 circuit breaker 一.fielddata加载 fielddata加载到内存的过程是lazy加 ...
随机推荐
- RocEDU.阅读.写作《乌合之众》(一)
序言 作者在序言里主要论述了时代演变的内在原因,表明对群体进行研究的重要性,阐述了研究群体行为特征时的研究方法,并概述了群体的发展过程. 造成文明变革的唯一重要变化,是影响到思想.观念和信仰的变化.目 ...
- 20145331 《Java程序设计》第7周学习总结
20145331 <Java程序设计>第7周学习总结 教材学习内容总结 第十二章 Lambda 1.Lambda定义:一个不用被绑定到一个标识符上,并且可能被调用的函数. 2.在只有Lam ...
- MiniTools在ubuntu下快捷方式
解压MiniTools-Linux-20140317.tgz root@ubuntu:~/tiny4412/MiniTools-20140317# ls -l total 38008 -rw-r--r ...
- .net 数据缓存(二)之Redis部署
现在的业务系统越来复杂,大型门户网站内容越来越多,数据库的数据量也越来愈大,所以有了“大数据”这一概念的出现.但是我们都知道当数据库的数据量和访问过于频繁都会影响系统整体性能体验,特别是并发量高的系统 ...
- MR案例:外连接代码实现
[外连接]是在[内连接]的基础上稍微修改即可.具体HQL语句详见Hive查询Join package join.map; import java.io.IOException; import java ...
- du df 磁盘命令
du命令是检查硬盘使用情况,统计文件或目录及子目录使用硬盘的空间大小.参数的不同组合,可以更快的提高工作效率,以下仅列出了经常使用到的参数,如需更详细的信息,请用man du命令来获得. 说明 - ...
- Elasticsearch之IKAnalyzer的过滤停止词
它在哪里呢? 非常重要! [hadoop@HadoopMaster custom]$ pwd/home/hadoop/app/elasticsearch-2.4.3/plugins/ik/config ...
- SSH免密码登录Linux
如果两台linux之间交互频繁,但是每次交互如果都需要输入密码,就会很麻烦,通过配置SSH就可以解决这一问题 下面就说下配置流程(下面流程在不同机器上全部操作一边) 1)cd ~到这个目录中 2)ss ...
- codeforces103E Buying Sets
本文版权归ljh2000和博客园共有,欢迎转载,但须保留此声明,并给出原文链接,谢谢合作. 本文作者:ljh2000 作者博客:http://www.cnblogs.com/ljh2000-jump/ ...
- hibernate:inverse、cascade,一对多、多对多详解
1.到底在哪用cascade="..."? cascade属性并不是多对多关系一定要用的,有了它只是让我们在插入或删除对像时更方便一些,只要在cascade的源头上插入或是删除,所 ...