Resilience4j 入门教程 - 基础篇之一

大纲

前言

技术资源

版本说明

组件版本说明
Spring Boot3.2.0
Spring Cloud2023.0.0
Consul1.15.4 作为注册中心

Hystrix 停更

Netflix Hystrix 是一个用于处理分布式系统的延迟和容错的开源库。在分布式系统里,许多依赖不可避免的会调用失败,比如处理超时、出现异常等,Hystrix 能够保证在一个依赖出现问题的情况下,不会导致整个系统崩溃,避免级联故障,以提高分布式系统的可用性。

由于 Netflix Hystrix 早已停更,Spring Cloud 官方建议使用 Resilience4j 来替代。

分布式系统面临的问题

服务雪崩问题

在复杂分布式体系结构中,应用程序有数十个依赖关系,每个依赖关系在某些时候将不可避免地调用失败。

上图中的请求需要调用 A、P、H、I 四个服务,如果一切顺利则没有问题,关键是如果 I 服务处理超时会出现什么情况呢?

服务雪崩

多个微服务之间互相调用的时候,假设微服务 A 调用了微服务 B,而微服务 B 又调用了微服务 C,这就是所谓的 "扇出"。如果扇出的链路上某个微服务的调用响应时间过长或者不可用,对微服务 A 的调用就会占用越来越多的系统资源,进而引起系统崩溃,这就是所谓的 "服务雪崩" 效应。

对于高流量的应用来说,单一的后端依赖可能会导致所有服务器上的所有资源都在几秒钟内饱和(资源耗尽)。比调用失败更糟糕的是,这些应用程序还可能导致服务之间的延迟增加,备份队列、线程和其他系统资源紧张,最终导致整个系统发生更多的级联故障。这些都表示需要对服务的故障和延迟进行隔离和管理,以便单个依赖关系的失败,不能影响整个应用程序或系统。简而言之,当发现一个模块下的某个实例失败后,这时候这个模块依然还会接收流量,然后这个有问题的模块还被其他的模块调用了,这样就会发生级联故障,或者叫 “服务雪崩”。

服务雪崩解决

  • 服务熔断

    • 类似保险丝,保险丝闭合状态(CLOSE)代表可以正常使用,当达到最大服务访问后,直接拒绝访问跳闸限电(OPEN),此时调用方会接受服务降级的处理并返回友好的兜底提示。
    • 这就是类似家用的保险丝,从闭合(CLOSE)供电状态 → 跳闸(OPEN)断开状态。
  • 服务降级

    • 服务器繁忙,请稍后再试。
    • 不让客户端长时间等待,立刻返回一个友好的提示给客户端(FallBack 处理)。
  • 服务限流

    • 秒杀等高并发操作,严禁一窝蜂的涌过来,要求排好队,一秒钟 N 个,有序进行处理。
  • 其他方案

    • 服务限时
    • 服务预热
    • 接近实时的监控
    • 兜底的处理动作

提示

"断路器" 本身是一种开关装置,当某个服务发生故障之后,通过断路器的故障监控(类似熔断保险丝),向服务调用方返回一个符合预期的、可处理的备选响应结果 (FallBack),而不是长时间地让服务调用方等待,或者抛出服务调用方无法处理的异常。这样就可以保证服务调用方的线程不会被长时间、不必要地占用,从而避免了故障在分布式系统中的蔓延,乃至出现服务雪崩。

Circuit Breaker 介绍

Circuit Breaker 的简介

Spring Cloud Circuit Breaker 提供了一个跨不同断路器实现的抽象。它提供了一个一致的 API 在应用程序中使用,使开发人员能够选择最适合应用程序需求的断路器实现。目前 Spring Cloud Circuit Breaker 有两种实现:

Circuit Breaker 的原理

Circuit Breaker 的目的是保护分布式系统免受故障和异常,提高系统的可用性和健壮性。当一个服务出现故障时,Circuit Breaker 会迅速切换到开放 OPEN 状态 (类似保险丝跳闸断电),阻止请求发送到该服务,从而避免更多的请求发送到该服务。这可以减少对该服务的负载,防止该服务进一步崩溃,并使整个系统能够继续正常运行。同时,Circuit Breaker 还可以提高系统的可用性和健壮性,因为它可以在分布式系统的各个服务之间自动切换,从而避免单点故障的问题。

总结

