微服务容错简介

在高并发访问下,比如天猫双11,流量持续不断的涌入,服务之间的相互调用频率突然增加,引发系统负载过高,这时系统所依赖的服务的稳定性对系统的影响非常大,而且还有很多不确定因素引起雪崩,如网络连接中断,服务宕机等。一般微服务容错组件提供了限流、隔离、降级、熔断等手段,可以有效保护我们的微服务系统。

Resilience4j

https://resilience4j.readme.io/docs/getting-started

https://github.com/lmhmhl/Resilience4j-Guides-Chinese/blob/main/index.md

Resilience4j是一个轻量级容错框架,设计灵感来源于Netflix 的Hystrix框架,为函数式编程所设计。

Resilience4j 提供了一组高阶函数(装饰器),包括断路器,限流器,重试,隔离,可以对任何的函数式接口,lambda表达式,或方法的引用进行增强,并且这些装饰器可以进行叠加。这样做的好处是,你可以根据需要选择特定的装饰器进行组合。

Supplier<String> supplier = () -> service.sayHelloWorld(param1);

String result = Decorators.ofSupplier(supplier)
  .withBulkhead(Bulkhead.ofDefaults("name"))
  .withCircuitBreaker(CircuitBreaker.ofDefaults("name"))
  .withRetry(Retry.ofDefaults("name"))
  .withFallback(asList(CallNotPermittedException.class, BulkheadFullException.class),  
      throwable -> "Hello from fallback")
  .get()

在使用时,你不需要引入所有和Resilience4j相关的包,只需要引入所需要的即可。

核心模块

  • resilience4j-circuitbreaker: 熔断
  • resilience4j-ratelimiter: 限流
  • resilience4j-bulkhead: 隔离
  • resilience4j-retry: 自动重试
  • resilience4j-cache: 结果缓存
  • resilience4j-timelimiter: 超时处理

Resilience4j使用

添加依赖(按需引入)
//使用注解时引入
<dependency>
    <groupId>io.github.resilience4j</groupId>
    <artifactId>resilience4j-spring-boot2</artifactId>
    <version>1.7.1</version>
</dependency>

<dependency>
    <groupId>io.github.resilience4j</groupId>
    <artifactId>resilience4j-circuitbreaker</artifactId>
    <version>${resilience4j.version}</version>
</dependency>
<dependency>
    <groupId>io.github.resilience4j</groupId>
    <artifactId>resilience4j-ratelimiter</artifactId>
    <version>${resilience4j.version}</version>
</dependency>
<dependency>
    <groupId>io.github.resilience4j</groupId>
    <artifactId>resilience4j-retry</artifactId>
    <version>${resilience4j.version}</version>
</dependency>
<dependency>
    <groupId>io.github.resilience4j</groupId>
    <artifactId>resilience4j-bulkhead</artifactId>
    <version>${resilience4j.version}</version>
</dependency>
断路器(CircuitBreaker)

断路器通过有限状态机实现,有三个普通状态:关闭、开启、半开,还有两个特殊状态:禁用、强制开启。

resilience4j 谁做的_java

断路器使用滑动窗口来存储和统计调用的结果。你可以选择基于调用数量的滑动窗口或者基于时间的滑动窗口。基于访问数量的滑动窗口统计了最近N次调用的返回结果。居于时间的滑动窗口统计了最近N秒的调用返回结果。

resilience4j 谁做的_resilience4j 谁做的_02

当熔断器关闭时,所有的请求都会通过熔断器。如果失败率超过设定的阈值,熔断器就会从关闭状态转换到打开状态,这时所有的请求都会被拒绝。当经过一段时间后,熔断器会从打开状态转换到半开状态,这时仅有一定数量的请求会被放入,并重新计算失败率,如果失败率超过阈值,则变为打开状态,如果失败率低于阈值,则变为关闭状态。

断路器使用滑动窗口来存储和统计调用的结果。你可以选择基于调用数量的滑动窗口或者基于时间的滑动窗口。基于访问数量的滑动窗口统计了最近N次调用的返回结果。居于时间的滑动窗口统计了最近N秒的调用返回结果。

除此以外,熔断器还会有两种特殊状态:DISABLED(始终允许访问)和FORCED_OPEN(始终拒绝访问)。这两个状态不会生成熔断器事件(除状态装换外),并且不会记录事件的成功或者失败。退出这两个状态的唯一方法是触发状态转换或者重置熔断器。

创建一个CircuitBreakerRegistry

Resilience4j使用基于ConcurrentHashMap的CircuitBreakerRegistry来保证线程安全和原子性,你可以使用CircuitBreakerRegistry管理(创建和检索)断路器实例,可以使用全局默认的CircuitBreakerConfig配置为所有的断路器实例创建一个CircuitBreakerRegistry。

