AICon上海|与字节、阿里、腾讯等企业共同探索Agent 时代的落地应用 了解详情
写点什么

Resilience4j 实用指南

  • 2021-03-12
  • 本文字数:6881 字

    阅读完需:约 23 分钟

Resilience4j 实用指南

简介

Resilience4j 是受 Netflix Hystrix 启发的轻量级容错库,但专为 Java 8 和函数式编程而设计。轻巧,因为该库仅使用 Vavr,而 Vavr 没有任何其他外部库依赖项。相比之下,Netflix Hystrix 对 Archaius 具有编译依赖性,而 Archaius 具有更多的外部库依赖性,例如 Guava 和 Apache Commons Configuration。另外,Netflix Hystrix 目前处于维护状态,不在主动开发,SpringCloud 在 2020 版本后,已经移除了 spring-cloud-netflix 相关模块,容错这块也推荐使用 Resilience4j。


Resilience4j 提供了通过装饰器的方式,以使用断路器,速率限制器,重试或隔板来增强任何功能接口,lambda 表达式或方法引用。您可以在任何功能接口,lambda 表达式或方法引用上堆叠多个装饰器来做熔断、限流等动作。

原理说明

容错是指系统在部分组件(一个或多个)发生故障时仍能正常运作的能力。要具有这个能力,通常要包含断路器(CircuitBreaker)、并发调用隔离(Bulkhead)、限流(RateLimiter)、重试(Retry)、超时(Timeout)机制。

断路器 — CircuitBreaker    

断路器一般通过 3 个有限状态机来实现,CLOSED、OPEN、HALF_OPEN。此外,还有 2 个特殊的状态机,DISABLED 和 FORCED_OPEN。状态的存储更新必须是线程安全的,即只有一个线程能够在某个时间点更新状态。



  • 关闭 —> 打开:当故障率等于或大于可配置的阈值时,CircuitBreaker 的状态将从“关闭”更改为“打开”。

  • 打开 —> 半开:当 CircuitBreaker 打开时,它会拒绝带有 CallNotPermittedException 的调用。经过一段等待时间后,CircuitBreaker 状态从 OPEN 变为 HALF_OPEN,并允许可配置数量的服务调用是否仍然不可用或再次变为可用。用 CallNotPermittedException 拒绝其他调用,直到所有允许的调用完成。如果故障率或慢呼叫率等于或大于配置的阈值,则状态会变回 OPEN。

  • 半开 —> 关闭:如果故障率和慢呼叫率低于阈值,则状态将变回“已关闭”。

  • DISABLED:始终允许调用。

  • FORCED_OPEN:始终拒绝调用。


滑动窗口:

断路器使用滑动窗口来存储和汇总调用结果,有两种选择。基于计数的滑动窗口 Count-based 和基于时间的滑动窗口 Time-based。          


基于计数的滑动窗口:汇总最近 N 次调用的结果。 

基于时间的滑动窗口:汇总最近 N 秒的调用结果。


相关配置:请查看附录 CircuitBreaker 配置。

并发调用隔离 — Bulkhead    

在系统设计中,需要预期故障的发生,将应用程序拆分成多个组件,通过资源隔离确保一个组件的故障不会影响其他的组件。例如:


生活:就像轮船用隔板(Bulkhead)分成多个小隔间,每个隔间都被隔板密封,这样可以防止洪水时整艘船沉没。


系统:两个服务 A 和服务 B,A 的某些 API 依赖 B,当服务 B 运行速度非常慢的时候,A 调用 B 的请求变多时,A 的性能会受到影响,服务 A 中那些不依赖于服务 B 的功能也无法处理。因此,需要隔离资源专门处理服务 A 依赖服务 B 的调用请求。

并发调用的隔离一般有两种方式来实现:信号量 Semaphore 和线程池 ThreadPool。Resilience4j 提供了 SemaphoreBulkhead 和 FixedThreadPoolBulkhead 来实现 Bulkhead。

相关配置:请查看附录 Bulkhead 配置。

限流 — RateLimiter    

流量控制是确保服务的高可用性和可靠性的重要技术。流控的场景,服务 A 依赖服务 B,服务 A 有 3 个实例,服务 B 会为了接收到请求做大量的 CPU / IO 密集工作,因此服务 B 在给定的时间范围内设置可以处理的最大请求数的限制。


设置流控后



流控和断路器的区别

流控:速率限制器通过控制吞吐量来帮助保护服务器免于过载。

断路器:当目标服务器出现故障/无响应时,Circuit Breaker 有助于保持客户端的安全和正常运行。

相关配置:请参考附录 RateLimiter 配置

重试 — Retry   

微服务体系中,多个服务互相依赖,当被依赖的服务出现问题而无法按预期响应时,就会级联到下游服务,导致不良的用户体验。