Circuit Breaker 只是一套规范和接口,真正的落地实现者是 Resilience4j 和 Spring Cloud Retry。

Resilience4j 介绍

Resilience4j 的简介

Resilience4j 是一个轻量级容错框架,设计灵感来源于 Netflix 的 Hystrix 框架,专为函数式编程所设计。Resilience4j 提供了一组高阶函数(装饰器),包括断路器、限流器、重试、隔离等。Resilience4j 可以对任何的函数式接口、Lambda 表达式或者方法引用进行增强,并且这些装饰器可以进行叠加使用。这样做的好处是,开发者可以根据需要选择特定的装饰器进行组合使用。Resilience4j 整个框架只是使用了 Varr 的库,不需要引入其他的外部依赖。与此相比,Netflix Hystrix 对 Archaius 具有编译依赖,而 Archaius 需要更多的外部依赖,例如 Guava 和 Apache Commons Configuration。值得一提的是,Resilience4j 2 需要依赖 JDK 17。更多介绍请阅读 Resilience4j 项目Resilience4j 官方英文文档Resilience4j 非官方中文文档

Resilience4j 的核心功能

Resilience4j 提供了以下几个核心模块(默认都带降级功能):

  • resilience4j-circuitbreaker:断路器(熔断)
  • resilience4j-ratelimiter:速率限制(限流)
  • resilience4j-bulkhead:舱壁(隔离)
  • resilience4j-timelimiter:超时处理(限时处理)
  • resilience4j-retry:自动重试(失败重试)
  • resilience4j-cache:结果缓存

Resilience4j 还提供了用于 Metrics、Feign、Kotlin、Spring、Ratpack、Vertx、RxJava2 等的附加模块。

Resilience4j 与 Hystrix 的区别

  • Hystrix 使用 HystrixCommand 来调用外部的系统,而 Resilience4j 提供了一些高阶函数,例如断路器、限流器、隔离机制等,这些函数作为装饰器对函数式接口、Lambda 表达式、函数引用进行装饰。此外,Resilience4j 还提供了失败重试和缓存调用结果的装饰器。开发者可以在函数式接口、Lambda 表达式、函数引用上叠加地使用一个或多个装饰器,这意味着隔离机制、限流器、重试机制等能够进行组合使用。这么做的优点在于,开发者可以根据需要选择特定的装饰器。任何被装饰的方法都可以同步或异步执行,异步执行可以采用 CompletableFuture 或 RxJava。
  • 当有很多超过规定响应时间的请求时,在远程系统没有响应和引发异常之前,断路器将会开启。
  • 当 Hystrix 处于半开状态时,Hystrix 根据只执行一次请求的结果来决定是否关闭断路器。而 Resilience4j 允许执行可配置次数的请求,将请求的结果和配置的阈值进行比较来决定是否关闭断路器。
  • Resilience4j 提供了自定义的 Reactor 和 RxJava 操作符对断路器、隔离机制、限流器中任何的反应式类型进行装饰。
  • Hystrix 和 Resilience4j 都会发出一个事件流,系统可以对发出的事件进行监听,得到相关的执行结果和延迟的时间统计数据都是十分有用的。

Resilience4j 使用

断路器的使用

断路器的介绍

Resilience4j CircuitBreaker(断路器)的底层是通过有限状态机实现的,一共有三个普通状态:关闭(CLOSED)、开启(OPEN)、半开(HALF_OPEN),还有两个特殊状态:禁用(DISABLED)、强制开启(FORCED_OPEN)。

  • 当断路器关闭时,所有的请求都会通过断路器。

    • 如果失败率超过设定的值,断路器就会从关闭状态转换到打开状态,这时所有的请求都会被拒绝。
    • 当经过一段时间后,断路器会从打开状态转换到半开状态,这时仅有一定数量的请求会被放行,并重新计算失败率。
    • 如果失败率超过阀值,则断路器切换为打开状态。如果失败率低于阀值,则断路器切换为关闭状态。
  • 断路器使用滑动窗口来存储和统计调用的结果。开发者可以选择基于调用数量的滑动窗口或者基于时间的滑动窗口。

    • 基于调用数量的滑动窗口统计了最近 N 次调用的返回结果。
    • 基于时间的滑动窗口统计了最近 N 秒的调用返回结果。
  • 除此以外,断路器还有两种特殊状态:DISABLED(总是允许访问)和 FORCED_OPEN(总是拒绝访问)。

    • 这两种状态不会生成断路器事件(除了状态转换),并且不会记录任何指标,比如不会记录事件的成功或者失败。
    • 退出这两个状态的唯一方法是触发状态转换或者重置断路器。
  • 断路器是线程安全的。

    • 断路器的状态是原子引用类型。
    • 断路器使用原子操作以无副作用的功能来更新状态。
    • 从滑动窗口中记录调用结果和读取快照是同步的。

