分享

稳定性五件套-熔断的原理和实现

 编程一生 2022-03-09

背景

打算用五篇的篇幅介绍一下限流、熔断、降级、隔离、超时重试。继《稳定性五件套-限流的原理和实现》,这是第二篇。主要说一说熔断。

熔断的概念

上篇提到过我对限流和熔断区别的理解。限流作用是防御上游流量超过处理能力的手段,熔断作用是容错下游的快速失败手段。

超时重试也是容错下游的手段。也经常有人认为熔断和超时重试是一回事,或者需要统一来看,因为都是可以基于熔断器来实现。但我认为它们有本质的区别,熔断针对的是下游的各种错误:超时是一种情况;错误率过高也是一种情况;还有某些特定场景需要对一种特定的返回码做熔断,比如服务拒绝请求。

总而言之,超时重试和熔断有两点主要的不同。

第一,超时重试和熔断要解决的问题不同。

熔断是解决服务级联故障的问题;超时重试是解决分布式系统的三态问题。分布式系统的三态是成功、失败和超时。对于超时,如果重试成功,最终是成功的。如果重试之后都失败,那就失败的。这样三态问题可以转换为两态。

第二,超时重试是针对单个请求的处理。而熔断是针对一类或者一组请求的处理。比如一般熔断要结合计数器,当错误超过阈值才熔断。

举个生活中的限流例子:

上篇提到小A经过一段时间终于找到了心仪的女朋友。但是实际上他心仪的女朋友并不只一个。而是有5个。为了显示对女朋友的喜爱和尊重,他女朋友们发过来的消息他决定基本采取秒回的方式。所以小A很忙。

小A同时开着5个消息窗口。实时去关注这些窗口的动态。如果他一些女朋友有段时间很爱聊天。人的精力是有限的,是吧。所以他就采用一定的限流策略。对于其中任意一个女朋友来说:如果1分钟内超过了3条消息需要回复,他就只回复其中的一条。

举个生活中的熔断例子:

小A除了要接收他女朋友们的消息之外,作为一个暖男。小A还会时不时自己主动嘘寒问暖一下。发消息给他女朋友们提醒她们多喝水。当然他女朋友们自己并不知道有其他人,只知道自己收到了消息。有的女朋友比如小C就回复很快,那小A就会和小C保持一个很频繁的来回通信。

有的女朋友比如小D是个程序媛,有时候需要开各种会。小A发给消息很长时间都不回,那他还是老是开着窗口不断看着小D有没有回消息就太浪费了。这时候小A会采用熔断措施,如果超过10分钟不回,他会先关掉窗口。隔半小时再打开看看。

小A还有个女朋友小E,她电脑中了病毒,给小A发的都是各种垃圾广告。小A判断这些信息没有必要看了,也做了熔断,关闭了窗口。但他还是会隔半小时再打开看看小E的电脑病毒问题有没有解决,是不是可以正常发消息了。

举个生活中的超时重试例子:

小A终于觉得自己女朋友实在是太多了,所以他决定和其中一个小F分手。但是小A想好的分手的台词发消息过去,小F都不回。小A都不知道到底分手是成功还是不成功。小A就很小心的每10分钟给小E发个消息问她是否同意这个决定。这就是超时重试。

熔断的原理

熔断本质上是做快速失败,防止级联故障引起雪崩。它的主要采用的手段是基于断路器的设计模式。

断路器有基本模式和扩展模式。

基本模式中,断路器由两个状态和一个动作组成:断路器打开状态、断路器关闭状态和跳闸动作。在断路器关闭状态下,请求过来每次都要先经由跳闸动作,由跳闸动作判断是否需要将状态改成断路器打开状态。断路器打开状态下,请求直接执行快速失败的动作,不会向后请求。

扩展模式中,断路器增加了一个半开状态,它允许有限数量的请求通过,如果执行成功,恢复到关闭状态;如果失败,则恢复到开放,然后重启定时器,一段时间后再用半开状态来尝试。扩展模式可以半自动化的进行熔断恢复,避免了基本模式中,一旦断路器被打开完全依赖人工判断是否恢复的弊端。

扩展模式对应的状态机流转如下:

熔断的实现

基础实现

在Java中业界用的比较多的是hystrix或者resilience4j实现熔断。原理差不多。实际上resilience4j是受到hystrix启发而形成的。

下面以hystrix为例进行讲解。这是一个根据token认证的服务,正常情况下调用authenticate方法会调用下游来判断token是否有效。但是如果下游一旦出现问题了,因为这是一个内网服务,平时很少有非法流量进来,并且内网服务安全性可控。这个认证的统计学意义更大一些。所以在下游出现故障时可以快速降级直接返回认证成功。避免对整个服务造成影响。

从上面代码可知,使用方面关注点在于@HystrixCommand这个注解。基本用法就是上面的代码示例。

下面简单讲解一下源码。重点是HystrixCircuitBreakerImpl这个实现类

public static class HystrixCircuitBreakerImpl implements HystrixCircuitBreaker {
private final HystrixCommandProperties properties;
private final HystrixCommandMetrics metrics;
private AtomicBoolean circuitOpen = new AtomicBoolean(false);
private AtomicLong circuitOpenedOrLastTestedTime = new AtomicLong();

protected HystrixCircuitBreakerImpl(HystrixCommandKey key, HystrixCommandGroupKey commandGroup, HystrixCommandProperties properties, HystrixCommandMetrics metrics) {
this.properties = properties;
this.metrics = metrics;
}

public void markSuccess() {
if(this.circuitOpen.get() && this.circuitOpen.compareAndSet(true, false)) {
this.metrics.resetStream();
}

}

public boolean allowRequest() {
if(((Boolean)this.properties.circuitBreakerForceOpen().get()).booleanValue()) {
return false;
} else if(((Boolean)this.properties.circuitBreakerForceClosed().get()).booleanValue()) {
this.isOpen();
return true;
} else {
return !this.isOpen() || this.allowSingleTest();
}
}

public boolean allowSingleTest() {
long timeCircuitOpenedOrWasLastTested = this.circuitOpenedOrLastTestedTime.get();
return this.circuitOpen.get() && System.currentTimeMillis() > timeCircuitOpenedOrWasLastTested + (long)((Integer)this.properties.circuitBreakerSleepWindowInMilliseconds().get()).intValue() && this.circuitOpenedOrLastTestedTime.compareAndSet(timeCircuitOpenedOrWasLastTested, System.currentTimeMillis());
}

public boolean isOpen() {
if(this.circuitOpen.get()) {
return true;
} else {
HealthCounts health = this.metrics.getHealthCounts();
if(health.getTotalRequests() < (long)((Integer)this.properties.circuitBreakerRequestVolumeThreshold().get()).intValue()) {
return false;
} else if(health.getErrorPercentage() < ((Integer)this.properties.circuitBreakerErrorThresholdPercentage().get()).intValue()) {
return false;
} else if(this.circuitOpen.compareAndSet(false, true)) {
this.circuitOpenedOrLastTestedTime.set(System.currentTimeMillis());
return true;
} else {
return true;
}
}
}

}

1>这里面成员变量中HystrixCommandProperties是用来处理@HystrixCommand这个注解的commandProperties的。

和断路器相关的主要是下面这些:

1>如果circuitBreakerForceOpen = true会将断路器强制打开circuitBreakerForceClose = true会将断路器强制关闭。这个一般配合配置中心来用,起到人为干预的作用。

2>HystrixCommandMetrics是计数器,用这里面存的健康请求数量来判断是否需要熔断。

3>circuitOpen即为断路器状态,有打开和关闭两种。

4>markSuccess() 方法是在调用attemptExecution正常逻辑成功时,调用 #markSuccess() 方法,关闭断路器。这时候HystrixCommandMetrics也会重置。

5>allowRequest()是用来判断是否允许指令执行。

6>allowSingleTest()是用来判断是否打开熔断到一定时间了,到了的话就测试一下是否已经恢复了。

7>isOpen()是判断断路器是否打开。

高阶实现

如果项目中全套使用了spring cloud,可以使用feign。但是如果只是想使用熔断,还是建议直接使用hystrix。个人实验如果想直接使用spring cloud feign做熔断,坑比较多。

    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多