CircuitBreakerRegistry circuitBreakerRegistry = 
  CircuitBreakerRegistry.ofDefaults();
创建和配置CircuitBreaker

你可以自定义CircuitBreakerConfig,为了创建自定义的CircuitBreakerConfig,你可以使用CircuitBreakerConfig建造器,你可以使用建造者模式来配置下面的属性。

配置属性

默认值

描述

failureRateThreshold

50

以百分比配置失败率阈值。当失败率等于或大于阈值时,断路器状态并关闭变为开启,并进行服务降级。

slowCallRateThreshold

100

以百分比的方式配置,断路器把调用时间大于slowCallDurationThreshold的调用视为满调用,当慢调用比例大于等于阈值时,断路器开启,并进行服务降级。

slowCallDurationThreshold

60000 [ms]

配置调用时间的阈值,高于该阈值的呼叫视为慢调用,并增加慢调用比例。

permittedNumberOfCallsInHalfOpenState

10

断路器在半开状态下允许通过的调用次数。

maxWaitDurationInHalfOpenState

0

断路器在半开状态下的最长等待时间,超过该配置值的话,断路器会从半开状态恢复为开启状态。配置是0时表示断路器会一直处于半开状态,直到所有允许通过的访问结束。

slidingWindowType

COUNT_BASED

配置滑动窗口的类型,当断路器关闭时,将调用的结果记录在滑动窗口中。滑动窗口的类型可以是count-based或time-based。如果滑动窗口类型是COUNT_BASED,将会统计记录最近slidingWindowSize次调用的结果。如果是TIME_BASED,将会统计记录最近slidingWindowSize秒的调用结果。

slidingWindowSize

100

配置滑动窗口的大小。

minimumNumberOfCalls

100

断路器计算失败率或慢调用率之前所需的最小调用数(每个滑动窗口周期)。例如,如果minimumNumberOfCalls为10,则必须至少记录10个调用,然后才能计算失败率。如果只记录了9次调用,即使所有9次调用都失败,断路器也不会开启。

waitDurationInOpenState

60000 [ms]

断路器从开启过渡到半开应等待的时间。

automaticTransition FromOpenToHalfOpenEnabled

false

如果设置为true,则意味着断路器将自动从开启状态过渡到半开状态,并且不需要调用来触发转换。创建一个线程来监视断路器的所有实例,以便在WaitDurationInOpenstate之后将它们转换为半开状态。但是,如果设置为false,则只有在发出调用时才会转换到半开,即使在waitDurationInOpenState之后也是如此。这里的优点是没有线程监视所有断路器的状态。

recordExceptions

empty

记录为失败并因此增加失败率的异常列表。 除非通过ignoreExceptions显式忽略,否则与列表中某个匹配或继承的异常都将被视为失败。 如果指定异常列表,则所有其他异常均视为成功,除非它们被ignoreExceptions显式忽略。

ignoreExceptions

empty

被忽略且既不算失败也不算成功的异常列表。 任何与列表之一匹配或继承的异常都不会被视为失败或成功,即使异常是recordExceptions的一部分。

recordException

throwable -> true· By default all exceptions are recored as failures.

一个自定义断言,用于评估异常是否应记录为失败。 如果异常应计为失败,则断言必须返回true。如果出断言返回false,应算作成功,除非ignoreExceptions显式忽略异常。

ignoreException

throwable -> false By default no exception is ignored.

自定义断言来判断一个异常是否应该被忽略,如果应忽略异常,则谓词必须返回true。 如果异常应算作失败,则断言必须返回false。

// 自定义断路器配置
CircuitBreakerConfig circuitBreakerConfig = CircuitBreakerConfig.custom()
  .failureRateThreshold(50)
  .slowCallRateThreshold(50)
  .waitDurationInOpenState(Duration.ofMillis(1000))
  .slowCallDurationThreshold(Duration.ofSeconds(2))
  .permittedNumberOfCallsInHalfOpenState(3)
  .minimumNumberOfCalls(10)
  .slidingWindowType(SlidingWindowType.TIME_BASED)
  .slidingWindowSize(5)
  .recordException(e -> INTERNAL_SERVER_ERROR
                 .equals(getResponse().getStatus()))
  .recordExceptions(IOException.class, TimeoutException.class)
  .ignoreExceptions(BusinessException.class, OtherBusinessException.class)
  .build();
# 熔断器半开时的缓冲区大小
resilience4j.circuitbreaker.configs.default.permitted-number-of-calls-in-half-open-state=3

# 熔断器关闭时的缓冲区大小
resilience4j.circuitbreaker.configs.default.sliding-window-size=10
# 熔断器从打开到半开需要的时间
resilience4j.circuitbreaker.configs.default.wait-duration-in-open-state=2s
# 熔断器打开的失败阈值
resilience4j.circuitbreaker.configs.default.failureRateThreshold=30

