服务治理三板斧之熔断
什么是断路器
在软件工程领域,许多术语实际上是对现实世界的一种模拟。比如,设计模式的许多概念源自建筑工程学,限流则借鉴了现实生活中的交通管制。而本文要介绍的熔断机制同样是基于现实世界的模拟,它的概念来自于电子工程中的断路器(Circuit Breaker)。通过将这些现实场景的概念应用于软件开发中,我们能够更好地理解和解决各种问题。
下图为一款直流塑壳断路器,它主要应用于光伏发电、充电桩、发电厂、电力电源、轨道交通、通讯等直 流环境,用来分配电能,保护线路和电源设备,使免受过载、短路等故障的危害。
在互联网系统中,断路器模式的作用类似电器发生短路后经常故障保护。当某个服务单元发生故障之后,通过断路器的故障监控(类似熔断保险丝),向调用方返回一个错误响应,而不是长时间的等待。这样就不会使得线程因调用故障服务被长时间占用不释放,避免了故障在分布式系统中的蔓延。
常用场景
熔断机制在以下场景中可以得到有效的应用:
- 服务调用故障:当一个服务在调用其他服务时出现故障或延迟,熔断机制可以快速停止对该服务的请求,避免等待超时和资源浪费,保护系统的稳定性。
- 服务降级:当系统资源不足或出现异常情况时,可以通过熔断将请求转发到备用的降级逻辑或默认响应,确保系统的基本功能可用,减轻系统压力。
- 限流保护:当系统面临突发的高并发请求时,熔断机制可以限制请求的流量,防止系统超负荷运行。通过设置适当的阈值,熔断可以在系统达到承载极限之前停止接收新的请求,保护系统的稳定性和可用性。
- 故障恢复:当一个服务或资源出现故障后,熔断机制可以快速拒绝对该服务的请求,并在一段时间后尝试重新请求。这样可以避免连续不断地发送请求,等待故障服务恢复正常,从而减少对故障服务的压力,提高系统的恢复能力。
- 避免雪崩效应:在面对大规模故障或不可用的情况下,熔断机制可以限制对故障服务的请求,防止雪崩效应的发生。通过熔断,系统可以快速识别并隔离故障,保护系统其他部分免受故障的影响,提高整个系统的稳定性。
总之,熔断机制适用于任何可能导致服务不可用或故障的场景,通过快速失败和控制请求流量,能够保护系统免受故障的影响,并提供更好的用户体验。它是构建健壮和可靠的分布式系统的重要组成部分。
断路器模式
我们先来看看电子电路中断路器背后的基本思想非常简单。将受保护的对象调用包装在断路器对象中,断路器负责监视故障。一旦故障达到某个阀值,断路器就会跳闸,所有对断路器进一步的调用都会返回错误,而根本不会调用到受保护的对象。如下为基本的断路器(Curcuit Breaker)结构:
可见断路器有两个基本状态(CLOSED、OPEN )和一个基本trip动作:
CLOSED
:client向supplier发起的服务请求, 直接无阻碍通过断路器, supplier的返回值接直接由断路器交回给client.trip
:在close状态下,如果supplier持续超时报错, 达到规定的阀值后,断路器就发生trip, 之后断路器状态就会从close进入open.OPEN
:client向supplier发起的服务请求后,断路器不会将请求转到supplier, 而是直接返回client, client和supplier之间的通路是断的
以上作为现实中建筑物中的断路器的一个简单模拟,当断路器被开启后,通常需要外部干预来恢复(重置)它。对于软件工程中的断路器,我们可以让断路器定期探测supplier的服务是否恢复, 比如可以通过在适当的间隔时间后再次尝试调用受保护的对象来实现自重置,并在成功时重置断路器。
所以软件中的断路器一般通过 3 个有限状态机来实现,CLOSED、OPEN、HALF_OPEN。此外,还有 2 个特殊的状态机,DISABLED 和 FORCED_OPEN。状态的存储更新必须是线程安全的,即只有一个线程能够在某个时间点更新状态。
- 关闭 —> 打开:当故障率等于或大于可配置的阈值时,CircuitBreaker 的状态将从“关闭”更改为“打开”。
- 打开 —> 半开:当 CircuitBreaker 打开时,它会拒绝带有 外部 的调用。经过一段等待时间后,CircuitBreaker 状态从 OPEN 变为 HALF_OPEN,并允许一定数量(初始化时提前配置)的服务调用来检测supplier是否仍然不可用或再次变为可用。如果故障率或慢呼叫率等于或大于配置的阈值,则状态会变回 OPEN。
- 半开 —> 关闭:如果故障率和慢呼叫率低于阈值,则状态将变回“已关闭”。
- DISABLED:始终允许调用。
- FORCED_OPEN:始终拒绝调用。
常见熔断算法
基于断路器的设计模式(Circuit Breaker),存在两种实现方法。它可以根据实际需求进行配置和调整,包括失败阈值、时间窗口大小、重试策略等。
基于计数的断路器(Count-based Circuit Breaker)
基于计数的断路器根据一定的计数规则来判断是否需要打开断路器。它会统计一段时间内请求的成功率或失败率,当失败率超过预设的阈值时,断路器将打开,停止转发请求并快速失败。这种断路器适合于对请求进行频率或成功率限制的场景。它可以防止系统在请求过多失败时继续发送请求,从而减轻对故障服务的压力,保护系统的稳定性。
public class CountBasedCircuitBreaker {
private final int failureThreshold;
private final int recoveryThreshold;
private final AtomicInteger failureCount;
public CountBasedCircuitBreaker(int failureThreshold, int recoveryThreshold) {
this.failureThreshold = failureThreshold;
this.recoveryThreshold = recoveryThreshold;
this.failureCount = new AtomicInteger(0);
}
public boolean isAllowed() {
int currentFailureCount = failureCount.get();
if (currentFailureCount >= failureThreshold) {
// 达到故障阈值,断开断路器
return false;
} else {
// 未达到故障阈值,允许通过
return true;
}
}
public void recordFailure() {
failureCount.incrementAndGet();
}
public void recordSuccess() {
int currentFailureCount = failureCount.get();
if (currentFailureCount > 0 && currentFailureCount <= recoveryThreshold) {
// 在恢复阈值内,减少故障计数
failureCount.decrementAndGet();
}
}
public static void main(String[] args) {
CountBasedCircuitBreaker circuitBreaker = new CountBasedCircuitBreaker(5, 3);
// 模拟调用失败
for (int i = 0; i < 7; i++) {
if (circuitBreaker.isAllowed()) {
System.out.println("调用服务...");
circuitBreaker.recordFailure();
} else {
System.out.println("断路器打开,拒绝调用服务。");
}
}
// 模拟调用成功
circuitBreaker.recordSuccess();
circuitBreaker.recordSuccess();
circuitBreaker.recordSuccess();
// 继续尝试调用
if (circuitBreaker.isAllowed()) {
System.out.println("调用服务...");
circuitBreaker.recordFailure();
} else {
System.out.println("断路器打开,拒绝调用服务。");
}
}
}
基于时间的断路器(Time-based Circuit Breaker)
基于时间的断路器根据一定的时间窗口来判断是否需要打开断路器。它会在一段时间内统计请求的成功与失败情况,如果失败的请求超过预设的阈值,断路器将打开。不同于计数断路器,基于时间的断路器会在一段时间后自动重置,允许一部分请求继续尝试,以检测服务是否恢复正常。这种断路器适用于对故障服务的快速检测和自动恢复的场景,可以防止系统长时间无法访问故障服务而导致整个系统无法正常运行。
public class TimeBasedCircuitBreaker {
// 故障阈值时间窗口
private final Duration failureThreshold;
// 恢复超时时间窗口
private final Duration recoveryTimeout;
// 最后一次故障时间
private Instant lastFailureTime;
public TimeBasedCircuitBreaker(Duration failureThreshold, Duration recoveryTimeout) {
this.failureThreshold = failureThreshold;
this.recoveryTimeout = recoveryTimeout;
this.lastFailureTime = Instant.MIN;
}
public boolean isAllowed() {
Instant now = Instant.now();
if (lastFailureTime.plus(failureThreshold).isBefore(now)) {
// 最后一次故障时间加上故障阈值早于当前时间,即超过故障阈值时间窗口,断路器关闭,允许通过
return true;
} else {
// 否则断路器开启,不允许通过
return false;
}
}
public void recordFailure() {
lastFailureTime = Instant.now();
}
public void recordSuccess() {
Instant now = Instant.now();
if (lastFailureTime.plus(recoveryTimeout).isBefore(now)) {
// 超过恢复超时时间窗口,重置故障时间
lastFailureTime = Instant.MIN;
}
}
public static void main(String[] args) {
TimeBasedCircuitBreaker circuitBreaker = new TimeBasedCircuitBreaker(Duration.ofSeconds(10), Duration.ofMinutes(1));
// 模拟调用失败
for (int i = 0; i < 3; i++) {
if (circuitBreaker.isAllowed()) {
System.out.println("调用服务...");
circuitBreaker.recordFailure();
} else {
System.out.println("断路器打开,拒绝调用服务。");
}
}
// 暂停一段时间
try {
Thread.sleep(15000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 继续尝试调用
if (circuitBreaker.isAllowed()) {
System.out.println("调用服务...");
circuitBreaker.recordFailure();
} else {
System.out.println("断路器打开,拒绝调用服务。");
}
// 模拟调用成功
circuitBreaker.recordSuccess();
// 继续尝试调用
if (circuitBreaker.isAllowed()) {
System.out.println("调用服务...");
circuitBreaker.recordFailure();
} else {
System.out.println("断路器打开,拒绝调用服务。");
}
}
}
小结
通过这篇文章我们了解了如何断路器的模式和一些熔断算法。那么,如果让我们自己来设计一个服务的熔断,需要考虑以下几个关键方面:
- 故障阈值:确定触发熔断的故障阈值。这是指在一定时间窗口内出现的连续故障次数或错误率超过的阈值。根据应用的需求和性能特征,选择适当的故障阈值,以平衡服务的可用性和稳定性。
- 熔断状态:定义熔断状态,并确定在熔断状态下对服务的请求如何处理。熔断状态表示服务不可用或出现故障,可以是完全拒绝请求、返回预设的错误响应或返回缓存数据等。需要根据具体业务场景和需求来确定熔断状态下的行为。
- 熔断恢复:设定熔断恢复的策略和逻辑。一旦服务进入熔断状态,需要定义熔断恢复的条件和机制。例如,设置一个恢复时间窗口,在窗口内观察服务的健康状况,如果达到一定条件(如错误率降低、请求成功率达到阈值等),则允许服务逐渐恢复。
- 服务降级:在熔断状态下,可以采用服务降级的策略。服务降级是指在服务不可用或故障时,通过提供部分功能或返回预设的响应,保证系统的可用性。通过定义降级策略,可以避免服务完全不可用,提高系统的容错性。
- 监控和报警:建立监控和报警机制,及时感知服务的健康状态和熔断情况。通过收集关键指标(如请求成功率、错误率、响应时间等),实时监控服务的运行状况。当达到预设的阈值或触发熔断条件时,及时发送报警通知,便于运维人员进行相应的处理和调整。
- 逐步恢复:在熔断状态解除后,需要逐步恢复服务的正常流量。可以采用渐进式的策略,逐渐增加服务的负载,同时监控服务的稳定性,确保系统能够承受恢复后的高负载。
综上所述,设计服务熔断时需要考虑故障阈值、熔断状态、熔断恢复、服务降级、监控和报警以及逐步恢复等关键设计要点,以确保系统在面临故障或异常情况时能够保持可用性和稳定性。