同样,在微服务体系中,一个服务会有多个实例,如果其中一个实例可能有问题,并且无法正确响应我们的请求,则如果我们重试该请求,则负载均衡器可以将请求发送到运行状况良好的节点并正确获得响应。通过重试,有更多机会获得正确的响应。


相关配置:请参考附录 Retry 配置

超时 — Timeout   

在微服务体系中,服务间相互依赖,例如:A—>B—>C—>D,可能由于某些网络原因,导致被依赖服务 D 无法按预期响应,这种缓慢会导致下游服务一直到服务 A,并且阻塞单个服务中的线程。由于这不是很常见的问题,在设计时需要设置超时来应对服务缓慢/不可用性问题。


  • 即使依赖服务不可用,也可以使核心服务始终工作

  • 避免无限期的等待

  • 避免阻塞任何线程

  • 使用一些缓存的响应来处理与网络相关的问题并使系统保持运行状态

 

相关配置:请参考附录 Timeout 配置

使用指南

引入依赖



<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId></dependency><dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId></dependency><dependency> <groupId>io.github.resilience4j</groupId> <artifactId>resilience4j-spring-boot2</artifactId> <version>1.6.1</version></dependency>
复制代码

重试 Retry  

配置-服务调用方


<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId></dependency><dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId></dependency><dependency> <groupId>io.github.resilience4j</groupId> <artifactId>resilience4j-spring-boot2</artifactId> <version>1.6.1</version></dependency>
复制代码


代码-服务提供方,模拟异常



@GetMapping("/rating_random_fail/{productId}")public ResponseEntity<ProductRatingDTO> getRatingRandomFail(@PathVariable Integer productId) { ProductRatingDTO productRatingDTO = ratingService.getRatingForProduct(productId); return failRandomly(productRatingDTO);}
/** * 模拟服务随机失败 * * @param productRatingDTO * @return */private ResponseEntity<ProductRatingDTO> failRandomly(ProductRatingDTO productRatingDTO){ int random = ThreadLocalRandom.current().nextInt(1, 4); log.info("[服务端模拟重试场景,数字] -> {}", random); if(random < 2){ return ResponseEntity.status(500).build(); }else if(random < 3){ return ResponseEntity.badRequest().build(); } return ResponseEntity.ok(productRatingDTO);}
复制代码


服务调用方-重试



private static int retryCount; // 记录重试次数,进行验证
/** * 服务端模拟随机失败,客户端实现重试 * * @param productId * @return */@Retry(name = "ratingRetryService", fallbackMethod = "getDefaultProductRating")public CompletionStage<ProductRatingDTO> getProductRatingDto(int productId){ retryCount++; log.info("[重试模拟 {}],开始调用 {}", retryCount, Instant.now()); Supplier<ProductRatingDTO> supplier = () -> this.restTemplate.getForEntity(this.ratingEndpoint + productId, ProductRatingDTO.class).getBody(); return CompletableFuture.supplyAsync(supplier);}
/** * 客户端失败回调方法 * * @param productId * @param throwable * @return */private CompletionStage<ProductRatingDTO> getDefaultProductRating(int productId, HttpClientErrorException throwable) { retryCount = 0; log.info("[重试模拟 {} ],进入回调方法.", retryCount); return CompletableFuture.supplyAsync(() -> ProductRatingDTO.of(0, Collections.emptyList()));}
复制代码

并发隔板 Bulkhead  

  • 配置-服务调用方



resilience4j: bulkhead: instances: ratingBulkheadService: max-concurrent-calls: 5 ## 隔板最大的信号量 max-wait-duration: 10ms
复制代码


  • 代码-服务提供方


/** * 服务提供者 — 模拟服务端处理缓慢 * * @param productId * @return * @throws InterruptedException */@GetMapping("/rating_slow_response/{productId}")public ResponseEntity<ProductRatingDTO> getRatingSlowResponse(@PathVariable Integer productId) throws InterruptedException { TimeUnit.SECONDS.sleep(10L); return ResponseEntity.ok(ratingService.getRatingForProduct(productId));}
复制代码


服务调用方


/** * 服务端模拟响应缓慢,客户端设置并发隔板 * * @param productId * @return */@Bulkhead(name = "ratingBulkheadService", type = Type.SEMAPHORE, fallbackMethod = "getDefault")public ProductRatingDTO getProductRatingDtoBulkhead(int productId) { log.info("[重试并发隔板 {}],调用开始。", Instant.now()); ProductRatingDTO productRatingDTO = this.restTemplate .getForEntity(this.productEndpoint + "/rating_slow_response/" + productId, ProductRatingDTO.class) .getBody(); log.info("[重试并发隔板 {}],调用结束。", Instant.now()); return productRatingDTO;}
/** * 客户端失败回调方法 * * @param productId * @param throwable * @return */private ProductRatingDTO getDefault(int productId, Throwable throwable) { log.info("==> 进入回调方法."); return ProductRatingDTO.of(0, Collections.emptyList());}
复制代码