# 事件缓冲区大小
resilience4j.circuitbreaker.configs.default.eventConsumerBufferSize=10
# 健康监测
resilience4j.circuitbreaker.configs.default.registerHealthIndicator=true
# 是否自动从打开到半开,不需要触发
resilience4j.circuitbreaker.configs.default.automaticTransitionFromOpenToHalfOpenEnabled=true
# 记录的异常
resilience4j.circuitbreaker.configs.default.recordExceptions=cn.dragonsoftbravo.practice.exception.BusinessBException,cn.dragonsoftbravo.practice.exception.BusinessAException
# 忽略的异常
resilience4j.circuitbreaker.configs.default.ignoreExceptions=cn.dragonsoftbravo.practice.exception.BusinessAException


resilience4j.circuitbreaker.instances.backendA.failure-rate-threshold=50
#慢调用时间阈值,高于这个阈值的呼叫视为慢调用,并增加慢调用比例
resilience4j.circuitbreaker.instances.backendA.slow-call-duration-threshold=2s
#慢调用百分比阈值,断路器把调用时间大于slow-call-duration-threshold,视为慢调用
resilience4j.circuitbreaker.instances.backendA.slow-call-rate-threshold=30
resilience4j.circuitbreaker.instances.backendA.sliding-window-size=10
resilience4j.circuitbreaker.instances.backendA.sliding-window-type=TIME_BASED
resilience4j.circuitbreaker.instances.backendA.minimum-number-of-calls=2
resilience4j.circuitbreaker.instances.backendA.permitted-number-of-calls-in-half-open-state=2
#从OPEN到HALF_OPEN状态需要等待的时间
resilience4j.circuitbreaker.instances.backendA.wait-duration-in-open-state=2s
隔离(Builkhead)

Resilience4j提供了两种隔离的实现方式,可以限制并发执行的数量。

  • SemaphoreBulkhead使用了信号量
  • FixedThreadPoolBulkhead使用了有界队列和固定大小线程池

SemaphoreBulkhead使用了信号量,配置属性,如下表4-3所示。

配置属性

默认值

描述

maxConcurrentCalls

25

隔离允许线程并发执行的最大数量

maxWaitDuration

0

当达到并发调用数量时,新的线程执行时将被阻塞,这个属性表示最长的等待时间。

//为Bulkhead创建自定义的配置
BulkheadConfig config = BulkheadConfig.custom()
    .maxConcurrentCalls(150)
    .maxWaitDuration(Duration.ofMillis(500))
    .build();
#隔离允许并发线程执行的最大数量
resilience4j.bulkhead.configs.default.max-concurrent-calls=5 
#当达到并发调用数量时,新的线程的阻塞时间
resilience4j.bulkhead.configs.default.max-wait-duration=20ms
resilience4j.bulkhead.instances.backendA.base-config=default
resilience4j.bulkhead.instances.backendB.max-concurrent-calls=20
resilience4j.bulkhead.instances.backendB.max-wait-duration.=10ms
#最大线程池大小
resilience4j.thread-pool-bulkhead.configs.default.max-thread-pool-size=4
#核心线程池大小
resilience4j.thread-pool-bulkhead.configs.default.core-thread-pool-size=2
#队列容量
resilience4j.thread-pool-bulkhead.configs.default.queue-capacity=2 

resilience4j.thread-pool-bulkhead.instances.backendA.base-config=default
resilience4j.thread-pool-bulkhead.instances.backendB.max-thread-pool-size=1
resilience4j.thread-pool-bulkhead.instances.backendB.core-thread-pool-size=1
resilience4j.thread-pool-bulkhead.instances.backendB.queue-capacity=1
/**
 * 信号量隔离
 */
BulkheadConfig config = BulkheadConfig.custom()
        .maxConcurrentCalls(5)
        .maxWaitDuration(Duration.ofMillis(500))
        .build();


/**
 * 线程池隔离
 */
ThreadPoolBulkheadConfig  poolBulkheadConfig= ThreadPoolBulkheadConfig.custom()
        .queueCapacity(2)
        .maxThreadPoolSize(4)
        .coreThreadPoolSize(2)
        .build();

Bulkhead bulkhead = Bulkhead.of("name", config);

ThreadPoolBulkhead threadPoolBulkhead = ThreadPoolBulkhead.of("name", poolBulkheadConfig);
限流(RateLimiter)

