NVIDIA 初创加速计划,免费加速您的创业启动 了解详情
写点什么

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:005638

评论

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

倒计时 1 天!1000+ 技术先锋,龙蜥社区受邀参加 OCP China Day 2023

OpenAnolis小助手

龙蜥活动 OCP China Day 2023

基于 KubeSphere 快速部署 ByConity

朱亚光

云原生 数据舱

加速解锁科学智能前沿,昇思MindSpore铸就了一把“全能”钥匙

脑极体

AI

C语言驱动开发之内核解锁与强删文件

智趣匠

何时使用MongoDB而不是MySql

越长大越悲伤

MySQL 数据库 mongodb

万物皆可销售:数字世界中的商业文明之光

B Impact

网络虚拟世界不是法外之地,必须严打网络暴力

石头IT视角

直播预告 | 博睿学院:浅析windows Hook原理和机制

博睿数据

可观测性 智能运维 博睿数据 直播预告 博睿学院

深化产教融合,华为与高校共建鸿蒙人才培养

Geek_2d6073

SpringBoot3文件管理

Java 架构 springboot SpringBoot3

在2023年及以后可以改善企业业务的五种软件

高端章鱼哥

项目管理 软件开发 业务管理

AI技术赋能,引领行业变革 | 百度营销创新打造“信息流AI投放”

科技热闻

一键登录和短信验证登录,到底有什么区别?

MobTech袤博科技

前端 App 前端开发 APP开发

Spring 简介

小万哥

Java spring 后端 spring-cloud spring-boot

go-zero 是如何实现计数器限流的?

AlwaysBeta

Go 微服务 限流算法 go-zero 限流器

springboot+activiti+vue+mysql轻松搞定审批!(源码)

金陵老街

条条大路通罗马系列—— 使用 Hiredis-cluster 连接 Amazon ElastiCache for Redis 集群

亚马逊云科技 (Amazon Web Services)

Amazon

IOS上架流程

SOFABoot 4.0 正式发布,多项新特性等你来体验!

SOFAStack

springboot Java 分布式 jdk17 #开源

贡献超 10 万代码的新晋 committer,持续参与 Apache IoTDB 社区的“秘籍”是?

Apache IoTDB

“铁头乔”出品!CommunityOverCode Asia 专题介绍之物联网(IoT)/工业物联网(IIOT)

Apache IoTDB

大咖云集︱2023第二届中国PMO&PM大会倒计时15天!

新消费日报

微软为AI投了多少钱?以及是否要担心这笔疯狂的资本支出

B Impact

go-zero 是如何做路由管理的?

AlwaysBeta

Go 源码 微服务 go-zero 路由

山东布谷科技直播软件源码探索高效、稳定直播传输的技术介绍:流媒体传输技术

山东布谷科技

软件开发 直播 源码搭建 直播软件源码 流媒体技术

Programming abstractions in C阅读笔记:p88-p90

codists

从数据仓库到数据结构:数据架构的演变之路

这我可不懂

数据架构 数据化

MySQL教学,是不是干货你说的算

java易二三

MySQL 数据库 程序员 计算机 电脑

veridex执行错误分析与方案

XCG00

Android;

现代数据分析的革命,ABI报表工具引领企业进入数据驱动时代

流量猫猫头

Vulkan-性能及精细化

江湖修行

android OpenGL ES opengl 渲染 vulkan

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