流控 RateLimiter

  •  配置-服务提供方

resilience4j:  ratelimiter:    instances:      productRateLimiter:         limitForPeriod: 3   ## 每10秒内可用3个        limitRefreshPeriod: 10s        timeoutDuration: 0
复制代码
  • 代码-服务提供方


/** * 根据 productId 获取商品 — 模拟流量控制 * * @param productId * @return */@RateLimiter(name = "productRateLimiter", fallbackMethod = "getProductByIdFallback")public BaseResponse<ProductDTO> getProductByIdRateLimiter(int productId) { ProductPO po = this.map.get(productId); ProductDTO productDTO = ProductDTO.of(po.getProductId(), po.getDescription(), po.getPrice(), null); return BaseResponse.of(productDTO, ResponseType.SUCCESS, Strings.EMPTY);}
private BaseResponse<ProductDTO> getProductByIdFallback(int productId, Throwable throwable) { return BaseResponse.of(null, ResponseType.FAILURE, "当前用户较多,请稍后再试。");}
复制代码

超时 Timeout  

  • 配置-服务调用方


resilience4j: timelimiter: instances: ratingTimeoutService: timeout-duration: 3s ## 3秒超时 cancel-running-future: true ## 超时后取消正在执行的线程任务
复制代码


  • 代码-服务提供方


/** * 服务提供者 — 重试服务端网络抖动 * * @param productId * @return * @throws InterruptedException */@GetMapping("/rating_timeout/{productId}")public ResponseEntity<ProductRatingDTO> getRatingTimeout(@PathVariable Integer productId) throws InterruptedException { int second = ThreadLocalRandom.current().nextInt(1, 5); log.info("[服务端模拟超时场景,超时 {} 秒]", second); TimeUnit.SECONDS.sleep(second); return ResponseEntity.ok(ratingService.getRatingForProduct(productId));}
复制代码

服务调用方


/** * 服务端模拟随机失败,客户端实现超时机制 * * @param productId * @return */@TimeLimiter(name = "ratingTimeoutService", fallbackMethod = "getDefaultTimeout")public CompletionStage<ProductRatingDTO> getProductRatingDtoTimeout(int productId) { log.info("[超时模拟],开始调用 {}", Instant.now()); Supplier<ProductRatingDTO> supplier = () -> this.restTemplate .getForEntity(this.productEndpoint + "/rating_timeout/" + productId, ProductRatingDTO.class) .getBody(); return CompletableFuture.supplyAsync(supplier);}
/** * 客户端超时回调方法 * * @param productId * @param throwable * @return */private CompletionStage<ProductRatingDTO> getDefaultTimeout(int productId, Throwable throwable){ log.info("[超时模拟 {} ],进入回调方法."); return CompletableFuture.supplyAsync(() -> ProductRatingDTO.of(0, Collections.emptyList()));}
复制代码

熔断 CircuitBreak  

  • 配置-服务调用方


resilience4j: circuitbreaker: configs: default: sliding-window-type: count-based sliding-window-size: 100 permitted-number-of-calls-in-half-open-state: 10 ## 在半开状态时,允许调用的数量 wait-duration-in-open-state: 10ms ## 从打开状态转变为半开状态等待的时间 failure-rate-threshold: 60 ## 失败率阀值,百分比 record-exceptions: - org.springframework.web.client.HttpServerErrorException instances: ratingCircuitBreakService: base-config: default retry: instances: ratingCircuitBreakService: max-attempts: 2 ## 最多重试3次 wait-duration: 1s ## 每次重试调用前,等待2秒 retry-exceptions: - org.springframework.web.client.HttpServerErrorException ignore-exceptions: - org.springframework.web.client.HttpClientErrorException
复制代码


  • 代码-服务提供方