Resilience4j提供了一个限流器,它将从epoch开始的所有纳秒划分为多个周期。每个周期的持续时间RateLimiterConfig.limitRefreshPeriod。在每个周期开始时,限流器将活动权限数设置为RateLimiterConfig.limitForPeriod。期间, 对于限流器的调用者,它看起来确实是这样的,但是对于AtomicRateLimiter实现,如果RateLimiter未被经常使用,则会在后台进行一些优化,这些优化将跳过此刷新。

限流器的默认实现是AtomicRateLimiter,它通过原子引用管理其状态。这个AtomicRateLimiter状态完全不可变,并且具有以下字段:

  • activeCycle -上次调用的周期号
  • activePermissions -在上次调用结束后,可用的活跃权限数。如果保留了某些权限,则可以为负。
  • nanosToWait - 最后一次调用要等待的纳秒数

还有一个使用信号量的SemaphoreBasedRateLimiter和一个调度程序,它将在每个RateLimiterConfig#limitRefreshPeriod之后刷新活动权限数。

Resilience4j的限流模块RateLimter基于滑动窗口,和令牌桶限流算法,配置如下,如下表4-5所示。

属性

默认值

描述

timeoutDuration

5秒

线程等待权限的默认等待时间

limitRefreshPeriod

500纳秒

限流器每隔limitRefreshPeriod刷新一次,将允许处理的最大请求数量重置为limitForPeriod。

limitForPeriod

50

在一次刷新周期内,允许执行的最大请求数

#线程等待权限的默认等待时间
resilience4j.ratelimiter.configs.default.timeout-duration=5
#限流器每隔1s刷新一次,将允许处理的最大请求重置
resilience4j.ratelimiter.configs.default.limit-refresh-period=1s
#在一个刷新周期内,允许执行的最大请求数
resilience4j.ratelimiter.configs.default.limit-for-period=2

resilience4j.ratelimiter.instances.backendA.base-config=default

resilience4j.ratelimiter.instances.backendB.timeout-duration=5
resilience4j.ratelimiter.instances.backendB.limit-refresh-period=1s
resilience4j.ratelimiter.instances.backendB.limit-for-period=5
RateLimiterConfig limiterConfig = RateLimiterConfig.custom()
        .limitRefreshPeriod(Duration.ofSeconds(1))
        .limitForPeriod(10)
        .timeoutDuration(Duration.ofMillis(25))
        .build();

RateLimiter rateLimiter = RateLimiterRegistry.of(limiterConfig).rateLimiter("name2");
重试(Retry)
创建 RetryRegistry

就像断路器模块一样,这么模块提供了在内存中的RetryRegistry,你可以使用这个管理(创建和检索)Retry实例。

RetryRegistry retryRegistry = RetryRegistry.ofDefaults();
创建和配置重试

你可以提供一个自定义的全局RetryConfig,为了创建一个自定义的全局RetryConfig,可以使用建造者模式对RetryConfig进行配置。

  • 最大的重试次数 。
  • 连续两次重试之间的时间间隔。
  • 自定义断言机制,评估一个响应是否可以触发重试机制。
  • 自定义断言机制,评估一个异常是否可以出发重试机制。
  • 定义一个异常的列表,这些异常能够触发重试机制。
  • 定义一个异常的列表,这些异常应该被忽略并且不会触发重试机制。

属性

默认值

描述

maxAttempts

3

最大重试次数

waitDuration

500 [ms]

两次重试之间的时间间隔

intervalFunction

numOfAttempts -> waitDuration

修改重试间隔的函数。默认情况下,等待时间保持不变。

retryOnResultPredicate

result -> false

配置用于计算是否应重试的断言。如果要重试,断言必须返回true,否则返回false。

retryOnExceptionPredicate

throwable -> true

配置一个断言,判断某个异常发生时,是否要进行重试。如果要重试,断言必须返回true,否则必须返回false。

retryExceptions

empty

配置一个Throwable类型的列表,被记录为失败类型,需要进行重试,支持子类型。

ignoreExceptions

empty

配置一个Throwable类型的列表,被记录为忽略类型,不会进行重试,支持子类型。

resilience4j.retry.configs.default.max-attempts=3
resilience4j.retry.configs.default.wait-duration=2s
resilience4j.retry.configs.default.enable-exponential-backoff=true
resilience4j.retry.configs.default.ignore-exceptions=java.lang.IllegalStateException,com.beust.jcommander.ParameterException
resilience4j.retry.configs.default.retry-exceptions=java.util.concurrent.TimeoutException,java.net.ConnectException
RetryConfig config = RetryConfig.custom()
  .maxAttempts(2)
  .waitDuration(Duration.ofMillis(1000))
  .retryOnResult(response -> response.getStatus() == 500)
  .retryOnException(e -> e instanceof WebServiceException)
  .retryExceptions(IOException.class, TimeoutException.class)
  .ignoreExceptions(BusinessException.class, OtherBusinessException.class)
  .build();