内容简介:上一篇介绍了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 熔断器设计:如何应对突发打车峰值
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。