服务熔断-熔断器设计
文章目录
- 服务为什么需要熔断
- 熔断器设计思想
- 熔断器代码实现
服务为什么需要熔断
对于服务端采用的保护机制为服务限流。 对于服务调用端是否存在保护机制?
假如要发布一个服务 B,而服务 B 又依赖服务 C,当一个服务 A 来调用服务 B 时,服务 B 的业务逻辑调用服务 C,而这时服务 C 响应超时了,由于服务 B 依赖服务 C,C 超时直接导致 B 的业务逻辑一直等待,而这个时候服务 A 在频繁地调用服务 B,服务 B 就可能会因为堆积大量的请求而导致服务宕机。
由此可见,服务 B 调用服务 C,服务 C 执行业务逻辑出现异常时,会影响到服务 B,甚至可能会引起服务 B 宕机。这还只是 A->B->C 的情况,试想一下 A->B->C->D->……呢?在整个调用链中,只要中间有一个服务出现问题,都可能会引起上游的所有服务出现一系列的问题,甚至会引起整个调用链的服务都宕机,这是非常恐怖的。
所以说,在一个服务作为调用端调用另外一个服务时,为了防止被调用的服务出现问题而影响到作为调用端的这个服务,这个服务也需要进行自我保护。而最有效的自我保护方式就是熔断。
熔断器设计思想
熔断状态实现包括三个:闭合、断开、半开,分别对应于正常、故障、检测故障是否已被修复的场景。
- 闭合:正常情况,后台会对调用失败次数进行积累,到达一定阈值或比例时则自动启动熔断机制。
- 断开:一旦对服务的调用失败次数达到一定阈值时,熔断器就会打开,这时候对服务的调用将直接返回一个预定的错误,而不执行真正的网络调用。同时,熔断器需要设置一个固定的时间间隔,当处理请求达到这个时间间隔时会进入半熔断状态。
- 半开:在半开状态下,熔断器会对通过它的部分请求进行处理,如果对这些请求的成功处理数量达到一定比例则认为服务已恢复正常,就会关闭熔断器,反之就会打开熔断器。
熔断设计的一般思路是,在请求失败 N 次后在 X 时间内不再请求,进行熔断;然后再在 X 时间后恢复 M% 的请求,如果 M% 的请求都成功则恢复正常,关闭熔断,否则再熔断 Y 时间,依此循环。在熔断的设计中,根据 Netflix 的开源组件 hystrix 的设计,我们可以仿照以下二个模块:熔断请求判断算法、熔断恢复机制:
熔断请求判断机制算法:根据事先设置的在固定时间内失败的比例来计算。
熔断恢复:对于被熔断的请求,每隔 X 时间允许部分请求通过,若请求都成功则恢复正常。
熔断器代码实现
package main.version4.v1.client.circuitBreaker;
import java.util.concurrent.atomic.AtomicInteger;
/**
* 管理熔断器的状态,并根据请求的成功和失败的情况进行状态转移
*/
public class CircuitBreaker {
// 当前状态
private CircuitBreakerStatus status = CircuitBreakerStatus.CLOSED;
private AtomicInteger failureCount = new AtomicInteger(0);
private AtomicInteger successCount = new AtomicInteger(0);
private AtomicInteger requestCount = new AtomicInteger(0);
// 失败次数阈值
private final int failureThreshold;
// 半开启-》关闭状态的成功次数的比例
private final double halfOpenSuccessRate;
// 恢复时间
private final long retryTimePeriod;
// 上一次失败时间
private long lastFailureTime = 0;
public CircuitBreaker(int failureThreshold, double halfOpenSuccessRate, long retryTimePeriod){
this.failureThreshold = failureThreshold;
this.halfOpenSuccessRate = halfOpenSuccessRate;
this.retryTimePeriod = retryTimePeriod;
}
// 查看当前熔断器是否允许请求通过
public boolean allowRequest(){
long currentTime = System.currentTimeMillis();
switch (status){
case OPEN:
// 当前处于打开状态,超过一定时间,进入半开状态
if(currentTime - lastFailureTime > retryTimePeriod){
// 细化锁的粒度,将锁从allowRequest判断中移除
synchronized (this){
if(status == CircuitBreakerStatus.OPEN){
status = CircuitBreakerStatus.HALF_OPEN;
resetCounts();
return true;
}
}
}
return false;
case HALF_OPEN:
// 在半开状态下,熔断器允许请求通过,并根据请求的成功率决定是否恢复到闭合状态或重新进入打开状态。
requestCount.incrementAndGet();
return true;
case CLOSED:
default:
return true;
}
}
// 记录成功
public synchronized void recordSuccess(){
if(status == CircuitBreakerStatus.HALF_OPEN) {
// 半开状态下,统计成功的请求数量,当成功请求数量达到阈值后,关闭熔断器
successCount.incrementAndGet();
if (successCount.get() >= halfOpenSuccessRate * requestCount.get()) {
status = CircuitBreakerStatus.CLOSED;
resetCounts();
}
}else if(status == CircuitBreakerStatus.CLOSED){
// 当熔断器关闭时,不清空数据
}else{
resetCounts();
}
}
// 记录失败
public synchronized void recordFailure(){
failureCount.incrementAndGet();
lastFailureTime = System.currentTimeMillis();
if(status == CircuitBreakerStatus.HALF_OPEN){
// 在半开状态下,请求再次失败,此时再次打开熔断器
status = CircuitBreakerStatus.OPEN;
lastFailureTime = System.currentTimeMillis();
}else if(failureCount.get() > failureThreshold){
// 失败次数达到阈值,熔断器进入打开状态,拒绝请求
status = CircuitBreakerStatus.OPEN;
}
}
// 重置次数
public void resetCounts(){
failureCount.set(0);
successCount.set(0);
requestCount.set(0);
}
// 获取状态
public CircuitBreakerStatus getStatus(){
return status;
}
}
enum CircuitBreakerStatus{
// 关闭、开启、半开启
CLOSED, OPEN, HALF_OPEN
}