1. 概述
Circuit breaker是一套规范和接口,落地实现是Resilience4j,Resilience4j是一个专为函数式编程设计的轻量级容错库,Resilience4j提供高阶函数(装饰器),以通过断路器、速率限制器、重试或隔板增强任何功能接口、lambda表达式或方法引用。可以在任何函数式接口、lambda表达式或方法引用上堆叠多个装饰器,优点是可以选择需要的装饰器。
Resilience4j提供以下几个核心模块:
- resilience4j-circuitbreaker:熔断
- resilience4j-ratelimiter:速率限制
- resilience4j-bulkhead:舱壁/隔离
- resilience4j-retry:自动重试(同步和异步)
- resilience4j-timelimiter:超时处理
- resilience4j-cache:结果缓存
CircuitBreaker的目的是保护分布式系统免受故障和异常,提高系统的可用性和健壮性。当一个组件或服务出现故障时,CircuitBreaker会迅速切换到开放OPEN状态(保险丝跳闸断电),阻止请求发送到该组件或服务从而避免更多的请求发送到该组件或服务。这可以减少对该组件或服务的负载,防止该组件或服务进一步崩溃,并使整个系统能够继续正常运行。同时,其还可以提高系统的可用性和健壮性,因为其可以在分布式系统的各个组件之间自动切换,从而避免单点故障的问题。更多详情可以参阅官网和github中文手册
示意图如下:
本文主要介绍resilience4j提供的熔断、隔离和限速
2. 服务熔断
2.1. 三种状态
断路器有三个普通状态:关闭(CLOSED) 、开启(OPEN)和半开(HALF_OPEN) ,还有两个特殊状态:禁用(DISABLED)和强制开启(FORCED_OPEN)
当熔断器关闭时,所有的请求都会通过熔断器
- 如果失败率超过设定的阈值,熔断器就会从关闭状态转换为打开状态,这时所有的请求都会被拒绝
- 当经过一段时间后,熔断器会从打开状态转换为半开状态,这时仅有一定数量的请求会被放入,并重新计算失败率
- 如果失败率超过阈值,则变为打开状态,如果失败率低于阈值,则变为关闭状态
断路器使用滑动窗口来存储和统计调用的结果,可以选择基于调用数量的滑动窗口或基于时间的滑动窗口
- 基于访问次数的滑动窗口统计了最近N次调用的返回结果,基于时间的滑动窗口统计了最近N秒调用的返回结果
熔断器另外两种特殊状态DISABLED(始终允许访问)和FORCED_OPEN(始终拒绝访问)
- 这两个状态不会生成熔断器事件(除状态转换外),并且不会记录事件的成功或失败
- 退出这两个状态的唯一方法是触发状态转换或者重置熔断器
2.2. 微服务集成circuitbreaker
2.2.1. 引入核心依赖
xml
<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>
2.2.2. 编写application.yml
熔断器分为基于次数和基于时间两种方式
基于次数配置如下:
yaml
resilience4j:
circuitbreaker:
configs:
default:
failure-rate-threshold: 50 #设置50%的调用失败时打开断路器,超过失败请求百分比CircuitBreaker变为OPEN状态
sliding-window-type: COUNT_BASED #滑动窗口的类型
sliding-window-size: 6 #滑动窗口的大小配置COUNT_BASED表示6个请求,配置TIME_BASED表示6秒
minimum-number-of-calls: 6 #断路器计算失败率或慢调用率之前所需的最小样本(每个滑动窗口周期),如果minimum-number-of-calls为10,则必须最少记录10个样本,然后才能计算失败率,如果只记录了9次调用,即使所有9次调用都失败,断路器也不会开启
automatic-transition-from-open-to-half-open-enabled: true #是否启用自动从开启状态过渡到半开状态,默认值为true,如果启用,circuitbreaker将自动从开启状态过渡到半开状态,并允许一些请求通过以测试服务是否恢复正常
wait-duration-in-open-state: 5s #从OPEN到HALF_OPEN状态需要等待的时间
permitted-number-of-calls-in-half-open-state: 2 #半开状态允许的最大请求数,默认值为10,在半开状态下,circuitbreaker将允许最多permitted-number-of-calls-in-half-open-state个请求通过,如果其中有任何一个请求失败,circuitbreaker将重新进入开启状态
record-exceptions:
- java.lang.Exception
instances:
spring-cloud-provider:
base-config: default
基于时间配置如下:
yaml
resilience4j:
timelimiter:
configs:
default:
timeout-duration: 10s #默认限制远程1s,超于1s就超时异常,配置了降级,就走降级逻辑
circuitbreaker:
configs:
default:
failure-rate-threshold: 50 #设置50%的调用失败时打开断路器,超过失败请求百分比CircuitBreaker变为OPEN状态
slow-call-duration-threshold: 2s #慢调用时间阈值,高于这个阈值的视为慢调用并增加慢调用比例
slow-call-rate-threshold: 30 #慢调用百分比峰值,断路器把调用时间大于slow-call-duration-threshold,视为慢调用,当慢调用比例高于阈值,断路器打开,并开启服务降级
sliding-window-type: TIME_BASED #滑动窗口的类型
sliding-window-size: 2 #滑动窗口的大小配置,配置TIME_BASED表示2秒
minimum-number-of-calls: 2 #断路器计算失败率或慢调用率之前所需的最小样本(每个滑动窗口周期)
wait-duration-in-open-state: 5s #从OPEN到HALF_OPEN状态需要等待的时间
permitted-number-of-calls-in-half-open-state: 2 #半开状态允许的最大请求数,默认值为10
record-exceptions:
- java.lang.Exception
instances:
spring-cloud-provider:
base-config: default
同时需要开启feign的circuit breaker
yaml
spring:
cloud:
openfeign:
circuitbreaker:
enabled: true #开启circuitbreaker和分组
group:
enabled: true #没开分组永远不用分组的配置,精确优先、分组次之(开了分组)、默认最后
2.2.3. 编写Controller接口类
java
@GetMapping(value = "/getCircuit/{id}")
@CircuitBreaker(name = "spring-cloud-circuit", fallbackMethod = "circuitFallBack")
public ResponseEntity<String> getCircuit(@PathVariable(value = "id") Integer id) {
return providerFeignClient.getCircuitInfo(id);
}
public ResponseEntity<String> circuitFallBack(Integer id, Throwable t) {
return ResponseEntity.ok("This is circuitFallBack, 系统错误,请稍后再试");
}
3. 服务隔离
3.1. 两种实现方式
主要用于依赖隔离&负载保护,即用来限制对于下游服务的最大并发数量的限制。Resilience4j提供了两种隔离的实现方式,可以限制并发执行的数量
- SemaphoreBulkhead使用了信号量
- FixedThreadPoolBulkhead使用了有界队列和固定大小线程池
更多详情可以参阅github中文手册
3.1.1. 信号量舱壁原理
当信号量有空闲时,进入系统的请求会直接获取信号量并开始业务处理。当信号量全被占用时,接下来的请求将会进入阻塞状态,SemaphoreBulkhead提供了一个阻塞计时器,如果阻塞状态的请求在阻塞计时内无法获取到信号量则会拒绝这些请求。若请求在阻塞计时内获取到了信号量,那将直接获取信号量并执行相应的业务处理
3.1.2. 固定线程池舱壁原理
FixedThreadPoolBulkhead使用一个固定线程池和一个等待队列来实现舱壁。当线程池中存在空闲时,则此时进入系统的请求将直接进入线程池开启新线程或使用空闲线程来处理请求。当线程池中无空闲时,接下来的请求将进入等待队列,若等待队列仍然无剩余空间时,接下来的请求将直接被拒绝。在队列中的请求等待线程池出现空闲时,将进入线程池进行业务处理。
另外,ThreadPoolBulkhead只对CompletableFuture方法有效,所以需要创建返回CompletableFuture类型的方法
3.2. 微服务集成bulkhead
3.2.1. 引入核心依赖
xml
<!--配置resilience4j依赖-->
<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>
<!--bulkhead依赖-->
<dependency>
<groupId>io.github.resilience4j</groupId>
<artifactId>resilience4j-bulkhead</artifactId>
</dependency>
3.2.2. 编写application.yml
信号量方式配置
yaml
spring:
cloud:
openfeign:
circuitbreaker:
enabled: true #开启circuitbreaker和分组
group:
enabled: true #没开分组永远不用分组的配置,精确优先、分组次之(开了分组)、默认最后
resilience4j:
bulkhead:
configs:
default:
max-concurrent-calls: 2 #隔离允许并发线程执行的最大数量
max-wait-duration: 1s #当达到并发调用数量时,新的线程的阻塞时间,只愿意等待1秒,过时不候进舱壁兜底fallback
instances:
spring-cloud-provider:
base-config: default
timelimiter:
configs:
default:
timeout-duration: 20s
线程池方式配置
yaml
spring:
cloud:
openfeign:
circuitbreaker:
enabled: true #开启circuitbreaker和分组
group:
enabled: false #没开分组永远不用分组的配置,精确优先、分组次之(开了分组)、默认最后;新启线程和原来主线程脱离
resilience4j:
timelimiter:
configs:
default:
timeout-duration: 10s #默认限制远程1秒
thread-pool-bulkhead:
configs:
default:
core-thread-pool-size: 4
max-thread-pool-size: 8
queue-capacity: 10
instances:
spring-cloud-provider:
base-config: default
3.2.3. 编写Controller接口类
java
@GetMapping(value = "/getBulkheadInfo/{id}")
@Bulkhead(name = "spring-cloud-provider", fallbackMethod = "bulkheadFallback", type = Bulkhead.Type.SEMAPHORE)
public ResponseEntity<String> getBulkheadInfo(@PathVariable(value = "id") Integer id) {
return providerFeignClient.getBulkheadInfo(id);
}
public ResponseEntity<String> bulkheadFallback(Integer id, Throwable t) {
return ResponseEntity.ok("bulkheadFallback, 隔板超出最大数量限制,系统繁忙,请稍后再试");
}
@GetMapping(value = "/getBulkheadPoolInfo/{id}")
@Bulkhead(name = "spring-cloud-provider", fallbackMethod = "bulkheadPoolFallback", type = Bulkhead.Type.THREADPOOL)
public CompletableFuture<String> getBulkheadPoolInfo(@PathVariable(value = "id") Integer id) {
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
return CompletableFuture.supplyAsync(() -> providerFeignClient.getBulkheadInfo(id).getBody());
}
public CompletableFuture<String> bulkheadPoolFallback(Integer id, Throwable t) {
return CompletableFuture.supplyAsync(() -> "bulkheadPoolFallback, 线程池隔板超出最大数量限制,系统繁忙,请稍后再试");
}
4. 服务限流
就是限制最大访问流量,系统能提供的最大并发是有限的,同时进入的请求数又太多,就需要限流
4.1. 四种限流算法
漏斗算法 :一个固定容量的漏桶,按照设定常量固定速率流出,不管源头流量多大,设定了均速流出,如果流入量超出了桶的容量,则流入的量将会溢出(被丢弃),而漏桶容量是不变的。其存在的缺点是,有两个变量,一个是桶的大小,支持流量突发增多时可以存多少请求,另一个是桶漏洞的大小。因为漏桶的漏出速率是固定的参数,所以,即使网络中不存在的资源冲突(没有发生拥塞),漏桶算法也不能使流量突发到端口速率。因此,漏桶算法对于存在突发特性的流量来说缺乏效率。
令牌桶算法 :以固定的速率向一个令牌桶中添加令牌,而当需要发送数据时,需要从桶中取出一个令牌,只有当桶中又令牌时才能发送数据,且取出的令牌数量与发送的数据量成正比。如果桶中没有令牌,那么数据将被限制等待,直到有足够的令牌可用。此种算法是Spring Cloud默认算法
滚动时间窗口算法 :允许固定数量的请求进入,超过数量就拒绝或排队,等下一个时间段进入,由于是在一个时间间隔内进行限制,如果用户在上个时间间隔结束前请求(但没有超过限制),同时在当前时间间隔刚开始请求(同样没超过限制),在各自的时间间隔内,这些请求都是正常的。其缺点是间隔临界的一段时间内的请求就会超过系统限制,可能导致系统被压垮。由于计数器算法存在时间临界点缺陷,因此在时间临界点左右的极短时间段内容易遭到攻击
滑动时间窗口算法:时间窗口是滑动的,其一需要定义窗口的大小,其二需要定义在窗口中滑动的大小。滑动大小不能超过窗口大小。滑动窗口算法是把固定时间片进行划分并且随着时间移动,移动方式为开始时间点变为时间列表中的第二哥时间点,结束时间点增加一个时间点,不断重复,通过这种方式可以巧妙的避开计数器的临界点的问题。
4.2. 微服务集成ratelimiter
4.2.1. 引入核心依赖
xml
<!--配置resilience4j依赖-->
<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>
<!--ratelimiter依赖-->
<dependency>
<groupId>io.github.resilience4j</groupId>
<artifactId>resilience4j-ratelimiter</artifactId>
</dependency>
4.2.2. 编写application.yml
yaml
spring:
cloud:
openfeign:
circuitbreaker:
enabled: true #开启circuitbreaker和分组
group:
enabled: true #没开分组永远不用分组的配置,精确优先、分组次之(开了分组)、默认最后
resilience4j:
ratelimiter:
configs:
default:
limit-for-period: 2 #在一次刷新周期内,允许执行的最大请求数
limit-refresh-period: 1s #限流器每隔limit-for-period刷新一次,将允许处理的最大请求数量重置为limit-for-period
timeout-duration: 1 #线程等待权限的默认等待时间
instances:
spring-cloud-provider:
base-config: default
4.2.3. 编写Controller接口类
java
@GetMapping(value = "/getRateLimiterInfo/{id}")
@RateLimiter(name = "spring-cloud-provider", fallbackMethod = "rateLimiterFallback")
public ResponseEntity<String> getRateLimiterInfo(@PathVariable(value = "id") Integer id) {
return ResponseEntity.ok(providerFeignClient.getRatelimitInfo(id).getBody());
}
public ResponseEntity<String> rateLimiterFallback(Integer id, Throwable t) {
return ResponseEntity.ok("rateLimiterFallback, 被限流了,禁止访问");
}