不只是容错-从熔断器看有限状态机

栏目: 后端 · 发布时间: 4年前

内容简介:上一篇介绍了Resilience4j的基本用法,提到过这个容错框架不只是容错,它的代码质量很高,有很多可以学习借鉴的地方。今天,我们从熔断器源码出发,一起探寻一下这个框架的核心组件的奥妙。很多人都了解熔断器,它在程序中就相当于电路中的保险丝,当短路发生的时候,保险丝熔断,从而保护电路不会被烧坏,同样的,程序中的熔断器也是在下游服务发生错误的时候进行熔断,从而停止对下游服务的调用,起到保护下游服务和系统的作用。其核心思想就是避免因不断重试导致的错误放大,及时止损。

上一篇介绍了Resilience4j的基本用法,提到过这个容错框架不只是容错,它的代码质量很高,有很多可以学习借鉴的地方。今天,我们从熔断器源码出发,一起探寻一下这个框架的核心组件的奥妙。

1.熔断器原理

很多人都了解熔断器,它在程序中就相当于电路中的保险丝,当短路发生的时候,保险丝熔断,从而保护电路不会被烧坏,同样的,程序中的熔断器也是在下游服务发生错误的时候进行熔断,从而停止对下游服务的调用,起到保护下游服务和系统的作用。其核心思想就是避免因不断重试导致的错误放大,及时止损。

不只是容错-从熔断器看有限状态机

上面这个张图想必大家都很熟悉,三个节点CLOSED、HALF_OPEN和OPEN标识着熔断器的三个状态,关闭、半开和打开,熔断器就是围绕着这三个状态实现接口保护和访问屏蔽的。对于Resilience4J来说,有几个参数是需要提前说明的:

  • 败率阈值(failureRateThreshold)

  • 关闭状态下的访问次数 ( ringBufferSizeInClosedState)

  • 半开状态下的访问次数 (ringBufferSizeInHalfOpenState)

  • 开启状态的持续时间(waitDurationInOpenState)

熔断器的实现原理也就清晰明了了:

  1. 熔断器初始是关闭状态的,当访问次数达到关闭状态下的访问次数时,熔断器开始计算失败率,即失败调用次数与总调用次数的比值

  2. 当失败率大于失败率阈值时,熔断器打开,所有调用都会直接被熔断,不会再调用被保护的服务。 在经过我们设置的时间之后,熔断器会进入半开状态。

  3. 半开状态的熔断器会重新尝试关闭熔断器,并放行我们配置的访问次数的调用,然后对调用结果再次计算失败率,如果失败率依然大于阈值,则熔断器继续回到开启状态,否则熔断器关闭。

看完熔断器的原理,我们可以看到,熔断器都是围绕着几个状态来回转换实现功能的,有些小可爱可能发现,这个玩意儿就是典型的有限状态机(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

针对接口中提供的三个部分,我们分别看看这个类是如何实现的:

  1. 状态:

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个子类,对应熔断器的各个状态

  1. 对象行为

回到熔断器实体,我们看看行为方法里面做了什么:


 

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逻辑全部清除

  1. 状态改变

最后看一下状态改变是如何实现的。


 

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,容错,可以轻一点

不只是容错-从熔断器看有限状态机


以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Java常用算法手册

Java常用算法手册

2012-5 / 59.00元

《Java常用算法手册》分三篇,共13章,分别介绍了算法基础、算法应用和算法面试题。首先介绍了算法概述,然后重点分析了数据结构和基本算法思想;接着,详细讲解了算法在排序、查找、数学计算、数论、历史趣题、游戏、密码学等领域中的应用;最后,列举了算法的一些常见面试题。书中知识点覆盖全面,结构安排紧凑,讲解详细,实例丰富。全书对每一个知识点都给出了相应的算法及应用实例,虽然这些例子都是以Java语言来编......一起来看看 《Java常用算法手册》 这本书的介绍吧!

HTML 压缩/解压工具
HTML 压缩/解压工具

在线压缩/解压 HTML 代码

图片转BASE64编码
图片转BASE64编码

在线图片转Base64编码工具

Base64 编码/解码
Base64 编码/解码

Base64 编码/解码