内容简介:上一篇介绍了Resilience4j的基本用法,提到过这个容错框架不只是容错,它的代码质量很高,有很多可以学习借鉴的地方。今天,我们从熔断器源码出发,一起探寻一下这个框架的核心组件的奥妙。很多人都了解熔断器,它在程序中就相当于电路中的保险丝,当短路发生的时候,保险丝熔断,从而保护电路不会被烧坏,同样的,程序中的熔断器也是在下游服务发生错误的时候进行熔断,从而停止对下游服务的调用,起到保护下游服务和系统的作用。其核心思想就是避免因不断重试导致的错误放大,及时止损。
上一篇介绍了Resilience4j的基本用法,提到过这个容错框架不只是容错,它的代码质量很高,有很多可以学习借鉴的地方。今天,我们从熔断器源码出发,一起探寻一下这个框架的核心组件的奥妙。
1.熔断器原理
很多人都了解熔断器,它在程序中就相当于电路中的保险丝,当短路发生的时候,保险丝熔断,从而保护电路不会被烧坏,同样的,程序中的熔断器也是在下游服务发生错误的时候进行熔断,从而停止对下游服务的调用,起到保护下游服务和系统的作用。其核心思想就是避免因不断重试导致的错误放大,及时止损。
上面这个张图想必大家都很熟悉,三个节点CLOSED、HALF_OPEN和OPEN标识着熔断器的三个状态,关闭、半开和打开,熔断器就是围绕着这三个状态实现接口保护和访问屏蔽的。对于Resilience4J来说,有几个参数是需要提前说明的:
-
失 败率阈值(failureRateThreshold)
-
关闭状态下的访问次数 ( ringBufferSizeInClosedState)
-
半开状态下的访问次数 (ringBufferSizeInHalfOpenState)
-
开启状态的持续时间(waitDurationInOpenState)
熔断器的实现原理也就清晰明了了:
-
熔断器初始是关闭状态的,当访问次数达到关闭状态下的访问次数时,熔断器开始计算失败率,即失败调用次数与总调用次数的比值
-
当失败率大于失败率阈值时,熔断器打开,所有调用都会直接被熔断,不会再调用被保护的服务。 在经过我们设置的时间之后,熔断器会进入半开状态。
-
半开状态的熔断器会重新尝试关闭熔断器,并放行我们配置的访问次数的调用,然后对调用结果再次计算失败率,如果失败率依然大于阈值,则熔断器继续回到开启状态,否则熔断器关闭。
看完熔断器的原理,我们可以看到,熔断器都是围绕着几个状态来回转换实现功能的,有些小可爱可能发现,这个玩意儿就是典型的有限状态机(finite state machine,FSM)。
2.有限状态机
2.1 什么是有限状态机
维基百科给出的定义如下:
It is an abstract machine that can be in exactly one of a finite number of states at any given time. The FSM can change from one state to another in response to some external inputs; the change from one state to another is called a transition. An FSM is defined by a list of its states, its initial state, and the conditions for each transition.
翻译过来:
有限状态机是一种抽象的机器,它在给定的任意时间内都处于 有限个状态 中的一个。有限状态机可以根据 外部输入 从一个状态切换至另一个状态;从一个状态切换至另一个状态的过程被称为 一次转换 。一个有限状态机由一组状态、起始状态和转换发生条件组成。
剖析一下上述定义,将它与我们的熔断器进行对比:首先,熔断器的状态有三个,是有限的,并且任何时间都是这三个状态;其次,熔断器会根据方法调用情况判断状态,方法调用就是外部输入;再次,当调用失败率达到阈值,熔断器就会发生状态转换,这一条也满足;最后看下三要素,熔断器的一组状态:打开、关闭和半开,起始状态:关闭,和转换发生条件即状态转换事件:方法调用。剖析完以后发现,熔断器完美的契合了有限状态机模型。
2.2 状态模式
其实有限状态机乍一听起来很玄妙,感觉是一个高大上的数学模型,但是在我们后端开发中,套用上这个模型,就是我们 设计模式 之一——状态模式。我们看下状态模式的定义:
当一个对象的内在状态被改变时,允许改变这个对象的行为,从而使得这个对象好像改变了它所属的类一样
我们剖析一下这个定义,首先,这个对象是有 状态 的;其次,这个对象的状态变化时,这个对象的 行为 也发生了变化;最后,这个对象的状态在特定情况下是可以 改变 的。剖析完定义,我们再来看下状态模式为我们解决了什么问题:状态模式让控制对象状态的逻辑语句变得简单,通过多态将状态切换的逻辑转移到接口中,从而将状态切换与实体对象进行解耦。
用代码解释一下,比如我们定义一个熔断器:
public class CircuitBreaker{
private StateEnum state;// 状态属性
public String change(Object input){// 根据input转换状态
... //do something
if(input.failed()){// 调用失败,熔断器打开
transition();
}
}
public void transition(){// 状态转换方法
if(this.state==CLOSE){
this.state==OPEN;
}
}
public void behaviour(){// 熔断器行为
if(this.state==HALF_OPEN){
... //do something
}else if(this.state==OPEN){
... //do something
}else if(this.state==CLOSE){
... //do something
}
}
}
我们可以看到,在不使用状态模式的情况下,在每一次调用我们熔断器behaviour方法的时候,都需要判断对象的状态,从而执行不同的逻辑,如果状态有七八个,那么这个方法充斥着大量ifelse,可读性差,后期维护成本也高。状态模式解耦后是什么样子的呢?Resilinence4j为我们提供了教科书式的实现,让我们走进Resilience4j的熔断器源码一探究竟。
3.Resilience4j熔断器源码
话不多说,show me the code! 先看一下熔断器接口:
public interface CircuitBreaker {
boolean isCallPermitted();
void onError(long durationInNanos, Throwable throwable);// 被保护方法调用失败执行此方法
void onSuccess(long durationInNanos);// 被保护方法调用成功执行此方法
void reset();
// 状态切换方法们
void transitionToClosedState();
void transitionToOpenState();
void transitionToHalfOpenState();
void transitionToDisabledState();
void transitionToForcedOpenState();
String getName();
CircuitBreaker.State getState();
CircuitBreakerConfig getCircuitBreakerConfig();
CircuitBreaker.Metrics getMetrics();
CircuitBreaker.EventPublisher getEventPublisher();
// 状态枚举
public static enum State {
DISABLED(3, false),
CLOSED(0, true),
OPEN(1, true),
FORCED_OPEN(4, false),
HALF_OPEN(2, true);
private final int order;
public final boolean allowPublish;
private State(int order, boolean allowPublish) {
this.order = order;
this.allowPublish = allowPublish;
}
public int getOrder() {
return this.order;
}
}
可以看到,熔断器接口基于状态模式,定义了状态模式中对象的定义,我们也可以按照这个定义,将这个接口的核心分成以下三个部分:
-
状态: 状态枚举 State
-
对象行为: onSuccess()、onError()
-
状态改变: transitionto...()
我们看下实现这个接口的类,也是真正的熔断器实体:CircuitBreakerStateMachine
针对接口中提供的三个部分,我们分别看看这个类是如何实现的:
-
状态:
private final AtomicReference<CircuitBreakerState> stateReference;
CircuitBreakerStateMachine类有一个类变量如上,这个变量保存了熔断器目前的状态。Resilience4j使用AtomicReference对该变量包装,保证对象的原子性。重点关注一下CircuitBreakerState这个类,这个是抽象类,是Resilience4j状态模式的核心,通过这个类实现了状态与实体的解耦,同时也是通过这个抽象类,通过多态实现了实体状态转换后行为的变换。我们看一下这个抽象类。
abstract class CircuitBreakerState {
CircuitBreakerStateMachine stateMachine; // 熔断器实体
CircuitBreakerState(CircuitBreakerStateMachine stateMachine) {
this.stateMachine = stateMachine;
}
abstract boolean isCallPermitted();
abstract void onError(Throwable throwable);// 行为方法
abstract void onSuccess(); // 行为方法
abstract State getState();
}
}
可以看到,这个类通过持有熔断器的实体对象的方式,与实体类进行了关联,同时提供的两个抽象方法,用于不同状态情况下对行为的不同实现。可以看到,根据熔断器状态,该抽象类分别有4个子类,对应熔断器的各个状态
-
对象行为
回到熔断器实体,我们看看行为方法里面做了什么:
public void onError(long durationInNanos, Throwable throwable) {
if (this.circuitBreakerConfig.getRecordFailurePredicate().test(throwable)) {
LOG.debug("CircuitBreaker '{}' recorded a failure:", this.name, throwable);
this.publishCircuitErrorEvent(this.name, durationInNanos, throwable);
((CircuitBreakerState)this.stateReference.get()).onError(throwable);
} else {
this.publishCircuitIgnoredErrorEvent(this.name, durationInNanos, throwable);
}
}
public void onSuccess(long durationInNanos) {
this.publishSuccessEvent(durationInNanos);
((CircuitBreakerState)this.stateReference.get()).onSuccess();
}
前面说过,onError方法是在方法调用失败后执行的方法,源码中可以看到,熔断器在这个方法中主要干了两件事儿,第一件是打印相关日志和上报异常事件,第二件事,就是调用该熔断器实体的状态实体的onError方法,也就是对应着我们上面提到的上线了CircuitBreakerState抽象类的某个状态的子类。那我们看一下OpenState这个类,它代表着熔断器打开状态:
void onError(Throwable throwable) {
this.circuitBreakerMetrics.onError();
}
可以看到,调用了Metircs的onError方法,metrics是对熔断器底层数据的整理,包括记录当前调用结果和计算失败率,代码如下:
float onError() {
int currentNumberOfFailedCalls = this.ringBitSet.setNextBit(true);
return this.getFailureRate(currentNumberOfFailedCalls);
}
看完开启状态,我们再看看关闭状态状态:
void onError(Throwable throwable) {
this.checkFailureRate(this.circuitBreakerMetrics.onError());
}
看下checkFailureRate这个方法。
private void checkFailureRate(float currentFailureRate) {
if (currentFailureRate >= this.failureRateThreshold) {
this.stateMachine.transitionToOpenState();
}
}
可以看到,这个方法的作用是检验失败率,如果失败率大于阈值,则调用状态切换方法,切换为开启状态。
从上面对比中,我们可以看出,熔断器实体在onError方法中统一调用了CircuitBreakState抽象类的onError方法,然后根据熔断器的所属状态执行相应的行为,通过这样一层抽象,状态模式帮我们把ifelse逻辑全部清除
-
状态改变
最后看一下状态改变是如何实现的。
public void transitionToForcedOpenState() {
this.stateTransition(State.FORCED_OPEN, (currentState) -> {
return new ForcedOpenState(this);
});
}
public void transitionToClosedState() {
this.stateTransition(State.CLOSED, (currentState) -> {
return new ClosedState(this, currentState.getMetrics());
});
}
public void transitionToOpenState() {
this.stateTransition(State.OPEN, (currentState) -> {
return new OpenState(this, currentState.getMetrics());
});
}
public void transitionToHalfOpenState() {
this.stateTransition(State.HALF_OPEN, (currentState) -> {
return new HalfOpenState(this);
});
}
我们可以看到,状态改变的方法做的事情很简单,调用了一个stateTransition方法:
private void stateTransition(State newState, Function<CircuitBreakerState, CircuitBreakerState> newStateGenerator) {
CircuitBreakerState previousState = (CircuitBreakerState)this.stateReference.getAndUpdate((currentState) -> {
return currentState.getState() == newState ? currentState : (CircuitBreakerState)newStateGenerator.apply(currentState);
});
if (previousState.getState() != newState) {
this.publishStateTransitionEvent(StateTransition.transitionBetween(previousState.getState(), newState));
}
}
我们可以看下这个方法除了上报事件之外,就是执行第二个变量传来的函数式接口包装的方法。回到上一层方法,可以看到,实际执行的就是创建一个对象,这个对象就是我们要切换到的状态的状态对象,并且把我们的熔断器实体传到这个状态对象中。说白了,就是把我们的熔断器交给下一个状态对象来管理。
4.总结
上面说了这么多,总结一下,我们的开发者根据有限状态机的数据模型,结合实际开发场景,提出了状态模式。在Resilience4j中,很多组件都是基于状态模式做的。这里以熔断器为例,在有限个状态的情况下,通过将状态抽象出来,与实体解耦,将实体在不同状态下的行为交给状态对象来管理,实体切换状态的时候就是状态对象创建并与实体产生关系的时候。一切都交给状态对象来管理,实体只需要做实体该做的事情。
想要了解如何使用Resilience4j吗,请移步: Resilience4j,容错,可以轻一点
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:- [译] Istio 熔断器解析
- Hystrix系列之熔断器实现原理
- 熔断器 Hystrix 源码解析 —— 执行命令方式
- [译] 3 分钟简述熔断器使用方法
- 服务容错模式:舱壁模式、熔断器的异同点
- [译] Grab 熔断器设计:如何应对突发打车峰值
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
计算机程序设计艺术:第4卷 第4册(双语版)
Donald E.Knuth / 苏运霖 / 机械工业出版社 / 2007-4 / 42.00元
关于算法分析的这多卷论著已经长期被公认为经典计算机科学的定义性描述。迄今已出版的完整的三卷组成了程序设计理论和实践的惟一的珍贵源泉,无数读者都赞扬Knuth的著作对个人的深远影响。科学家们为他的分析的美丽和优雅所惊叹,而从事实践的程序员们已经成功地应用他的“菜谱式”的解到日常问题上,所有人都由于Knuth在书中所表现出的博学、清晰、精确和高度幽默而对他无比敬仰。 为开始后续各卷的写作并更......一起来看看 《计算机程序设计艺术:第4卷 第4册(双语版)》 这本书的介绍吧!