断路器的线程安全问题

  • 断路器是线程安全的,这使原子性得到了保证。在某一时刻,只允许一个线程对断路器的状态进行更改和对滑动窗口进行操作,但是断路器不会同步方法调用,这意味着方法调用不是核心的部分。否则,断路器器将会带来大量的性能损失和瓶颈,耗时的方法调用会对整体性能和吞吐量带来巨大的负面影响。
  • 如果有 20 个并发线程想要执行某个函数,并且断路器的状态为关闭,那么所有的线程都会被允许进行方法调用,即使假设滑动窗口的大小是 15,也不意味滑动窗口只允许 15 个调用并发地执行。如果想要限制并发线程的数量,则需要使用隔离机制,也就是将隔离机制和断路器组合在一起使用。

断路器的配置

断路器的详细配置参数可以从以下文档中查阅:

断路器的核心配置参数如下:

提示

值得一提的是,Resilience4j CircuitBreaker 的核心配置类是 CircuitBreakerConfig,所有的断路器默认配置参数都定义在该类里面。

断路器的使用案例一

本节将演示如何使用 OpenFeign + Resilience4j CircuitBreaker 实现熔断与降级,并且是基于访问数量的滑动窗口。案例需求如下:

  • 在最近 6 次访问中,当调用方法的失败率达到 50% 时,CircuitBreaker 将切换到开启 OPEN 状态(保险丝跳闸断电)来拒绝所有请求。
  • 等待 5 秒后,CircuitBreaker 将自动从 OPEN 状态切换到 HALF_OPEN 半开状态,允许一些请求通过以测试服务是否恢复正常。
  • 如果还是调用失败,CircuitBreaker 将重新切换到开启 OPEN 状态;如果调用正常,CircuitBreaker 将切换到 CLOSED 状态来恢复正常处理请求。
创建父级 Pom 工程

在父工程里面配置好工程需要的父级依赖,目的是为了更方便管理与简化配置,具体 Maven 配置如下:

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
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<hutool.version>5.8.22</hutool.version>
<lombok.version>1.18.26</lombok.version>
<spring.boot.version>3.2.0</spring.boot.version>
<spring.boot.test.version>3.1.5</spring.boot.test.version>
<spring.cloud.version>2023.0.0</spring.cloud.version>
</properties>

