招行二面:为什么有了服务降级,还需要服务熔断?

在分布式系统中,我们经常听到服务熔断这个词,那么,什么是服务熔断?为什么需要服务熔断?如何实现服务熔断?这篇文章,我们来聊一聊。

一、什么是服务熔断?

简单来说,服务熔断(Circuit Breaker)是一种用于提高分布式系统健壮性的设计模式。它的灵感来源于电路中的熔断器,当电路中出现问题时,熔断器会自动断开,防止故障扩大,保护整个系统。应用在微服务架构中,服务熔断机制可以在某个服务出现故障或响应缓慢时,快速失败或采取备用方案,从而避免级联失败,提升系统的整体稳定性。

二、原理分析

接下来,我们讲解服务熔断的原理,整体总结成下面五个步骤。

1. 正常状态

在正常情况下,服务之间的调用是通畅的,熔断器处于关闭状态。所有请求都会正常发送到目标服务,没有任何干预。

2. 监控与检测

熔断器会监控目标服务的调用情况,包括请求成功率、失败率、响应时间等。当某个阈值被超过(比如连续失败次数超过预设值),熔断器会认为目标服务可能出现了问题。

3. 打开熔断

一旦检测到目标服务可能故障,熔断器会打开(Open),此时所有对该服务的请求都会被立即失败,不再发送实际请求。这就像是电路中的熔断器断开一样,防止故障蔓延。

4. 半开启状态

过一段时间后,熔断器会进入半开启状态(Half-Open),允许少量请求尝试调用目标服务。如果这些请求成功,熔断器会重新关闭,恢复正常状态;如果失败,熔断器继续保持打开状态。

5. 备用机制

当熔断器打开时,可以采取备用方案,比如返回默认值、跳过某些操作,甚至切换到其他服务实例,以保证系统的部分功能仍然可用。

通过这样的机制,服务熔断能够有效地防止单个服务故障导致的系统级别的连锁反应。

三、示例演示

为了更好地理解服务熔断,接下来,我们将使用 Resilience4j 这个轻量级的容错库来实现服务熔断机制。Resilience4j是一个专为 Java 8及以上版本设计的库,具有易用性和高性能的特点。

1. 环境准备

首先,确保你的项目中已经引入了Resilience4j的依赖。以Maven项目为例,添加以下依赖到pom.xml中:

复制
<dependencies> <!-- Resilience4j核心依赖 --> <dependency> <groupId>io.github.resilience4j</groupId> <artifactId>resilience4j-all</artifactId> <version>2.0.2</version> </dependency> <!-- 其他依赖项 --> </dependencies>1.2.3.4.5.6.7.8.9.
2. 编写服务熔断代码

下面是一个简单的示例,展示如何使用Resilience4j实现服务熔断。当目标服务响应慢或失败时,熔断器会起作用,快速返回备用结果。

复制
import io.github.resilience4j.circuitbreaker.*; import io.github.resilience4j.decorators.Decorators; import java.time.Duration; import java.util.concurrent.*; import java.util.function.Supplier; publicclass CircuitBreakerDemo { public static void main(String[] args) { // 创建CircuitBreaker配置 CircuitBreakerConfig config = CircuitBreakerConfig.custom() .failureRateThreshold(50) // 失败率阈值 .waitDurationInOpenState(Duration.ofSeconds(5)) // 打开状态持续时间 .slidingWindowSize(4) // 滑动窗口大小 .build(); // 创建CircuitBreaker实例 CircuitBreaker circuitBreaker = CircuitBreaker.of("myCircuitBreaker", config); // 模拟目标服务调用 Supplier<String> decoratedSupplier = Decorators.ofSupplier(() -> callExternalService()) .withCircuitBreaker(circuitBreaker) .withFallback(Collections.singletonList(CircuitBreaker.class), throwable -> "默认响应") .decorate(); // 模拟多次调用 for (int i = 0; i < 10; i++) { try { String response = decoratedSupplier.get(); System.out.println("响应: " + response); } catch (Exception e) { System.out.println("调用失败: " + e.getMessage()); } // 等待1秒 try { Thread.sleep(1000); } catch (InterruptedException e) { Thread.currentThread().interrupt(); break; } } } // 模拟外部服务调用,随机失败或延时 private static String callExternalService() { double random = Math.random(); if (random < 0.5) { // 模拟失败 thrownew RuntimeException("服务调用失败"); } else { // 模拟延时 try { Thread.sleep(2000); // 2秒延时 } catch (InterruptedException e) { Thread.currentThread().interrupt(); } return"成功响应"; } } }1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.22.23.24.25.26.27.28.29.30.31.32.33.34.35.36.37.38.39.40.41.42.43.44.45.46.47.48.49.50.51.52.53.54.55.56.57.58.59.60.61.62.63.
3. 代码解析配置CircuitBreaker:我们创建了一个自定义的熔断器配置,设置了失败率阈值为50%,滑动窗口大小为4次调用,打开状态持续5秒。装饰目标服务调用:使用Decorators将目标服务调用装饰为一个有熔断器保护的供应者(Supplier)。同时,我们设置了一个备用响应,当熔断器打开或目标服务调用失败时,返回“默认响应”。模拟调用:在for循环中,我们模拟了多次服务调用。目标服务callExternalService随机成功或失败,并可能产生延时。通过这种方式,我们可以观察熔断器是如何根据调用结果自动切换状态的。

运行这段代码,当失败率超过 50%时,熔断器会打开,后续的请求会立即返回“默认响应”。经过 5秒后,熔断器会进入半开启状态,尝试恢复调用。如果目标服务恢复正常,熔断器会重新关闭,系统恢复正常运行。

四、问题解答

回到文章的标题:为什么有了服务降级还需要服务熔断?

这里我们总结了四个核心理由:

避免资源浪费:当一个服务出现故障时,如果没有熔断机制,系统可能会持续不断地尝试调用这个失败的服务,导致请求积压和资源耗尽。服务熔断通过快速失败,避免了不必要的调用,节省了宝贵的系统资源。防止级联故障:在微服务架构中,服务之间通常相互依赖。如果一个服务出现问题,持续的失败调用可能会影响到依赖它的其他服务,导致级联故障。服务熔断器可以在问题初期及时切断受影响的服务调用,防止故障扩散到整个系统。加速系统恢复:通过熔断机制,系统能够更快地检测到服务的故障状态,并在熔断器打开后,等待一段时间再尝试恢复调用。这有助于目标服务有足够的时间进行自我修复,从而加速整个系统的恢复过程。提供更好的用户体验: 服务降级虽然能够保证核心功能的可用性,但在高负载或持续失败的情况下,用户可能会频繁遇到降级后的功能或默认响应,影响使用体验。服务熔断器通过控制调用频率和恢复策略,能够在保证必要降级的同时,减少对用户的负面影响。

THE END