/** * 服务提供者 — 模拟熔断场景 * * @param productId * @return * @throws InterruptedException */@GetMapping("/rating_circuit_break/{productId}")public ResponseEntity<ProductRatingDTO> getRatingCircuitBreakResponse(@PathVariable Integer productId) throws InterruptedException { ProductRatingDTO productRatingDTO = ratingService.getRatingForProduct(productId); return circuitBreakFailRandomly(productRatingDTO);} /** * 模拟熔断场景 * * @param productRatingDto * @return * @throws InterruptedException */private ResponseEntity<ProductRatingDTO> circuitBreakFailRandomly(ProductRatingDTO productRatingDto) throws InterruptedException { // 模拟响应延迟 TimeUnit.MILLISECONDS.sleep(100L); // 模拟响应失败 int random = ThreadLocalRandom.current().nextInt(1, 4); if(random < 3) { return ResponseEntity.status(500).build(); } return ResponseEntity.ok(productRatingDto);}
复制代码


服务调用方


/** * 服务端模拟响应延迟、响应失败,客户端设置熔断机制 * * @param productId * @return */@Retry(name = "ratingCircuitBreakService", fallbackMethod = "getDefault")@CircuitBreaker(name = "ratingCircuitBreakService", fallbackMethod = "getDefault")public ProductRatingDTO getProductRatingDtoCircuitBreak(int productId) { log.info("[熔断 {}],调用开始。", Instant.now()); ProductRatingDTO productRatingDTO = this.restTemplate .getForEntity(this.productEndpoint + "/rating_circuit_break/" + productId, ProductRatingDTO.class) .getBody(); log.info("[熔断 {}],调用结束。", Instant.now()); return productRatingDTO;}
/** * 客户端失败回调方法 * * @param productId * @param throwable * @return */private ProductRatingDTO getDefault(int productId, Throwable throwable) { log.info("==> 进入回调方法."); return ProductRatingDTO.of(0, Collections.emptyList());}
复制代码

附录

CircuitBreaker 配置  



Bulkhead 配置  


RateLimiter 配置 

Retry 配置 


Timeout 配置

参考资料:

  • https://github.com/resilience4j/resilience4j

  • https://dzone.com/articles/resilient-microservices-pattern-bulkhead-pattern

  • https://www.vinsguru.com/rate-limiter-pattern

  • https://www.vinsguru.com/retry-pattern-microservice-design-patterns

  • https://www.vinsguru.com/timeout-pattern-microservice-design-patterns/

  • https://www.jianshu.com/p/5531b66b777a

  • https://resilience4j.readme.io/docs

  • https://www.vavr.io


本文转载自:金科优源汇(ID:jkyyh2020)

原文链接:Resilience4j 实用指南

2021-03-12 07:007130

评论

发布
暂无评论
发现更多内容

五分钟搞懂缓存

俞凡

架构

模块二作业

沐风

职场高效协作手册

刘华Kenneth

效率 职场 沟通

诡异的Redis数据库内存,空间很大,内存使用还是不理想

Java 程序员 后端

详解SpringBoot(2,java入门视频教学

Java 程序员 后端

10个人9个答错,另外1个只对一半:数据库的锁(1)

Java 程序员 后端

读Java虚拟机类加载引发的血案,阿里P8大牛手把手教你

Java 程序员 后端

架构实战营课程总结

子豪sirius

架构实战营

133道Java面试题及答案(面试必看),2021年Java面试心得

Java 程序员 后端

15道常考SpringBoot面试题整理,java资源百度云盘

Java 程序员 后端

说一下 jvm 有哪些垃圾回收器?(1),java异常面试题

Java 程序员 后端

区块链日报网元宇宙频道正式上线

区块链日报

架构实战训练营模块二

人生就是梦

架构实战营

“元宇宙”火出圈,不仅是区块链与科技圈,小说里也出现了

区块链日报

区块链 人工智能 facebook vr 元宇宙

100道 IT名企前端面试真题,【面试必备】

Java 程序员 后端

10个人9个答错,另外1个只对一半:数据库的锁

Java 程序员 后端

话不多说直接上才艺“来吧!展示,【高级Java架构师系统学习

Java 程序员 后端

Vue进阶(幺伍柒):Form 表单重置失败问题解决

No Silver Bullet

Vue 10月月更

架构实战营第二模块总结

小何

模块2-朋友圈的架构设计

Geek_cb2b43

SpringCloud+Boot2+Docker+ES打造45K+Star的微服务项目,附教程

Java 程序员 Spring Cloud

与 Python 之父聊天:更快的 Python!

Python猫

Python

说一下 jvm 有哪些垃圾回收器?,kalilinux渗透教程百度云

Java 程序员 后端

说到分布式,重要的Paxos算法你看透了么?,简述mybatis工作原理

Java 程序员 后端

1000页神仙文档,连阿里P8面试官都说太详细了,面面俱到!搞懂这些直接P6+

Java 程序员 后端

100道 IT名企前端面试真题(1),java面试中经常被问到的问题

Java 程序员 后端

14道MyBatis面试题总结,看完你还敢说懂MyBatis吗?

Java 程序员 后端

详解:Java的重写方法与示例,用22天总结了一份完美的SQL学习笔记

Java 程序员 后端

最新!阿里发布第四版Spring Cloud Alibaba全彩版手册

Java 架构 面试 微服务 阿里

微信朋友圈高性能复杂度分析

小何

架构实战营

15张图搞定MySQL InnoDB工作原理,kafka视频分析

Java 程序员 后端

Resilience4j 实用指南_语言 & 开发_金科优源汇_InfoQ精选文章