<dependencyManagement>
<dependencies>
<!--SpringBoot-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring.boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!--SpringCloud-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring.cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!--HuTool-->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>${hutool.version}</version>
</dependency>
<!--Lombok-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
<optional>true</optional>
</dependency>
<!--SpringBoot Test-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<version>${spring.boot.test.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
</dependencyManagement>
创建 Provider 工程

为了测试 Resilience4j CircuitBreaker(断路器)的使用效果,先创建一个服务提供者。这里创建 Provider 的 Maven 工程,由于需要将服务注册到 Consul,工程下的 pom.xml 文件需要引入 spring-cloud-starter-consul-discovery

1
2
3
4
5
6
7
8
9
10
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-consul-discovery</artifactId>
<exclusions>
<exclusion>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
</exclusion>
</exclusions>
</dependency>

创建 Provider 的启动主类,添加注解 @EnableDiscoveryClient,将服务注册到 Consul

1
2
3
4
5
6
7
8
9
@SpringBootApplication
@EnableDiscoveryClient
public class ProviderApplication {

public static void main(String[] args) {
SpringApplication.run(ProviderApplication.class, args);
}

}

application.yml 文件中指定服务名称(provider-service)、注册中心地址与端口号

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
server:
port: 8001

spring:
application:
name: provider-service
cloud:
# Consul
consul:
host: 127.0.0.1
port: 8500
# 注册中心
discovery:
service-name: ${spring.application.name}
heartbeat:
enabled: true

创建用于测试的 Controller 类

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
@RestController
public class ProviderController {

/**
* 该接口用于测试服务调用方(消费者)的断路器
*/
@GetMapping(value = "/provider/circuit/{id}")
public String circuit(@PathVariable("id") Integer id) {
// 模拟业务处理出错
if (id == -4) {
throw new RuntimeException("Circuit id 不能为负数");
}

// 模拟业务长时间处理
if (id == 9999) {
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
return "Hello, circuit! inputId : " + id + " \t " + UUID.fastUUID();
}

}
创建 Consumer 工程

若要使用 Resilience4j CircuitBreaker(断路器),则需要在 pom.xml 文件中引入依赖 spring-cloud-starter-circuitbreaker-resilience4j,由于基于注解的方式配置断路器是依赖 AOP 实现的,所以必须引入 spring-boot-starter-aop。另外,由于需要从 Consul 获取服务列表,即作为 Consul 的客户端,还需要引入 spring-cloud-starter-consul-discovery

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
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-consul-discovery</artifactId>
<exclusions>
<exclusion>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
</exclusion>
</exclusions>
</dependency>

<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-circuitbreaker-resilience4j</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>

创建服务接口类,通过 OpenFeign 调用 Provider 提供的服务

1
2
3
4
5
6
7
8
9
10
@FeignClient("provider-service")
public interface ProviderFeignApi {

/**
* 该接口用于测试服务调用方(消费者)的断路器
*/
@GetMapping(value = "/provider/circuit/{id}")
String circuit(@PathVariable("id") Integer id);

}

创建启动主类,添加 @EnableDiscoveryClient@EnableFeignClients 注解

1
2
3
4
5
6
7
8
9
10
@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients
public class ConsumerApplication {

public static void main(String[] args) {
SpringApplication.run(ConsumerApplication.class, args);
}

}

application.yml 文件中配置端口号、注册中心地址、断路器

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
server:
port: 8003

spring:
application:
name: consumer-service
cloud:
# Consul
consul:
host: 127.0.0.1
port: 8500
# 注册中心
discovery:
service-name: ${spring.application.name}
heartbeat:
enabled: true
# OpenFeign
openfeign:
# 开启断路器和分组激活
circuitbreaker:
enabled: true
group:
enabled: true # 没有开启分组则永远不用分组的配置。精确优先、分组次之(开了分组)、默认最后

# Resilience4j
resilience4j:
# 断路器
circuitbreaker:
configs:
# 断路器的默认配置
default:
failureRateThreshold: 50 # 设置 50% 的调用失败时打开断路器,超过失败请求百分⽐ CircuitBreaker 将切换为 OPEN 状态
slidingWindowType: COUNT_BASED # 滑动窗口的类型,可选 COUNT_BASED 和 TIME_BASED,默认为 COUNT_BASED
slidingWindowSize: 6 # 滑动窗⼝的⼤⼩,配置 COUNT_BASED 时表示 6 个请求,配置 TIME_BASED 时表示 6 秒
minimumNumberOfCalls: 6 # 断路器计算失败率或慢调用率之前所需的最小样本(每个滑动窗口周期)。如果 minimumNumberOfCalls 为 10,则必须最少记录 10 个样本,然后才能计算失败率。如果只记录了 9 次调用,即使所有 9 次调用都失败,断路器也不会开启
automaticTransitionFromOpenToHalfOpenEnabled: true # 是否启用自动从开启状态切换到半开状态,默认值为 true。如果启用,CircuitBreaker 将自动从开启状态切换到半开状态,并允许一些请求通过以测试服务是否恢复正常
waitDurationInOpenState: 5s # 断路器从 OPEN 状态到 HALF_OPEN 状态需要等待的时间
permittedNumberOfCallsInHalfOpenState: 2 # 半开状态允许的最大请求数,默认值为 10
recordExceptions:
- java.lang.Exception
instances:
# 指定特定的服务实例或者方法使用哪个断路器配置,还可以在每个实例下进行自定义配置
provider-service:
baseConfig: default

创建用于测试的 Controller 类,并使用 @CircuitBreaker 注解来实现断路器的功能

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
@RestController
public class ConsumerController {

@Autowired
private ProviderFeignApi providerFeignApi;

/**
* 该接口用于测试服务调用方(消费者)的断路器
* <p> @CircuitBreaker 注解是写在服务调用方(消费者)一侧
*/
@GetMapping(value = "/consumer/circuit/{id}")
@CircuitBreaker(name = "provider-service", fallbackMethod = "circuitFallback")
public String circuitBreaker(@PathVariable("id") Integer id) {
return providerFeignApi.circuit(id);
}

/**
* 服务降级后的兜底处理方法
*/
public String circuitFallback(Integer id, Throwable t) {
// 这里是容错处理逻辑,返回备用结果
return "CircuitFallback,系统繁忙,请稍后再试 /(ㄒoㄒ)/~~";
}

}

特别注意

  • 这里 @CircuitBreaker 注解的 name 属性必须与 application.yml 配置文件中的 instances 匹配对应。
  • 换言之,name 指定的名称必须和 instances 中的某个键匹配,这样才能让指定的服务实例或者方法使用相应的 CircuitBreaker 配置。
测试案例代码
  • 第一次测试

    • 单次访问 http://localhost:8003/consumer/circuit/11,可以返回正确的结果。
  • 第二次测试

    • 单次访问 http://localhost:8003/consumer/circuit/-4,故意抛出异常,返回的是降级处理后的结果: CircuitFallback,系统繁忙,请稍后再试 /(ㄒ o ㄒ)/~~
  • 第三次测试

    • 单次访问 http://localhost:8003/consumer/circuit/11
    • 单次访问 http://localhost:8003/consumer/circuit/-4,故意抛出异常
    • 以上操作间隔重复执行三遍(一共执行 6 次),当出现 50% 错误率后,会触发熔断并给出服务降级的处理结果,以此告知调用方(消费者)服务不可用。
    • 此时就算是再次调用正确的接口地址也无法调用服务,因为断路器还处于 OPEN 状态;等一会断路器切换到 HALF_OPEN 半开状态后,继续访问正确的接口地址,断路器会慢慢切换到 CLOSE 状态,最终可以正常调用接口地址。
  • 第四次测试

    • 多次访问 http://localhost:8003/consumer/circuit/-4,故意抛出异常,让熔断器切换到 OPEN 状态,即触发熔断。
    • 单次访问 http://localhost:8003/consumer/circuit/11,发现刚开始熔断器不满足条件,就算是调用正确的接口地址也无法调用服务,等一会后才可以正常调用接口地址。
下载案例代码
  • 完整的案例代码可以从 这里 下载得到。

断路器的使用案例二

本节将演示如何使用 OpenFeign + Resilience4j CircuitBreaker 实现熔断与降级,并且是基于时间的滑动窗口。案例需求如下:

  • 在最近 2 秒内,当调用方法的失败率达到 50% 或者慢调用率达到 30% 时,CircuitBreaker 将切换到开启 OPEN 状态(保险丝跳闸断电)来拒绝所有请求。
  • 等待 5 秒后,CircuitBreaker 将自动从 OPEN 状态切换到 HALF_OPEN 半开状态,允许一些请求通过以测试服务是否恢复正常。
  • 如果还是调用失败,CircuitBreaker 将重新切换到开启 OPEN 状态;如果调用正常,CircuitBreaker 将切换到 CLOSED 状态来恢复正常处理请求。

创建父级 Pom 工程

在父工程里面配置好工程需要的父级依赖,目的是为了更方便管理与简化配置,具体 Maven 配置如下:

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
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<hutool.version>5.8.22</hutool.version>
<lombok.version>1.18.26</lombok.version>
<spring.boot.version>3.2.0</spring.boot.version>
<spring.boot.test.version>3.1.5</spring.boot.test.version>
<spring.cloud.version>2023.0.0</spring.cloud.version>
</properties>

<dependencyManagement>
<dependencies>
<!--SpringBoot-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring.boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!--SpringCloud-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring.cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!--HuTool-->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>${hutool.version}</version>
</dependency>
<!--Lombok-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
<optional>true</optional>
</dependency>
<!--SpringBoot Test-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<version>${spring.boot.test.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
</dependencyManagement>
创建 Provider 工程

为了测试 Resilience4j CircuitBreaker(断路器)的使用效果,先创建一个服务提供者。这里创建 Provider 的 Maven 工程,由于需要将服务注册到 Consul,工程下的 pom.xml 文件需要引入 spring-cloud-starter-consul-discovery

1
2
3
4
5
6
7
8
9
10
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-consul-discovery</artifactId>
<exclusions>
<exclusion>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
</exclusion>
</exclusions>
</dependency>

创建 Provider 的启动主类,添加注解 @EnableDiscoveryClient,将服务注册到 Consul

1
2
3
4
5
6
7
8
9
@SpringBootApplication
@EnableDiscoveryClient
public class ProviderApplication {

public static void main(String[] args) {
SpringApplication.run(ProviderApplication.class, args);
}

}

application.yml 文件中指定服务名称(provider-service)、注册中心地址与端口号

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
server:
port: 8001

spring:
application:
name: provider-service
cloud:
# Consul
consul:
host: 127.0.0.1
port: 8500
# 注册中心
discovery:
service-name: ${spring.application.name}
heartbeat:
enabled: true

创建用于测试的 Controller 类

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
@RestController
public class ProviderController {

/**
* 该接口用于测试服务调用方(消费者)的断路器
*/
@GetMapping(value = "/provider/circuit/{id}")
public String circuit(@PathVariable("id") Integer id) {
// 模拟业务处理出错
if (id == -4) {
throw new RuntimeException("Circuit id 不能为负数");
}

// 模拟业务长时间处理
if (id == 9999) {
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
return "Hello, circuit! inputId : " + id + " \t " + UUID.fastUUID();
}

}
创建 Consumer 工程

若要使用 Resilience4j CircuitBreaker(断路器),则需要在 pom.xml 文件中引入依赖 spring-cloud-starter-circuitbreaker-resilience4j,由于基于注解的方式配置断路器是依赖 AOP 实现的,所以必须引入 spring-boot-starter-aop。另外,由于需要从 Consul 获取服务列表,即作为 Consul 的客户端,还需要引入 spring-cloud-starter-consul-discovery

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
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-consul-discovery</artifactId>
<exclusions>
<exclusion>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
</exclusion>
</exclusions>
</dependency>

<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-circuitbreaker-resilience4j</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>

创建服务接口类,通过 OpenFeign 调用 Provider 提供的服务

1
2
3
4
5
6
7
8
9
10
@FeignClient("provider-service")
public interface ProviderFeignApi {

/**
* 该接口用于测试服务调用方(消费者)的断路器
*/
@GetMapping(value = "/provider/circuit/{id}")
String circuit(@PathVariable("id") Integer id);

}

创建启动主类,添加 @EnableDiscoveryClient@EnableFeignClients 注解

1
2
3
4
5
6
7
8
9
10
@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients
public class ConsumerApplication {

public static void main(String[] args) {
SpringApplication.run(ConsumerApplication.class, args);
}

}

application.yml 文件中配置端口号、注册中心地址、断路器

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
server:
port: 8003

spring:
application:
name: consumer-service
cloud:
# Consul
consul:
host: 127.0.0.1
port: 8500
# 注册中心
discovery:
service-name: ${spring.application.name}
heartbeat:
enabled: true
# OpenFeign
openfeign:
# 开启断路器和分组激活
circuitbreaker:
enabled: true
group:
enabled: true # 没有开启分组则永远不用分组的配置。精确优先、分组次之(开了分组)、默认最后

# Resilience4j
resilience4j:
# 超时处理
timelimiter:
configs:
default:
timeout-duration: 20s # 神坑的位置,timelimiter 默认限制请求处理耗时为 1s,超过 1s 就会认为请求超时,如果配置了降级,就会走降级逻辑
# 断路器
circuitbreaker:
configs:
# 断路器的默认配置
default:
failureRateThreshold: 50 # 设置 50% 的调用失败时打开断路器,超过失败请求百分⽐ CircuitBreaker 切换为 OPEN 状态
slowCallDurationThreshold: 2s # 慢调用的时间阈值,高于这个阈值的视为慢调用,并增加慢调用比例
slowCallRateThreshold: 30 # 慢调用百分比阀值,断路器把调用时间⼤于 slowCallDurationThreshold 视为慢调用,当慢调用比例高于阈值时,断路器将会打开,并开启服务降级
slidingWindowType: TIME_BASED # 滑动窗口的类型,可选 COUNT_BASED 和 TIME_BASED,默认为 COUNT_BASED
slidingWindowSize: 2 # 滑动窗⼝的⼤⼩,配置 COUNT_BASED 时表示 2 个请求,配置 TIME_BASED 时表示 2 秒
minimumNumberOfCalls: 2 # 断路器计算失败率或慢调用率之前所需的最小样本(每个滑动窗口周期),如果 minimumNumberOfCalls 为 10,则必须最少记录 10 个样本,然后才能计算失败率。如果只记录了 9 次调用,即使所有 9 次调用都失败,断路器也不会开启
permittedNumberOfCallsInHalfOpenState: 2 # 断路器在半开状态下允许的最大请求数,默认值为 10
automaticTransitionFromOpenToHalfOpenEnabled: true # 是否启用自动从开启状态切换到半开状态,默认值为 true。如果启用,CircuitBreaker 将自动从开启状态切换到半开状态,并允许一些请求通过以测试服务是否恢复正常
waitDurationInOpenState: 5s # 断路器从 OPEN 到 HALF_OPEN 状态需要等待的时间
recordExceptions:
- java.lang.Exception
instances:
# 指定特定的服务实例或者方法使用哪个断路器配置,还可以在每个实例下进行自定义配置
provider-service:
baseConfig: default

特别注意

这里必须配置 Resilience4j TimeLimiter 的 timeout-duration 的参数,因为 TimeLimiter 默认限制请求处理耗时为 1 秒,超过 1 秒 就会认为请求超时(在默认情况下不会打印超时的异常信息,而是直接走 Fallback 处理逻辑,导致很难发现超时问题)。为了避免影响后面的代码测试,需要将 timeout-duration 的参数值设置大一点。

创建用于测试的 Controller 类,并使用 @CircuitBreaker 注解来实现断路器的功能

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
@RestController
public class ConsumerController {

@Autowired
private ProviderFeignApi providerFeignApi;

/**
* 该接口用于测试服务调用方(消费者)的断路器
* <p> @CircuitBreaker 注解是写在服务调用方(消费者)一侧
*/
@GetMapping(value = "/consumer/circuit/{id}")
@CircuitBreaker(name = "provider-service", fallbackMethod = "circuitFallback")
public String circuitBreaker(@PathVariable("id") Integer id) {
return providerFeignApi.circuit(id);
}

/**
* 服务降级后的兜底处理方法
*/
public String circuitFallback(Integer id, Throwable t) {
// 这里是容错处理逻辑,返回备用结果
return "CircuitFallback,系统繁忙,请稍后再试 /(ㄒoㄒ)/~~";
}

}

特别注意

  • 这里 @CircuitBreaker 注解的 name 属性必须与 application.yml 配置文件中的 instances 匹配对应。
  • 换言之,name 指定的名称必须和 instances 中的某个键匹配,这样才能让指定的服务实例或者方法使用相应的 CircuitBreaker 配置。
测试案例代码
  • 第一次测试

    • 单次访问 http://localhost:8003/consumer/circuit/22,可以正常调用接口
    • 单次访问 http://localhost:8003/consumer/circuit/9999,故意超时,可以正常调用接口
  • 第二次测试

    • 四次并发访问 http://localhost:8003/consumer/circuit/9999,多次故意超时,断路器会切换到 OPEN 状态(即触发熔断)
    • 单次访问 http://localhost:8003/consumer/circuit/22,发现刚开始熔断器不满足条件,就算是调用正确的接口地址也无法调用服务,等一会后才可以正常调用接口地址
下载案例代码
  • 完整的案例代码可以从 这里 下载得到。

断路器的使用总结

  • 断路器的最佳使用实践

    • 基于访问数量的滑动窗口(COUNT_BASED)和基于时间的滑动窗口(TIME_BASED)不建议混合使用。
    • 推荐使用基于访问数量的滑动窗口(COUNT_BASED),一家之言仅供参考,特殊场景特殊分析。
  • 断路器开启或者关闭的条件

    • 当失败率和慢调用率达到一定的条件后,断路器将会切换到 OPEN 状态(类似保险丝跳闸),即服务熔断。
    • 当断路器处于 OPEN 状态的时候,服务调用者(消费者)的所有请求都不会再发送给服务提供者(生产者),而是直接走 fallbackmetnod 兜底处理方法,即服务降级。
    • 一段时间过后,断路器会从 OPEN 状态切换到 HALF_OPEN 半开状态,然后会放行几个请求过去测试服务提供者是否恢复正常。如果调用成功,断路器会切换到 CLOSE 状态(类似保险丝闭合,恢复可用);如果调用失败,则继续处于 OPEN 状态。