Resilience4j熔断器-使用与源码解析
🌏 环境:
🌳 JDK11
🌱 IDEA 2019.03
🌾 Resilience4j 0.13.2
🍃 知识依赖:juc,位图
一、什么是熔断
在分布式系统中,各服务间的相互调用更加频繁,上下游调用中充满了可能性,一个服务可能会被很多其他服务依赖并调用,在这个过程中如果某个服务由于某种原因出错(业务出错、负载过高),可能会导致整个分布式调用链路失败:
图1
上面这个过程最终可能会导致全链路瘫痪(服务雪崩),此时需要一种可以解决上述问题的策略,此策略设计目标为:
- 在发现有服务调用失败后,及时计算失败率
- 失败率达到某种阈值时,切断与该服务的所有交互,服务走切断后的自定义逻辑
- 切断并且不再调用该服务后主动监听被切断的服务是否已经恢复了处理能力,若恢复,则继续让其提供服务
这个策略被放进图1中,就变成了下面这样:
图2
这个过程中,C服务在自己出问题的情况下,并不会像图1里那样仍然有大量流量打进来,也不会影响到上游服务,这个结果让调用链看起来比图1更加的稳定,这个过程就叫熔断。
针对这个过程,可以看到在C不可用时,B走了熔断后的降级逻辑,这个逻辑可以自定义,如果C在整个调用链里属于那种必须要成功的服务,那么这里的逻辑就可以是直接抛错,如果C属于那种失败了也无所谓,不影响整个业务处理,那么降级逻辑里就可以不做处理,例如下面的场景:
图3
类似这种接口,降级策略很适合不做处理,返回空信息即可,这样最坏的情况就是页面少了某个板块的信息,可能会对用户造成不太好的体验,但是不影响其对外服务,被熔断的服务恢复后页面也会重新回归正常。熔断后的降级处理方式是件值得思考的事情,熔断和降级是相互独立的概念,熔断后必然会有降级操作(哪怕直接抛异常也是一种降级策略),这个降级操作是熔断这个动作导致的,所以很多时候会把熔断和降级放在一起说,其实降级还可以由其他动作触发,比如限流后抛出“系统繁忙”,这也是一种降级策略,只不过它是由限流触发的,再比如通过开关埋点在系统负载过高时主动关停一些次要服务来提升核心功能的响应速度,这也是一种降级策略,降级是最终产物,而产生它的方式有很多种。
二、Resilience4j中的熔断器
2.1:Resilience4j是什么?
它是一个轻量、易用、可组装的高可用框架,支持熔断、高频控制、隔离、限流、限时、重试等多种高可用机制。本篇文章只关注其熔断部分。
2.2:如何使用?
通过第一部分的介绍,可以认为一个熔断器必须要具备统计单位请求内的错误率、全熔断、半熔断放量、恢复这几个流程,带着这个流程,下面来介绍下Resilience4j里熔断器的用法。
通过图2里服务B调用服务C的例子,现在利用java类来进行简单模拟下这个流程。
首先定义ServerC类,用于模拟服务C:
public class ServerC {
//使用该方法模拟服务C获取C信息的方法,假设现在服务C的getCInfo方法里有个bug,当输入的id为0时报错,其他情况正常
public String getCInfo(int id) {
if (id == 0) {
throw new RuntimeException("输入0异常");
}
return "id=" + id + "的C信息";
}
}
代码块1
再定义ServerB类,用于模拟服务B,这里给服务B调用服务C方法那里加上熔断器处理,注意这个类里的注释,会详细说明熔断器的主要配置项以及其使用方法:
public class ServerB {
private CircuitBreakerRegistry breakerRegistry;
private ServerC serverC = new ServerC(); //让服务B持有一个服务C的引用,用来表示正常服务间调用里的一个连接引用
ServerB() {
//初始化breaker注册器,可以利用该对象生产各种breaker对象(注:凡是用同一个注册器生产出来的breaker,都会继承注册器的配置属性)
breakerRegistry = CircuitBreakerRegistry.of(CircuitBreakerConfig.custom() //of方法里面放的就是breaker的配置属性对象
.enableAutomaticTransitionFromOpenToHalfOpen() //开启从全开状态经过下面的waitDurationInOpenState时间后自动切换到半开状态
.failureRateThreshold(50) //熔断器闭合状态下的错误率阈值,50表示50%,如果错误率达到这个阈值,那么熔断器将进入全熔断状态
.ringBufferSizeInClosedState(100) //熔断器闭合状态下,以该值为单位请求数,计算错误率,跟上面错误率阈值综合理解,这个值表示至少有100个请求,且错误50个以上才会触发全熔断
.ringBufferSizeInHalfOpenState(10) //熔断器半熔断状态下,以该值为单位请求数,计算错误率,跟上面错误率阈值综合理解,这个值表示至少有10个请求,且错误5个以上会再次触发全熔断,相比闭合状态,半熔断状态下更容易再次进入全熔断状态
.waitDurationInOpenState(Duration.ofMillis(1000L)) //熔断器全熔断状态持续的时间,全熔断后经过该时间后进入半熔断状态
.build());
}
//服务B通过服务C来获取到C的info信息,该方法就是用来干这个的,它会发起对服务C的调用
public String getCInfo(int id) {
//breaker对象是按照name划分全局单例的
CircuitBreaker breaker = breakerRegistry.circuitBreaker("getCInfo"); //这里给熔断器取个名,一般情况就是一个服务的path或方法名
try {
return breaker.executeCallable(() -> serverC.getCInfo(id));
} catch (CircuitBreakerOpenException e) { //一旦抛出该异常说明已经进入全熔断状态
//被熔断后的降级逻辑
return "服务C出错,触发服务B的降级逻辑";
} catch (Exception e) {
//熔断关闭或者半熔断状态下,C抛出的错误会被catch到这里
return "调用服务C出错";
}
}
public CircuitBreaker getBreaker() {
return breakerRegistry.circuitBreaker("getCInfo"); //为了方便做测试,这里返回对应的breaker对象
}
}
代码块2
上述配置的熔断器解释如下:
在熔断器闭合的情况下(也即是正常情况下),以100个请求为单位窗口计算错误率,一旦错误率达到50%,立刻进入全熔断状态,该状态下服务B不会再发生对服务C的调用,直接走自己的降级逻辑,经过1000ms后恢复为半熔断状态,此时流量开始打进服务C,此时仍然会计算错误率,只是半熔断状态下,是以10个请求为单位窗口计算的错误率,这个可以保证在服务C没有恢复正常的情况下可以更快速的进入全熔断状态。
2.3:测试-熔断器状态切换
然后开始编写测试方法,下面会通过测试方法来详细解析该熔断器的状态变迁:
public void testBreak() throws Exception {
//按照B服务里熔断器的配置,如果进行100次请求,有50次失败了,则对ServerC的调用进入全熔断状态
//1000ms后恢复为半熔断状态,半熔断状态下进行10次请求,如果有5次依然失败,则再次进入全熔断状态
for (int i = 0; i < 100; i++) {
if (i < 50) {
serverB.getCInfo(0); //前50次全部报错
} else {
serverB.getCInfo(1); //后50次全部成功
}
}
//断言:此时熔断器为全熔断状态
System.out.println(serverB.getBreaker().getState().equals(CircuitBreaker.State.OPEN));
//全熔断状态下并不会实际调用C,而是会走服务B的降级逻辑,即便我们输入的参数是对的,也一样会被降级
System.out.println(serverB.getCInfo(1));
Thread.sleep(500L);
//断言:由于全熔断状态配置的持续时间时1000ms,所以500ms过去后,仍然是全熔断状态
System.out.println(serverB.getBreaker().getState().equals(CircuitBreaker.State.OPEN));
Thread.sleep(500L);
//断言:1000ms过后,熔断器处于半熔断状态
System.out.println(serverB.getBreaker().getState().equals(CircuitBreaker.State.HALF_OPEN));
//半熔断状态下会尝试恢复,所以会实际调用C,分别输入正确和错误的参数进行测试
System.out.println(serverB.getCInfo(1));
System.out.println(serverB.getCInfo(0));
//半熔断状态下,只需要请求10次,有5次出错即可再次进入全熔断状态
for (int i = 0; i < 10; i++) {
if (i < 4) { //因为上面传过一次0了,所以这里只需要4次便可以重新回到全开状态
serverB.getCInfo(0); //前5次全部报错
} else {
serverB.getCInfo(1); //后5次全部成功
}
}
//断言:此时熔断器为全熔断状态
System.out.println(serverB.getBreaker().getState().equals(CircuitBreaker.State.OPEN));
//同样的,全熔断状态下并不会实际调用C,而是会走服务B的降级逻辑
System.out.println(serverB.getCInfo(1));
//这时静待1000ms,再次进入半熔断状态,我们尝试恢复服务C的调用
Thread.sleep(1000L);
//这时我们让其10次请求里有6次成功
for (int i = 0; i < 10; i++) {
if (i < 6) { //前6次成功
serverB.getCInfo(1);
} else { //后4次失败
serverB.getCInfo(0);
}
}
//由于10次请求里只失败了4次,达不到50%的全开阈值,所以此时会恢复
//断言:此时熔断器为闭合状态
System.out.println(serverB.getBreaker().getState().equals(CircuitBreaker.State.CLOSED));
System.out.println(serverB.getCInfo(1)); //正常输出
System.out.println(serverB.getCInfo(0)); //走普通异常逻辑
}
代码块3
最终输出如下:
true
服务C出错,触发服务B的降级逻辑
true
true
id=1的C信息
调用服务C出错
true
服务C出错,触发服务B的降级逻辑
true
id=1的C信息
调用服务C出错
可以看到,单位请求内达到错误率阈值后熔断器会进入全开状态(全熔断),全开状态下走降级逻辑,此时不再会实际请求服务C,一段时间后(全开持续时间),进入半开状态(半熔断),半开时仍然正常打入服务C,只是由于单位请求量相比闭合时更小,若服务还没恢复,计算错误率会更快达到错误率阈值而迅速进入全开状态,以此类推。如果服务已经恢复,那么将会从半开状态进入闭合状态。
2.4:测试-错误率统计方式
通过上面的测试用例可以知道触发熔断器状态切换的时机,而且闭合状态下和半熔断状态下统计错误率的单位请求数不相同,那么这个请求数量又是怎么统计的呢?如果一个请求先错误了49次,然后在第101次请求的时候再错误1次是否可以成功触发熔断器全开?如果把这49次失败往后挪一位呢?现在再来按照设想测试下其错误率的统计方式:
public void testRate() {
//首先闭合状态下单位请求仍然是100,现在让前49次全部失败
for (int i = 0; i < 100; i++) {
if (i < 49) {
serverB.getCInfo(0);
} else {
serverB.getCInfo(1);
}
}
//断言:虽然请求了100次,但是错误率并没有达到阈值(50%),所以这里仍然是闭合状态的
System.out.println(serverB.getBreaker().getState().equals(CircuitBreaker.State.CLOSED));
//这里再让其失败一次
serverB.getCInfo(0);
//断言:这里应该还是闭合状态的,按照100次单位请求来看,第一次失败的那个请求会被这次失败这个请求顶替掉(这里不理解没关系,下面有图)
System.out.println(serverB.getBreaker().getState().equals(CircuitBreaker.State.CLOSED));
}
代码块4
输出结果为:
true
true
然后我们让第一次失败的那次请求和其后面出错的请求后移一位:
public void testRate() {
//首先闭合状态下单位请求仍然是100,仍然让其错误49次,但现在让第2~50次失败
for (int i = 0; i < 100; i++) {
if (i != 0 && i < 50) { //第2~50次请求失败,总计失败49次
serverB.getCInfo(0);
} else {
serverB.getCInfo(1);
}
}
//断言:跟上面例子一样,错误率并没有达到阈值,仍然是闭合状态
System.out.println(serverB.getBreaker().getState().equals(CircuitBreaker.State.CLOSED));
//这里再让其失败一次
serverB.getCInfo(0);
//断言:这里应该是全开状态,按照100次单位请求来看,第一次成功的那个请求会被这次失败这个请求顶替掉,然后凑够50次失败请求(参考图4)
System.out.println(serverB.getBreaker().getState().equals(CircuitBreaker.State.OPEN));
}
代码块5
输出结果为:
true
true
用图来描述下导致这两种情况发生的流程:
图4
所以Resilience4j在计算失败率的时候,是会发生滑动的,错误率是根据当前滑动窗口内的请求进行计算得出的,每次请求都会导致窗口移动,都会重新计算当前失败率,这个在源码解析里会说明这是怎样的一种结构,这里简单了解即可。
三、源码解析
3.1:注册器入口
通过上面ServerB类里的使用,首先会通过CircuitBreakerRegistry.of生成一个注册器对象,然后利用注册器对象的circuitBreaker方法来生成一个实际的breaker对象,代码如下:
public interface CircuitBreakerRegistry {
//静态方法返回了InMemoryCircuitBreakerRegistry的实例
static CircuitBreakerRegistry of(CircuitBreakerConfig circuitBreakerConfig) {
return new InMemoryCircuitBreakerRegistry(circuitBreakerConfig);
}
}
代码块6
InMemoryCircuitBreakerRegistry类代码如下(已简化处理,只展示流程相关代码):
public final class InMemoryCircuitBreakerRegistry implements CircuitBreakerRegistry {
//所有的breaker被存方在这个map里,breaker按照name不同而不同,每个breaker里都有自己的一份错误率统计数据
private final ConcurrentMap<String, CircuitBreaker> circuitBreakers;
private final CircuitBreakerConfig defaultCircuitBreakerConfig; //开始的配置对象,闭合状态单位请求量、半开状态单位请求量、错误率阈值等都会放在这里面
public InMemoryCircuitBreakerRegistry(CircuitBreakerConfig defaultCircuitBreakerConfig) {
this.defaultCircuitBreakerConfig = Objects.requireNonNull(defaultCircuitBreakerConfig, "CircuitBreakerConfig must not be null");
this.circuitBreakers = new ConcurrentHashMap<>();
}
@Override
public CircuitBreaker circuitBreaker(String name) { //添加一个breaker,若存在,直接返回
return circuitBreakers.computeIfAbsent(Objects.requireNonNull(name, "Name must not be null"),
(k) -> CircuitBreaker.of(name, defaultCircuitBreakerConfig));
}
}
代码块7
这个流程很简单,就是用一个map来维护所有breaker的,所以需要注意的是,命名breaker的时候,不要携带一些id之类的字段,很容易把map撑爆。
3.2:Breaker实体-CircuitBreaker
拿到breaker实体后首先会通过其executeCallable方法执行需要被熔断的逻辑块,之前提到的所有的错误率统计、状态切换都发生在这个实体内。
public interface CircuitBreaker {
default T executeCallable(Callable callable) throws Exception{
return decorateCallable(this, callable).call(); //包装原始的callable
}
//方法包装,返回一个Callable对象,真正的业务逻辑callable在这里被执行
static Callable decorateCallable(CircuitBreaker circuitBreaker, Callable callable){
return () -> {
//全熔断状态下,这里返回false,会抛出CircuitBreakerOpenException类型的异常,ServerB里判定是否走降级逻辑就是通过catch该异常来决定的
if(!circuitBreaker.isCallPermitted()) {
throw new CircuitBreakerOpenException(String.format("CircuitBreaker '%s' is open", circuitBreaker.getName()));
}
//非全熔断状态触发下面的逻辑
long start = System.nanoTime();
try {
T returnValue = callable.call(); //执行实际的业务逻辑
long durationInNanos = System.nanoTime() - start;
circuitBreaker.onSuccess(durationInNanos); //非常关键的方法,用来累计执行成功的数量,计算错误率
return returnValue;
} catch (Throwable throwable) { //执行异常,调用onError累计出错数
long durationInNanos = System.nanoTime() - start;
circuitBreaker.onError(durationInNanos, throwable); //非常关键的方法,用来累计执行失败的数量,计算错误率
throw throwable;
}
};
}
}
代码块8
CircuitBreaker是一个接口,CircuitBreakerStateMachine是它的实现类,上述代码里比较关键的isCallPermitted、onSuccess、onError都是在这个CircuitBreakerStateMachine类里实现的。 CircuitBreakerStateMachine类比较复杂,牵扯到整个熔断器的状态切换、错误统计触发等,精简一下该类,只关注核心部分:
public final class CircuitBreakerStateMachine implements CircuitBreaker {
//熔断器的名称
private final String name;
/**
* 非常非常关键的一个属性,它是一个引用对象,CircuitBreakerState一共有以下子类:ClosedState、HalfOpenState、OpenState、DisabledState、ForcedOpenState
* 熔断器每次发生状态切换,都会new出一个新的XXState对象,让下面的引用指向新的状态对象
*/
private final AtomicReference stateReference;
//开始设置的熔断器配置,通过该对象可以拿到错误率阈值、全熔断持续状态等信息
private final CircuitBreakerConfig circuitBreakerConfig;
//&&& 事件处理器,这里不是重点,放到第四部分说,可以先忽略
private final CircuitBreakerEventProcessor eventProcessor;
//构造器
public CircuitBreakerStateMachine(String name, CircuitBreakerConfig circuitBreakerConfig) {
this.name = name;
this.circuitBreakerConfig = circuitBreakerConfig;
this.stateReference = new AtomicReference<>(new ClosedState(this)); //初始化的时候,熔断器状态都是闭合状态,所以首先new一个ClosedState并让stateReference指向它
this.eventProcessor = new CircuitBreakerEventProcessor();
}
//切换到闭合状态,new ClosedState,可以看到每个XXState对象都持有当前CircuitBreakerStateMachine对象
@Override
public void transitionToClosedState() {
stateTransition(CLOSED, currentState -> new ClosedState(this, currentState.getMetrics()));
}
//切换到全熔断状态,new OpenState,
@Override
public void transitionToOpenState() {
stateTransition(OPEN, currentState -> new OpenState(this, currentState.getMetrics()));
}
//切换到半熔断状态,new HalfOpenState,
@Override
public void transitionToHalfOpenState() {
stateTransition(HALF_OPEN, currentState -> new HalfOpenState(this));
}
//状态切换方法(也即是XXState对象切换的地方)
private void stateTransition(State newState, Function<CircuitBreakerState, CircuitBreakerState> newStateGenerator) {
//引用指向新的XXState对象
CircuitBreakerState previousState = stateReference.getAndUpdate(currentState -> {
if (currentState.getState() == newState) {
return currentState;
}
return newStateGenerator.apply(currentState);
});
if (previousState.getState() != newState) {
//&&& 状态切换事件发布,本部分忽略,参考第四部分
publishStateTransitionEvent(StateTransition.transitionBetween(previousState.getState(), newState));
}
}
//代码块8里的isCallPermitted方法,这个方法决定了是否抛出"已熔断"异常
@Override
public boolean isCallPermitted() {
//可以看到,这个解决取决于对应XXState里isCallPermitted方法的返回结果
boolean callPermitted = stateReference.get().isCallPermitted();
if (!callPermitted) {
//&&& 已熔断异常事件发布,本部分忽略,参考第四部分
publishCallNotPermittedEvent();
}
return callPermitted;
}
//代码块8里的onError方法,业务处理错误后会触发这个方法的调用
@Override
public void onError(long durationInNanos, Throwable throwable) {
//这个判断是过滤需要忽略的异常处理,一般情况下没配置的话所有异常都会走下面实际的onError逻辑
if (circuitBreakerConfig.getRecordFailurePredicate().test(throwable)) {
//&&& 处理错误事件发布,参考第四部分
publishCircuitErrorEvent(name, durationInNanos, throwable);
//可以看到,实际上onError也是调用的XXState里的onError方法
stateReference.get().onError(throwable);
} else {
//&&& 命中了可忽略的异常,忽略错误事件发布,本部分忽略,参考第四部分
publishCircuitIgnoredErrorEvent(name, durationInNanos, throwable);
}
}
//代码块8里的onSuccess方法,业务处理正常会触发这个方法的调用
@Override
public void onSuccess(long durationInNanos) {
//&&& 处理正常事件发布,本部分忽略,参考第四部分
publishSuccessEvent(durationInNanos);
//同样的,onSuccess也是调用的XXState里的onError方法
stateReference.get().onSuccess();
}
}
代码块9
3.3:状态类
通过上面的代码可以知道isCallPermitted、onSuccess、onError这三个方法实际上都是调用对应XXState对象里的方法,下面来看下ClosedState、OpenState、HalfOpenState这三个状态对象里有关这三个方法的实现(因为上面的测试用例只涉及这三种状态的互转,实际上这三种状态也是最常用的,所以为了避免混乱,只展示这三种,所有状态类均继承自CircuitBreakerState抽象类)
3.3.1:ClosedState
闭合状态时初始状态,中途只会由半熔断状态切换而来,正常情况下都是闭合状态,代码如下:
final class ClosedState extends CircuitBreakerState {
//用来度量错误率的对象
private final CircuitBreakerMetrics circuitBreakerMetrics;
//就是配置里的failureRateThreshold属性,闭合状态时的错误率阈值(第二部分的测试用例中是50)
private final float failureRateThreshold;
//参考代码块9的CircuitBreakerStateMachine构造器中初始化stateReference时,初始态都是闭合状态,最初都是通过该方法完成初始化的
ClosedState(CircuitBreakerStateMachine stateMachine) {
this(stateMachine, null);
}
//这个构造器是状态转换时触发的,参考代码块9里的transitionToClosedState方法
ClosedState(CircuitBreakerStateMachine stateMachine, CircuitBreakerMetrics circuitBreakerMetrics) {
super(stateMachine);
//拿到熔断器的配置
CircuitBreakerConfig circuitBreakerConfig = stateMachine.getCircuitBreakerConfig();
if(circuitBreakerMetrics == null){
//初始化metrics对象,传进去的是闭合状态时计算错误率的单位请求数(第二部分的测试用例中是100)
this.circuitBreakerMetrics = new CircuitBreakerMetrics(
circuitBreakerConfig.getRingBufferSizeInClosedState());
}else{
//中途进行状态转换,调用的都是这里的逻辑,利用circuitBreakerMetrics的copy方法,重新赋值给circuitBreakerMetrics属性,暂时忽略,参考第3.4部分
this.circuitBreakerMetrics = circuitBreakerMetrics.copy(circuitBreakerConfig.getRingBufferSizeInClosedState());
}
//赋值错误率阈值
this.failureRateThreshold = stateMachine.getCircuitBreakerConfig().getFailureRateThreshold();
}
@Override
boolean isCallPermitted() {
//闭合状态下返回true,不会触发降级逻辑(ps:只有在全熔断状态下才会返回true)
return true;
}
@Override
void onError(Throwable throwable) {
// 闭合状态下,onerror需要记录错误率,注:circuitBreakerMetrics的onError方法会记录一笔错误的记录,并把当前的错误率返回
checkFailureRate(circuitBreakerMetrics.onError());
}
@Override
void onSuccess() {
// 闭合状态下,onerror需要记录成功数,注:circuitBreakerMetrics的onSuccess方法会记录一笔正确的记录,并把当前的错误率返回
checkFailureRate(circuitBreakerMetrics.onSuccess());
}
//根据当前的错误率,决定是否切到半熔断状态
private void checkFailureRate(float currentFailureRate) {
if (currentFailureRate >= failureRateThreshold) { //这里判断当前错误率是否超过阈值
// 利用CircuitBreakerStateMachine的transitionToOpenState方法,将状态对象转换成OpenState
stateMachine.transitionToOpenState();
}
}
}
代码块10
3.3.2:OpenState
一般全熔断状态会从闭合或者半熔断状态里切换而来,它的代码如下:
final class OpenState extends CircuitBreakerState {
//根据全熔断持续时间推出的进入半熔断状态的时间
private final Instant retryAfterWaitDuration;
//同样是用来度量错误率的对象,该对象就是上一个State对象里的Metrics对象
private final CircuitBreakerMetrics circuitBreakerMetrics;
OpenState(CircuitBreakerStateMachine stateMachine, CircuitBreakerMetrics circuitBreakerMetrics) {
super(stateMachine);
//就是配置里的waitDurationInOpenState属性,全熔断持续时间(第二部分的测试用例中是100ms)
final Duration waitDurationInOpenState = stateMachine.getCircuitBreakerConfig().getWaitDurationInOpenState();
//当前时间加上持续时间,就是切换至半熔断状态的时机
this.retryAfterWaitDuration = Instant.now().plus(waitDurationInOpenState);
//直接用之前的circuitBreakerMetrics对象
this.circuitBreakerMetrics = circuitBreakerMetrics;
//如果配置了自动切换半熔断状态的开关为true,则会发起一个延时任务,用来主动切换状态
if (stateMachine.getCircuitBreakerConfig().isAutomaticTransitionFromOpenToHalfOpenEnabled()) {
AutoTransitioner.scheduleAutoTransition(stateMachine::transitionToHalfOpenState, waitDurationInOpenState);
}
}
@Override
boolean isCallPermitted() {
// 如果全熔断状态持续时间超出目标范围,则认为现在可以切换为半熔断状态,然后返回true
if (Instant.now().isAfter(retryAfterWaitDuration)) {
stateMachine.transitionToHalfOpenState();
return true;
}
circuitBreakerMetrics.onCallNotPermitted(); //记录一次NotPermitted(简单的累加)
return false; //全熔断状态,直接返回false,表示已被熔断,让调用方抛出CircuitBreakerOpenException异常
}
@Override
void onError(Throwable throwable) {
//理论上处于全熔断状态,isCallPermitted返回false,onError不会被触发(参考代码块8里的decorateCallable方法)
//但是存在一种特殊的情况,假设有俩线程,线程1执行的时候还是闭合状态,isCallPermitted返回true,这时线程2里触发了熔断阈值
//线程2把stateReference的指向置为OpenState,这时线程1继续往下执行,触发的onError其实是OpenState里的onError(也即是本例中的这个方法)
//全熔断状态下即便是上面这种临界情况发生,这次失败也会被统计上去
circuitBreakerMetrics.onError();
}
/**
* Should never be called when isCallPermitted returns false.
*/
@Override
void onSuccess() {
//跟onError一样,有概率会访问到
circuitBreakerMetrics.onSuccess();
}
}
代码块11
3.3.3:HalfOpenState
半熔断状态一定是由全熔断切换出来的,来看下它的代码:
final class HalfOpenState extends CircuitBreakerState {
//同样是用来度量错误率的对象
private CircuitBreakerMetrics circuitBreakerMetrics;
//同样是配置里的failureRateThreshold属性
private final float failureRateThreshold;
HalfOpenState(CircuitBreakerStateMachine stateMachine) {
super(stateMachine);
CircuitBreakerConfig circuitBreakerConfig = stateMachine.getCircuitBreakerConfig();
//初始化度量对象,相比闭合状态,这里传入的是ringBufferSizeInHalfOpenState(第二部分的测试用例中是10)
this.circuitBreakerMetrics = new CircuitBreakerMetrics(
circuitBreakerConfig.getRingBufferSizeInHalfOpenState());
//闭合状态和半开状态共用同一个错误率阈值(第二部分的测试用例中是50)
this.failureRateThreshold = stateMachine.getCircuitBreakerConfig().getFailureRateThreshold();
}
@Override
boolean isCallPermitted() {
//跟闭合状态一样,返回true
return true;
}
@Override
void onError(Throwable throwable) {
// 跟闭合状态一样,要记录和判断当前的错误率(来决定是恢复闭合状态还是进入全熔断状态)
checkFailureRate(circuitBreakerMetrics.onError());
}
@Override
void onSuccess() {
// 同上
checkFailureRate(circuitBreakerMetrics.onSuccess());
}
//通过该方法,判断错误率,决定是否恢复为闭合状态或者再次进入全熔断状态
private void checkFailureRate(float currentFailureRate) {
//Metrics返回-1表示请求量表示还没有达到单位请求量(ringBufferSizeInHalfOpenState)
//下面的逻辑可以看出,在半熔断状态下,经过ringBufferSizeInHalfOpenState次请求后根据错误率判断,就可以决定出下一步切换到哪个状态了
if (currentFailureRate != -1) {
//当前错误率如果再次超出阈值,则再次进入全熔断状态
if (currentFailureRate >= failureRateThreshold) {
stateMachine.transitionToOpenState();
} else { //否则恢复为闭合状态
stateMachine.transitionToClosedState();
}
}
}
}
代码块12
3.3.4:状态间的切换关系
上面三种状态的切换关系如下:
图5
在这些状态中,最初为熔断闭合状态,ServerB的所有请求正常访问ServerC,ServerC报错,错误率累计达到50%后触发熔断全开状态,此时Server对ServerC发出的请求将走ServerB的降级逻辑,不再实际访问ServerC的方法,这个状态会持续waitDurationInOpenState这么久(测试用例中是1000ms),然后进入熔断半开状态,此时跟闭合状态一样,ServerB的所有请求仍会正常访问ServerC,不同的是半开状态下只需要满足ringBufferSizeInHalfOpenState次调用(测试用例中是10次),就可以直接判断错误率是否达到阈值,这点可以在代码块12里的checkFailureRate方法体现,图5中可以看到,如果未达到错误阈值表示ServerC已恢复,则可以关闭熔断,否则再次进入全熔断状态。
3.3.5:度量对象(CircuitBreakerMetrics)的传递
这个对象在3.4中会详细说明,目前只需要知道该类用于做错误统计用,错误率计算的核心,核心方法为onError和onSuccess,这俩方法用于错误/正确请求的触发点,用于触发CircuitBreakerMetrics对象对错误率的统计。
通过代码块10、11、12可以看到CircuitBreakerMetrics对象的流向,首先初始化的时候是调用ClosedState第一个构造器触发第二个构造器,第二个构造器会new一个CircuitBreakerMetrics,传过去的size为ringBufferSizeInClosedState,然后由ClosedState切换至OpenState状态时,其CircuitBreakerMetrics会被传递给OpenState对象,根据代码块11可以知道,OpenState利用该对象统计熔断期间被熔断的次数,然后OpenState切换至HalfOpenState时,HalfOpenState没有接受CircuitBreakerMetrics对象的构造器,不管由谁切换到半开状态,CircuitBreakerMetrics对象都是全新的,由代码块12可知,初始化CircuitBreakerMetrics对象时传过去的size就是ringBufferSizeInHalfOpenState。
CircuitBreakerMetrics对象的传递以及传递后在State对象里所做的操作:
图6
图6根据代码块10、11、12画出,简单体现了Metrics对象的生成以及流向,以及这个对象在各State对象里所做的主要操作。通过图6可以看出实际产生新的Metrics对象的地方为闭合态和半开态,因为这俩地方是需要做错误统计的,需要全新的Metric对象,全开态下仅接收前一状态的Metrics对象,在命中熔断后对其内部numberOfNotPermittedCalls(不是很懂这个属性,简单的累加,连用到的地方都没,可能仅仅是做个熔断数统计让业务方获取的吧,做监控可以用),在半开态再次进入闭合态时,其Metrics仍然被传递给了闭合态,由代码块10可知,如果传了Metrics对象,闭合态在产生新的Metrics对象时,会通过copy方法来产生,这个方法在3.4会详细说明,简单来说就是把前一个状态(只可能是半开态)的Metrics里的请求计数同步到它自己的Metrics里,这样做有一个好处,就是新的闭合态不用重新累计错误率了,以单元测试所配的参数试想一下,如果在半开态下,进行了10次请求,发生了4次错误,此时会切回闭合态,闭合态copy了这10次请求的数据,那么只需要再经过90次请求和46次错误便可以再次进入全熔断状态(其实就是保证了状态的平滑切换,不丢失之前已经统计了的数据)。
3.4:错误统计
3.4.1:CircuitBreakerMetrics
通过3.3的了解,闭合和半开时的请求状态计数都是通过CircuitBreakerMetrics对象来完成的,现在来看下这个类里都干了些什么:
class CircuitBreakerMetrics implements CircuitBreaker.Metrics {
//通过3.3的代码块可知,该值就是闭合或者半开状态下设置的ringBufferSizeInClosedState和ringBufferSizeInHalfOpenState
//表示一次请求窗口的大小,测试用例中就是闭合时的100以及半开时的10,通过图4和下方的getFailureRate方法可以知道,
//至少要累计完成一个请求窗口的请求量后才会实际计算错误率
private final int ringBufferSize;
//实际用来记录一个请求窗口的请求统计数据的结构,本节不深究,详细参考3.4.2
private final RingBitSet ringBitSet;
//全开状态下累计被熔断的请求个数,触发点参考图6以及代码块11
private final LongAdder numberOfNotPermittedCalls;
//构造器1,参考图6,在最开始的闭合状态以及后续的半开状态下初始化Metrics对象用的就是该构造器
CircuitBreakerMetrics(int ringBufferSize) {
this(ringBufferSize, null);
}
//参考图6,由半开转到闭合态的时候,是通过该方法进行初始化的
public CircuitBreakerMetrics copy(int targetRingBufferSize) {
return new CircuitBreakerMetrics(targetRingBufferSize, this.ringBitSet); //这里会把当前Metrics对象里的ringBitSet传递下去
}
//构造器2
CircuitBreakerMetrics(int ringBufferSize, RingBitSet sourceSet) {
this.ringBufferSize = ringBufferSize;
if(sourceSet != null) {
//通过copy初始化会走这里(每次的半开态转闭合态),将原来Metrics对象里的ringBitSet传递下去(用来初始化新的请求窗口)
this.ringBitSet = new RingBitSet(this.ringBufferSize, sourceSet);
}else{
//非copy新建Metrics对象(每次的半开态和最初的闭合态)
this.ringBitSet = new RingBitSet(this.ringBufferSize);
}
this.numberOfNotPermittedCalls = new LongAdder();
}
//onError和onSuccess的触发点参考3.3里的State类
//当请求发生错误时触发该方法,该方法用于记一次失败,然后把当前错误率返回
float onError() {
int currentNumberOfFailedCalls = ringBitSet.setNextBit(true); //通过ringBitSet的setNextBit置为true,算作一笔失败的记录
return getFailureRate(currentNumberOfFailedCalls);
}
//当请求正常时触发该方法,该方法用于记一次成功,然后把当前错误率返回
float onSuccess() {
int currentNumberOfFailedCalls = ringBitSet.setNextBit(false); //通过ringBitSet的setNextBit置为false,算作一笔成功的记录
return getFailureRate(currentNumberOfFailedCalls);
}
//全开状态下累计被熔断的请求个数
void onCallNotPermitted() {
numberOfNotPermittedCalls.increment();
}
//通过getFailureRate计算错误率并返回
@Override
public float getFailureRate() {
return getFailureRate(getNumberOfFailedCalls());
}
//下方注释中的窗口大小就是ringBufferSize属性
//该方法通过ringBitSet对象返回当前请求窗口内发生请求的总次数,如果达到了ringBufferSize次,则这个值就恒等于ringBufferSize
@Override
public int getNumberOfBufferedCalls() {
return this.ringBitSet.length();
}
//该方法通过ringBitSet对象返回当前请求窗口内发生错误的次数
@Override
public int getNumberOfFailedCalls() {
return this.ringBitSet.cardinality();
}
//错误率计算方法
private float getFailureRate(int numberOfFailedCalls) {
//若请求还没有完成一个请求窗口,则返回-1
if (getNumberOfBufferedCalls() < ringBufferSize) {
return -1.0f;
}
//完成了一次请求窗口,才会真正计算错误率
return numberOfFailedCalls * 100.0f / ringBufferSize;
}
}
代码块13
通过上面的代码可以知道最终统计错误数的是在RingBitSet结构中,下面来仔细了解下这个类~
3.4.2:位图&BitSetMod
了解RingBitSet之前,先来了解一种数据结构-位图,如果已经了解过位图,那么可以直接去看RingBitSet。
RingBitSet持有一个BitSetMod对象,BitSetMod基于位图实现,位图是怎样的一种结构呢?先看下图7,然后再去解析它的源码实现。
图7
通过上图可知,位图就是利用数组内每个元素的bit位存入一个标记,标记只有存在或者不存在(对应二进制的0和1),这样就可以做到用一个long型的数字就可以产生出64个标记信息,非常适合数据量庞大而判断状态少的应用场景,比如判断一个词语是否是屏蔽词,首先屏蔽词状态只有两种:命中or不命中,但是屏蔽词可能是个非常庞大的集合,如果一个个拿来比较,效率完全保证不了,那么就可以利用这个数据结构来解决这类问题,可以首先把所有的屏蔽词放到一个位图结构里,如果有相同的词语,只需要简单的两部运算就可以拿到是否命中结果,构建这个位图结构的过程如下:
图8
通过上图,屏蔽词位图结构就构建好了,如果有个词语需要判定是否命中屏蔽词,只需要让这个词语通过上面的哈希算法计算出哈希值,然后找到对应的数组下标,通过位运算算出其所在位置,将该位置的值取出,如果是0,则认为没有命中,1则认为命中。
以上就是位图结构,通过上面的例子,可以认为同一个值一定命中位图里的同一个位置,那么抽象成熔断器的错误率,错误状态只有0和1,1表示错误,0表示正确,给每次请求编号,当成是图8中的哈希值,相同编号的请求一定会落到同一个位置,现在不理解没关系,这个要结合RingBitSet一起看,目前只需要理解位图特性即可。
Resilience4j里通过BitSetMod简单实现了一个位图结构,来看下代码(注:代码里有大量位运算,过程说明都写在了注释里):
/**
* 下方为源码注释↓↓
* {@link io.github.resilience4j.circuitbreaker.internal.BitSetMod} is simplified version of {@link java.util.BitSet}.
* It has no dynamic allocation, expanding logic, boundary checks
* and it's set method returns previous bit state.
*/
public class BitSetMod {
/**
* 1.此类是一种怎样的数据结构?
* 根据原有注释,可知这是一个简易版的BitSet,即位图结构,可以通过图7更为直观的了解下该结构
* 由图7可知,位图分为x,y轴,y轴就是本类的long型的数组(words),其中内部每一个元素都包括64个bit,因此bit位横向扩展就是x轴(x轴大小恒等于64)
* 如果要标记一个数字是否存在于图中,只需要先找到所属的y轴位置(即对应的words下标),然后再计算出它应该出现的x轴long型数字中哪个bit位,
* 然后判断该bit位是否已被标记为true,若是,则返回已存在,否则返回不存在。
* <p>
* <p>
* 2.位运算
* 简单了解下本类中出现的位运算,任意两个数的乘法或除法都可以用<<(左移)或>>(右移)来表示
* 例:
* a * b == a << log2(b)
* a / b == a >> log2(b)
* 本例中的ADDRESS_BITS_PER_WORD属性,其实就是long型位数以2为底的对数,即log2(64) = 6
* 那么接下来代码中针对ADDRESS_BITS_PER_WORD的位运算就可以简单理解为乘以/除以64了
*/
//long类型bit位的对数,即log2(64)=6,利用该值可以进行简单的乘除法的位运算
private final static int ADDRESS_BITS_PER_WORD = 6;
//最终可以存放的总位数,计算方式:words.length * 64(每个long型有64位,利用数组长度乘以位数,就计算出了位图的总位数)
//用位运算表示为:words.length << 6(右移表示乘法,右移6位表示乘以2^6,即words.length * 64)
private final int size;
//位图数组,long型,64个bit位
private final long[] words;
//构造器,传入位图的容量大小
public BitSetMod(final int capacity) {
//计算数组大小(即y轴大小),根据上面对位图的基本解释,可以知道,y轴是一个long型数组,
//而每次一个数字进来,会首先找到y轴所属的位置,那么这个数组得多大才合适呢?我们知道x轴固定为64个,
//也就是说正常情况下,任意数字进来后都会被分到某个y轴对应的long型数字里的某一位,那么y轴大小就很好推算了,
//利用给出的容量大小(这个表示任意数最大时为多大),除以64进行平均分组,这样不管传的任意数为多大,始终都可以找到对应的[x,y],且不会越界
int countOfWordsRequired = wordIndex(capacity - 1) + 1;
//上面说过,size就是位图里所有位数,即x * y,也就是words.length * 64,用位运算表示为:words.length << 6
size = countOfWordsRequired << ADDRESS_BITS_PER_WORD;
//最终((capacity - 1)/64)+1就是y轴数组大小,初始化数组即可
words = new long[countOfWordsRequired];
//到这里,一个位图对象就被我们创建好了,数组(y轴)是它实际的实体,x轴是数组里long型数字的二进制位(64)
}
private static int wordIndex(int bitIndex) {
//下面这个位运算等同于:bitIndex/64
return bitIndex >> ADDRESS_BITS_PER_WORD;
}
//开始设置数字信息,bitIndex为目标放置位置,value为值(0或1)
public int set(int bitIndex, boolean value) {
// 利用位置数字除以64,推算出它对应的y轴下标
int wordIndex = wordIndex(bitIndex);
// 注:下面的代码都是位运算,开始前先来了解一下如何定位某个数字的二进制第n位上的数字是0还是1
// 将1右移bitIndex位,可以得到一个类似1000000的二进制数字,利用这个数字跟原来的数字本身做位与运算,可以推算出原数第bitIndex位上的数字是1还是0
// 举个例子,我想知道下面这个二进制数字中第5位的数字是0还是1(跟十进制一样,位数是从右往左数,位数最高的在最左边,下标从0开始算起)
// 假设该二进制数为λ,设:λ=101010101
// 现在将1右移5位得到bitMask,它用二进制表示为:100000,1的位置正好位于第5位(从右往左,下标从0算起)
// 利用λ跟bitMask进行位与运算:
// 101010101(λ)
// &
// 000100000(bitMask)
// ------------------
// 000000000(位与结果)
// 由这个过程可以发现,λ的第5位如果是0,位与后的结果也是0,如果是1,那么位与运算后的结果肯定是不等于0的,通过这种方式,我们就可以利用1右移的方式,
// 知道λ的第n位是0还是1
//
// 通过上面的例子,可以知道,任意数与1右移后的数字(bitMask)进行位与运算的结果要么不等于0,要么等于0,因为1右移n位后生成的二进制数在其n位上一定为1,
// 其余位置一定为0,0&0、0&1均为0,所以最后的结果要么是000000000,要么还等于1右移后的那个数:000010000,这取决于原始数字里第n位上是否是1,
// 如果是1,则相与后的结果值一定不等于0,反之则等于0
// 结合上面所有的描述,这里可以再思考一个问题,为什么位不会相互覆盖?比如我传了一个bitIndex为100,long型1<<100等价于1<<36(以64为模轮回),那么当我传100的时候岂不是会覆盖掉传36时那次做标记?
// 这个问题答案是否定的,因为在最初的时候就已经把bitIndex按照64为单位进行相除计算出下标了,也就是说bitIndex等于100那次,跟bitIndex等于36那次,不在一个下标里(不在一个次元)
// 根据这些规则,下面的代码就好理解了。
long bitMask = 1L << bitIndex;
int previous = (words[wordIndex] & bitMask) != 0 ? 1 : 0; //把该位置上当前的值(0或1)赋值给previous(也就是最后返回出去的结果)
if (value) {
// 重新赋值,注意,这里是原值跟bitMask进行或运算,意味着目标位的值会直接变成1,其余位置的值均不变
words[wordIndex] = words[wordIndex] | bitMask;
// 结合例子,参考下面这个过程更容易理解
// 101010101
// |
// 000100000
// ---------
// 101110101
} else {
// value等于false的时候,bitMask取反后跟原值进行与运算,跟上面相反,这是把目标位变成0
words[wordIndex] = words[wordIndex] & ~bitMask;
// 结合例子,参考下面这个过程更容易理解
// 101010101
// &
// 111011111(bitMask的反码)
// ---------
// 101010101
}
return previous;
}
int size() {
//返回位图里的总位数
return size;
}
boolean get(int bitIndex) {
// 注:如果对下方的右移等操作还不是很了解,请先看set方法里的注释
// 首先还是利用位置数字除以64,推算出它对应的y轴下标
int wordIndex = wordIndex(bitIndex);
//如果set里的位运算理解了,下面这个很容易理解,这个流程跟set方法里获取previous一样
long bitMask = 1L << bitIndex;
return (words[wordIndex] & bitMask) != 0; //大于0时返回true,表示目标位是1,否则返回false,目标位是0
}
}
代码块14
上面是Resilience4j针对位图的简单实现,它负责存储单位请求内的错误/成功标志。
3.4.3:RingBitSet
之前说过,最终请求被放到了一个环形结构里才对,沿着环执行一周就是一次单位请求,回看下图4,其实第101次请求就是顶替掉第一次请求的结果罢了,现在把图4中以100为请求窗口弯曲成一个环,假如第一次请求是失败的,第101次请求是成功的(绿色背景表示成功的请求,红色背景表示失败的请求):
图9
如何利用位图结构记录每次请求的错误/成功标记然后再实现图9里的环形结构呢?Resilience4j通过RingBitSet来实现,来看下它的代码:
public class RingBitSet {
//单位请求数,根据State类初始化RingSet时给的size值,可以确定该值就是各种ringBufferSize
private final int size;
//真正存放错误率的位图结构
private final BitSetMod bitSet;
//在完成一个请求窗口后,该值为false,表示请求已满一次请求窗口
private boolean notFull;
//给请求编号,方便位图计算位置
private int index = -1;
//请求数量,最终等于size
private volatile int length;
//当前请求窗口内的错误数,就是利用这个数实时计算错误率的(参考CircuitBreakerMetrics.getNumberOfFailedCalls)
private volatile int cardinality = 0;
RingBitSet(int bitSetSize) {
notFull = true;
size = bitSetSize;
bitSet = new BitSetMod(bitSetSize);
}
//携带RingBitSet参数的构造器会把sourceSet里的统计数据赋值给新的RingBitSet(继承其请求数、错误率等)
//调用该构造器的触发点在CircuitBreakerMetrics.copy中触发,通过图6可知,每次由半开状态转到闭合状态时,都会调用copy方法,
//让新的闭合态继承上次半开态的请求量和错误率,这是合理的,比较平滑无损的过度到闭合态。
RingBitSet(int bitSetSize, RingBitSet sourceSet) {
this(bitSetSize);
int targetLength = Integer.min(bitSetSize, sourceSet.length);
int sourceIndex = sourceSet.index;
int forwardIndex = sourceSet.size - sourceIndex;
for (int i = 0; i < targetLength; i++) {
this.setNextBit(sourceSet.bitSet.get(sourceIndex));
// looping sourceIndex backwards without conditional statements
forwardIndex = (forwardIndex + 1) % sourceSet.size;
sourceIndex = (sourceSet.size - forwardIndex) % sourceSet.size;
}
}
//非常非常重要的方法,它的触发点在CircuitBreakerMetrics的onError和onSuccess,主要用于记录错误率
public synchronized int setNextBit(boolean value) {
increaseLength();
//环形结构依靠这里来实现,index永远在0~size间循环累加,类似:[0,1,2,3...99,0,1,2,3...99]
index = (index + 1) % size;
//利用位图,将本次的错误/成功标记设置到对应index的位置上,
//并且拿到当前index对应上次请求窗口中同样为index位置的请求结果previous,至于为啥要拿到这个值,参考下方的逻辑
int previous = bitSet.set(index, value);
//本次请求结果
int current = value ? 1 : 0;
//下面这一步就是刷新错误数的,计算方式为:减去同位置上个请求窗口的请求结果,然后加上这次的请求结果
//举个例子,假设单位请求窗口是100,第一个请求窗口的第一次请求错误,index=0的位置被标为1,第101次请求,也就是第二个请求窗口的第一次请求,
//意味着index仍然为0,那么第101次请求的结果就会覆盖掉第1次请求的那个结果,以此来完成窗口滚动(参考图9)
cardinality = cardinality - previous + current;
return cardinality;
}
//返回当前请求窗口内的错误总量
public int cardinality() {
return cardinality;
}
public int size() {
return bitSet.size();
}
public int length() {
return length;
}
@Override
public String toString() {
StringBuilder result = new StringBuilder();
for (int i = 0; i < size; i++) {
result.append(bitSet.get(i) ? '1' : '0');
}
return result.toString();
}
synchronized int getIndex() {
return index;
}
//累加当前请求窗口内的请求量,当完成一次单位请求窗口时,length恒等于单位请求窗口大小(size)
private void increaseLength() {
if (notFull) {
int nextLength = length + 1;
if (nextLength < size) {
length = nextLength;
} else {
length = size;
notFull = false;
}
}
}
}
代码块15
四、总结
Resilience4j通过CircuitBreakerStateMachine来独立出一个熔断器,其内部持有一个CircuitBreakerState对象的引用,在错误率达到某个阈值时,会发生状态切换,CircuitBreakerState的引用会指向新的状态对象。每个状态对象持有一个CircuitBreakerMetrics对象,用于做实时统计和错误率监听使用,CircuitBreakerMetrics对象通过RingBitSet来完成单位请求窗口的错误率统计,这个统计是实时的,每次请求都会触发一次错误率的判断。RingBitSet通过Resilience4j自己实现的一个轻量级的位图结构BitSetMod来标记请求错误/成功,顺便说下,这里通过RingBitSet来保证环形结构,而位图只负责存储请求结果,那么既然这样,我用普通的数组或者其他的可以通过下标获取数值的集合结构也可以实现啊,为什么一定要用位图呢?猜测是位图既可以保证跟数组一样高效,都是O(1)的复杂度,又可以节省存储空间,比如我的单位请求是1w次,如果是数组结构,虽然效率跟位图一样高,但是数组却需要存1w个0或1这样的数组,即便用byte类型的数组,每个数组元素都浪费了7个bit位。其他集合就更不用说了,效率无法保证,其次他们浪费的内存比单纯数组要高,所以,类似这种只有true或false的数据的存储,位图再适合不过了。
感觉有些地方说的不太清晰,待后续改进描述方式。