侧边栏壁纸
  • 累计撰写 98 篇文章
  • 累计创建 20 个标签
  • 累计收到 3 条评论

Resilience4j 学习笔记

林贤钦
2021-08-16 / 0 评论 / 0 点赞 / 1,284 阅读 / 25,157 字
温馨提示:
本文最后更新于 2021-10-14,若内容或图片失效,请留言反馈。部分素材来自网络,若不小心影响到您的利益,请联系我们删除。

Resilience4j 学习笔记

Resilience4j简介

为什么需要熔断器?

微服务的本质是分布式的。当我们在使用分布式系统时,任何事情都可能发生,可能是网络问题,服务不可用,应用程序缓慢等。一个系统的问题,可能会影响另一个系统的行为/性能。处理任何此类意外故障/网络问题可能很难解决。


主流熔断器比较

`Sentinel`、`Hystrix`、`resilience4j`
SentinelHystrix(维护状态)Resilience4j(Spring推荐)
开发者alibabaNetflix独立
隔离策略信号量隔离(并发线程数限流)线程池隔离/信号量隔离信号量隔离
熔断降级策略基于响应时间、异常比率、异常数基于异常比率基于异常比率、响应时间
实时统计实现滑动窗口(LeapArray)滑动窗口(基于 RxJava)Ring Bit Buffer
动态规则配置支持多种数据源支持多种数据源有限支持
扩展性多个扩展点插件的形式接口的形式
基于注解的支持支持支持支持
限流基于 QPS,支持基于调用关系的限流有限的支持Rate Limiter
流量整形支持预热模式、匀速器模式、预热排队模式不支持简单的 Rate Limiter 模式
系统自适应保护支持不支持不支持
控制台提供开箱即用的控制台,可配置规则、查看秒级监控、机器发现等简单的监控查看不提供控制台,可对接其它监控系统
  • Sentinel:轻量级,核心库无多余依赖,性能损耗小,方便接入,开源生态广泛,丰富的流量控制场景,易用的控制台,提供实时监控,机器发现,规则管理等能力,比较完善的扩展性设计。

  • Resilience4j是比较轻量的库,在较小较新的项目中使用还是比较方便的,但是 Resilience4j 只包含限流降级的基本场景,对于非常复杂的企业级服务架构可能无法很好地 cover 住;同时 Resilience4j 缺乏生产级别的配套设施(如提供规则管理和实时监控能力的控制台)


Resilience4j是什么?

Resilience4j是Spring Cloud Greenwich版推荐的容错解决方案,相比 Hystrix , Resilience4j 专为 Java 8 以及函数式编程而设计。

相比之下,Netflix Hystrix 对 Archaius 有一个编译依赖项,它有更多的外部库依赖项,例如 Guava 和 Apache Commons Configuration。

Resilience4j 提供高阶函数(装饰器)以增强任何具有断路器、速率限制器、重试或隔板的函数接口、lambda 表达式或方法引用。您可以在任何函数式接口、lambda 表达式或方法引用上堆叠多个装饰器。优点是你可以选择你需要的装饰器而不是别的。


Resilience4j 能做什么?

Resilience4j 核心模块提供了多种功能,提供分布式环境下服务常见问题的限时限流断路重试等解决方案,其他系统不可用情况下,尽可能降低影响,达到系统基本可用,避免服务间雪崩效应。

Resilience4j 核心模块断路器、限流、基于信号量的隔离、缓存、限时、请求重试

name类型how does it work?description描述links
重试重写执行失败请求部分请求可能偶尔失败,重写请求可能恢复正常overview, documentation, Spring
断路器部分服务可能出现故障当服务不可用时,快速失败overview, documentation, Feign, Retrofit, Spring
限速限制周期内请求数限制接受请求的速率overview, documentation, Feign, Retrofit, Spring
限时限制执行期限超过一定等待时间将不能返回正确结果documentation, Retrofit, Spring
隔离限制并发执行失败资源放入隔离区overview, documentation, Spring
缓存缓存一个成功结果相似请求返回documentation
备路为失败请求提供另一个结果请求失败后的处理Try::recover, Spring, Feign

Resilience4j核心模块-介绍使用

各种核心模块功能的演示基于springboot,先配置好springboot环境及依赖。

maven配置

<dependency>
    <groupId>io.github.resilience4j</groupId>
    <artifactId>resilience4j-spring-boot2</artifactId>
    <version>1.6.1</version>
</dependency>

gradle配置

compile("io.github.resilience4j:resilience4j-spring-boot2:1.6")

1、限流模式

在微服务架构中,当有多个服务(A,B,C,D)时,服务A可能依赖于另一个服务B,而服务B又可能依赖于服务C,以此类推。当出现这种情况,服务A依赖服务B,而服务B必须为它接收到的请求做大量的CPU和IO密集型工作,服务B可能很长时间才能返回。

问题:如果服务A偶尔会接收到大量的请求,如果我们依赖服务B,对于每个请求,它可能会给服务B增加过多的负载,从而导致服务中断。

解决:作为一种保护措施,服务B系统通过拒绝它无法处理的调用来保护自己收到太多请求,服务 B 对其在给定时间窗口内可以处理的最大请求数有限制。

速率限制器模式仅通过限制我们可以在特定窗口中进行/处理的调用数量来帮助我们使我们的服务高度可用。换句话说,它帮助我们控制吞吐量。当我们收到太多请求时,服务可能只是拒绝调用。客户端必须稍后重试,或者可以使用一些默认/缓存值。

示例:提供两个接口,一个是正常的业务接口,不限速,另一个是cpu/io型接口,需要限速。

application.yml配置信息

  resilience4j.ratelimiter:
      configs:
          default:
              limitForPeriod: 10
              limitRefreshPeriod: 60s
              timeoutDuration: 1s
      instances:
          backendA:
              baseConfig: default
          backendB:
              limitForPeriod: 5
              limitRefreshPeriod: 60s
              timeoutDuration: 0

配置项说明

配置属性默认值描述
timeoutDuration5 [s]默认等待权限持续时间
limitRefreshPeriod500 [ns]限制刷新的时间段。在每个时间段之后,速率限制器将其权限计数重新设置为limitForPeriod
limitForPeriod50限制刷新期间段可用的权限数

BackendBController提供三个方法,分别三种情况,第一种不做限制,第二种做限制,但没有默认返回,第三种限制后,默认返回特定结果。

@RestController
@RequestMapping(value = "/backendB")
public class BackendBController {
    /**
       * 无限制调用
       */
    @GetMapping("/rateLimiter/crud")
    public AjaxResult rateLimiterCURD(){
        return AjaxResult.success("请求成功");
    }

    /**
       * 限制调用,无默认返回
       */
    @GetMapping("/rateLimiter/cpuORio")
    @RateLimiter(name = "backendB" )
    public AjaxResult rateLimiterCpuORio(){
        return AjaxResult.success("请求成功");
    }

    /**
       * 限制调用,默认返回
       */
    @GetMapping("/rateLimiter/cpuORioDefault")
    @RateLimiter(name = "backendB", fallbackMethod  = "getDefault")
    public AjaxResult rateLimiterCpuORioDefault(){
        return AjaxResult.success("请求成功");
    }

    public AjaxResult getDefault(Throwable throwable){
        return AjaxResult.error("被限制请求了!");
    }
  }

演示:模拟请求api的各种情况

情况一/backendB/rateLimiter/crud

无论请求多少次,只会返回一种结果

{
  "msg": "请求成功",
  "code": 200
}

情况二/backendB/rateLimiter/cpuORio

根据配置,一分钟只支持5个请求,前五次输出和前面正常一致

限制后返回结果:

  {
      "timestamp": "2021-08-16T09:22:35.618+00:00",
      "path": "/backendB/rateLimiter/cpuORio",
      "status": 500,
      "error": "Internal Server Error",
      "requestId": "e25d6a6c-90"
  }

情况三/backendB/rateLimiter/cpuORioDefault

和情况二不同的是,如果达到阈值,就会返回默认的结果

默认返回结果:

{
  "msg": "被限制请求了!",
  "code": 500
}

速率限制器模式在控制方法调用的吞吐量方面非常有用。这样可以正确利用服务器资源并且防止服务器受到任何恶意攻击也很有用。

2、超时模式

有时候我们会遇到间歇性的应用程序缓慢没有明显原因。比如当多个服务(A,B,C,D)时,一个服务(A)可能依赖于另一个服务(B),而另一个服务(B)又可能依赖于 C,依此类推。有时由于某些网络问题,服务 D 可能无法按预期响应。这种缓慢可能会影响下游服务——一直到服务 A 并阻塞各个服务中的线程。

在设计的时候,希望通过为任何网络调用设置超时来考虑此服务缓慢/不可用的问题,即使依赖不可用,我们也可以让核心服务按预期工作作出响应。

优点:核心服务正常工作,不会无限期等待,不阻塞任何线程,使用缓冲响应处理相关问题,使系统保持功能。


示例

加设服务A依赖于服务B,服务A是核心服务,服务B拥有大量数据,比较缓慢。

yaml 配置

将超时设置为 3 秒。我们可以添加多个具有特定超时的服务。

# 超时配置
resilience4j.timelimiter:
  configs:
    default:
      timeoutDuration: 3s
      cancelRunningFuture: true
  instances:
    backendA:
      baseConfig: default
    backendB:
      baseConfig: default

服务B接口:/timelimiter/backendB/

初始化一个map,模拟数据源,请求该接口的时候随机休眠1-10秒,模拟线上缓慢问题

@RestController
@RequestMapping(value = "/timelimiter/backendB")
public class TimerBackendBController {

    private final Map<String, DemoBDto> map = new HashMap<>();

    @PostConstruct
    private void init() {
        map.put("1", DemoBDto.of("1", "demoB id=1描述"));
        map.put("2", DemoBDto.of("2", "demoB id=2描述"));
        map.put("3", DemoBDto.of("3", "demoB id=3描述"));
        map.put("4", DemoBDto.of("4", "demoB id=4描述"));
        map.put("5", DemoBDto.of("5", "demoB id=5描述"));
        map.put("6", DemoBDto.of("6", "demoB id=6描述"));
    }

    @GetMapping("{aId}")
    public DemoBDto getDemoBDto(@PathVariable String aId) throws InterruptedException {
        Thread.sleep(ThreadLocalRandom.current().nextInt(10, 10000));
        DemoBDto orDefault = this.map.getOrDefault(aId, new DemoBDto());
        return orDefault;
    }

}

服务A有个service,通过RestTemplate调用服务B的接口。

  • ***@TimeLimiter***表示resilience4j 将为此方法执行应用超时。
  • name=backendB表示resilience4j将使用yaml 中 backendB的配置。
  • ***fallbackMethod***在 main 方法由于某种原因失败时使用。
@Service
public class TimerBackendBServiceClient {
    private final RestTemplate restTemplate = new RestTemplate();

    private final String ratingService = "http://localhost:8092/timelimiter/backendB";
    @TimeLimiter(name = "backendB", fallbackMethod = "getDefault")
    public CompletionStage<DemoBDto> getDemoBDto(String aId){
        Supplier<DemoBDto> supplier = () -> this.restTemplate.getForEntity(this.ratingService + "/"+aId, DemoBDto.class).getBody();
        return CompletableFuture.supplyAsync(supplier);
    }

    private CompletionStage<DemoBDto> getDefault(String aId, Throwable throwable){
        return CompletableFuture.supplyAsync(()->DemoBDto.of("0", "无内容"));
    }
}

注意TimeLimiterAspect 仅支持反应式类型(rxjava 或 reactor)或CompletionStage.

服务A接口,查询自己服务的信息外,还需要调用TimerBackendBServiceClient通过RestTemplate调用服务B的接口,获取服务B的值。

@RestController
@RequestMapping(value = "/timelimiter/backendA")
public class TimerBackendAController {
    private final Map<String, String> map = new HashMap<>();
    private final TimerBackendBServiceClient timerBackendBServiceClient;
    public TimerBackendAController(TimerBackendBServiceClient timerBackendBServiceClient){
        this.timerBackendBServiceClient=timerBackendBServiceClient;
    }
    @PostConstruct
    private void init() {
        map.put("1", "demoA1描述");
        map.put("2", "demoA2描述");
        map.put("3", "demoA3描述");
        map.put("4", "demoA4描述");
        map.put("5", "demoA5描述");
        map.put("6", "demoA6描述");
    }

    @GetMapping("{aId}")
    public DemoADto getDemoADto(@PathVariable String aId) throws ExecutionException, InterruptedException {
        CompletionStage<DemoADto> demoADtoCompletionStage = this.timerBackendBServiceClient.getDemoBDto(aId).thenApply(demoBDto -> DemoADto.of(aId, map.get(aId), demoBDto));
        return demoADtoCompletionStage.toCompletableFuture().get();
    }
}

当我们调用服务A的接口时,服务B可能正常3秒内返回,可能返回比较久,可能服务挂了

正常情况下:

{
    "desc": "demoA1描述",
    "demoBDto": {
        "desc": "demoB id=1描述",
        "bid": "1"
    },
    "aid": "1"
}

非正常情况返回默认方法:

{
    "desc": "demoA1描述",
    "demoBDto": {
        "desc": "无内容",
        "bid": "0"
    },
    "aid": "1"
}

优点

  • 在这种方法中,我们不会无限期地阻塞产品服务中的任何线程
  • 网络调用期间的任何意外事件将在 3 秒内超时。
  • 核心服务不会因为依赖服务的性能不佳而受到影响。

缺点:

  • 我们仍然阻塞线程 3 秒。对于接收许多并发请求的应用程序来说,这仍然是一个问题。

3、重试模式

有时当***google.com***对我们不起作用时,我们只是不放弃。我们只是在假设下次可以正常工作时刷新页面,并且大多数时候它都可以。间歇性网络问题非常普遍。

在微服务世界中,我们可能会运行同一服务 D 的多个实例以实现高可用性和负载平衡。如果其中一个实例可能出现问题并且它没有正确响应我们的请求,如果我们重试请求,负载均衡器可以将请求发送到健康节点并正确获得响应。

因此,使用 Retry 选项,我们有更多机会获得正确的响应。

演示

配置信息

  • 我们可以有多个服务配置,如下所示。
  • 对于***backendB***,我们将最多进行 3 次重试,延迟 5 秒。
  • retryExceptions:这些是我们将重试的异常。它是一个数组字段。您可以配置多个例外。
  • ignoreExceptions:有些异常我们可能不想重试。例如,一个错误的请求就是一个错误的请求。重试是没有意义的。所以我们忽略它。
resilience4j.retry:
  instances:
    backendB:
      maxRetryAttempts: 3
      waitDuration: 5s
      retryExceptions:
        - org.springframework.web.client.HttpServerErrorException
      ignoreExceptions:
        - org.springframework.web.client.HttpClientErrorException
    backendA:
      maxRetryAttempts: 3
      waitDuration: 10s
      retryExceptions:
        - org.springframework.web.client.HttpServerErrorException
        - java.io.IOException

服务A是核心服务,服务B有多个节点,但有的节点可能出问题了,宕机等情况。

服务B模拟多节点,随机访问,有的节点正常,有的节点访问错误。

  • 在某些情况下它可以产生有效的响应
  • 在某些情况下,它会响应内部服务器错误
  • 对于错误的请求,它还会引发 400 错误。
@Slf4j
@RestController
@RequestMapping(value = "/retry/backendB")
public class RetryBackendBController {

    private final Map<String, DemoBDto> map = new HashMap<>();

    @PostConstruct
    private void init() {
        map.put("1", DemoBDto.of("1", "demoB id=1描述"));
        map.put("2", DemoBDto.of("2", "demoB id=2描述"));
        map.put("3", DemoBDto.of("3", "demoB id=3描述"));
        map.put("4", DemoBDto.of("4", "demoB id=4描述"));
        map.put("5", DemoBDto.of("5", "demoB id=5描述"));
        map.put("6", DemoBDto.of("6", "demoB id=6描述"));
    }

    @GetMapping("{aId}")
    public ResponseEntity<DemoBDto> getDemoBDto(@PathVariable String aId) throws InterruptedException {
        DemoBDto orDefault = this.map.getOrDefault(aId, new DemoBDto());
        return this.failRandomly(orDefault);
    }

    private ResponseEntity<DemoBDto> failRandomly(DemoBDto demoBDto){
        int random = ThreadLocalRandom.current().nextInt(1, 4);
        if(random < 2){
            log.info("返回错误码500.....");
            return ResponseEntity.status(500).build();
        }else if(random < 3){
            log.info("返回错误请求.....");
            return ResponseEntity.badRequest().build();
        }
        log.info("请求成功.....");
        return ResponseEntity.ok(demoBDto);
    }
}

服务A接口,查询自己服务的信息外,还需要调用RetryBackendBServiceClient通过RestTemplate调用服务B的接口,获取服务B的值。

  • ***@Retry***表示resilience4j 将为此方法执行应用重试逻辑。
  • name=backendB 表示 resilience4j 将使用yaml 中***backendB***的配置。
  • 当 main 方法由于某种原因失败时使用***fallbackMethod***。
@Service
public class RetryBackendBServiceClient {
    private final RestTemplate restTemplate = new RestTemplate();

    private final String serviceBApi = "http://localhost:8092/retry/backendB";

    @Retry(name = "backendB", fallbackMethod = "getDefault")
    public CompletionStage<DemoBDto> getDemoBDto(String aId){
        Supplier<DemoBDto> supplier = () -> this.restTemplate.getForEntity(this.serviceBApi + "/"+aId, DemoBDto.class).getBody();
        return CompletableFuture.supplyAsync(supplier);
    }

    private CompletionStage<DemoBDto> getDefault(String aId, Throwable throwable){
        return CompletableFuture.supplyAsync(()->DemoBDto.of("0", "无内容"));
    }
}

服务A接口

@RestController
@RequestMapping(value = "/retry/backendA")
public class RetryBackendAController {
    private Map<String, String> map = new HashMap<>();
    @Autowired
    private RetryBackendBServiceClient retryBackendBServiceClient;
    @PostConstruct
    private void init() {
        map.put("1", "demoA1描述");
        map.put("2", "demoA2描述");
        map.put("3", "demoA3描述");
        map.put("4", "demoA4描述");
        map.put("5", "demoA5描述");
    }
    @GetMapping("{aId}")
    public DemoADto getDemoADto(@PathVariable String aId) throws ExecutionException, InterruptedException {
        CompletionStage<DemoADto> demoADtoCompletionStage = this.retryBackendBServiceClient.getDemoBDto(aId).thenApply(demoBDto -> DemoADto.of(aId, map.get(aId), demoBDto));
        return demoADtoCompletionStage.toCompletableFuture().get();
    }
}

尝试请求接口:http://localhost:8092/retry/backendA/1

日志显示:

2021-08-18 00:56:41.199 [http-nio-8092-exec-1]  [INFO ] com.linxianqin.resilience4j.controller.retry.RetryBackendBController.failRandomly(RetryBackendBController.java:49) - 返回错误码500.....
2021-08-18 00:56:46.217 [http-nio-8092-exec-5]  [INFO ] com.linxianqin.resilience4j.controller.retry.RetryBackendBController.failRandomly(RetryBackendBController.java:55) - 请求成功.....

返回结果

{
    "desc": "demoA1描述",
    "demoBDto": {
        "desc": "demoB id=1描述",
        "bid": "1"
    },
    "aid": "1"
}

优点

  • 即使依赖服务不可用,也要使核心服务始终工作
  • 上游故障不会传播到下游
  • 避免间歇性网络问题

缺点

  • 重试会增加整体响应时间
  • 如果是应用程序问题,重试会在服务器上增加不必要的负载

重试模式 是用于设计弹性微服务的最简单的微服务 ***设计模式***之一。引入重试解决了网络相关问题。

4、断路器模式

当有多个服务(A、B、C 和 D)时,一个服务(A)可能依赖于另一个服务(B),而另一个服务(B)又可能依赖于 C,依此类推。有时由于某些问题,服务 D 可能无法按预期响应。服务 D 可能抛出了一些异常,如 OutOfMemory错误内部服务器错误。 此类异常会级联到下游服务,可能会导致用户体验不佳。

我们有重试模式,我们可以重试几次,直到得到正确的响应。这种重试方法的问题是——如果它是一个间歇性的网络问题,那么重试是有意义的!如果是应用程序问题,重试不能解决问题,反而会影响上下游的服务,导致下游服务预期响应时间变长,上游服务没有得到喘息的时间恢复。

这就是断路器模式可以帮助我们的地方!当对服务 B 的请求不断失败时,断路器只是跳过对服务 C 的调用,并在可配置的特定持续时间内使用回退方法/默认值。

也就是说,它不会连续轰炸服务 C。它为服务 C 提供了从故障中恢复的时间。断路器模式在一段时间后重试,依此类推。

断路器有三种状态

断路器会根据状态不同,做出不同的行为操作。

  • 关闭(CLOSED):相关服务(服务 C)已启动。允许对服务 C 的请求。
  • 打开(OPEN):依赖服务不可用/错误率超过阈值。 跳过对服务 C 的请求
  • 半开(HALF_OPEN):一旦状态变为 OPEN,我们就会在 OPEN 状态中等待一段时间。经过一定时间后,状态变为 HALF_OPEN。

在半开的状态下,我们会向服务 C 发送了一些请求,以检查我们是否仍然得到正确的响应。 如果故障率低于阈值,则状态将变为 CLOSED。 如果故障率高于阈值,则状态再次变为 OPEN,一直循环。

示例

现在有服务A和服务B,服务A有个接口的返回值,需要服务B提供,服务B不稳定,在某些情况下,它会响应内部服务器错误。

服务B接口

服务B是随机失败的,可能返回有效的响应,也可能响应内部服务器错误。

@Slf4j
@RestController
@RequestMapping(value = "/circuitbreaker/backendC")
public class CircuitbreakerBackendCController {

    private final Map<String, DemoBDto> map = new HashMap<>();

    @PostConstruct
    private void init() {
        map.put("1", DemoBDto.of("1", "demoB id=1,测试断路器-backendC"));
        map.put("2", DemoBDto.of("2", "demoB id=2,测试断路器-backendC"));
        map.put("3", DemoBDto.of("3", "demoB id=3,测试断路器-backendC"));
        map.put("4", DemoBDto.of("4", "demoB id=4,测试断路器-backendC"));
    }

    @GetMapping("{aId}")
    public ResponseEntity<DemoBDto> getDemoBDto(@PathVariable String aId) throws InterruptedException {
        DemoBDto orDefault = this.map.getOrDefault(aId, new DemoBDto());
        return this.failRandomly(orDefault);
    }

    private ResponseEntity<DemoBDto> failRandomly(DemoBDto demoBDto){
        try{
            Thread.sleep(100);
        }catch (Exception e){
            e.printStackTrace();
        }
        int random = ThreadLocalRandom.current().nextInt(1, 4);
         if(random < 3){
            log.info("断路器模式|返回错误请求.");
            return ResponseEntity.badRequest().build();
        }
        log.info("断路器模式|请求成功.");
        return ResponseEntity.ok(demoBDto);
    }
}

服务A配置和调用服务B接口方法

  • ***COUNT_BASED***滑动窗口来跟踪给定数量的请求

  • ***failureRateThreshold:***失败阈值设置为 60,100个请求失败次数小于60,则将断路器保持在***CLOSED***状态。

  • waitDurationInOpenState:当断路器处于 OPEN 状态时,等待 10 秒。变成半开状态,测试失败阈值。

# 熔断器配置
resilience4j.circuitbreaker:
  configs:
    default:
      slidingWindowType: COUNT_BASED #滑动窗口来跟踪给定数量的请求
      slidingWindowSize: 100 #滑动窗口的值
      permittedNumberOfCallsInHalfOpenState: 10 #半开测试次数
      waitDurationInOpenState: 10 #开启时等待时间,到时间转化为半开
      failureRateThreshold: 60 #失败次数
      recordExceptions: 
        - org.springframework.web.client.HttpServerErrorException
  instances:
    backendC:
      baseConfig: default

调用服务B的方法

@CircuitBreaker(name = "backendC", fallbackMethod = "getDefault")
public CompletionStage<DemoBDto> getDemoBDto(String aId){
    Supplier<DemoBDto> supplier = () -> this.restTemplate.getForEntity(this.serviceBApi + "/"+aId, DemoBDto.class).getBody();
    return CompletableFuture.supplyAsync(supplier);
}

private CompletionStage<DemoBDto> getDefault(String aId, Throwable throwable){
    log.info("断路器|使用默认方法返回.");
    return CompletableFuture.supplyAsync(()->DemoBDto.of("0", "无内容"));
}

服务A的接口

@GetMapping("{aId}")
public DemoADto getDemoADto(@PathVariable String aId) throws ExecutionException, InterruptedException {
    CompletionStage<DemoADto> demoADtoCompletionStage = this.circuitbreakerBackendBServiceClient.getDemoBDto(aId).thenApply(demoBDto -> DemoADto.of(aId, map.get(aId), demoBDto));
    return demoADtoCompletionStage.toCompletableFuture().get();
}

演示:通过调用服务A的接口从而调用B的接口。

  • 当服务B返回正常情况。

    {
        "desc": "demoA id=1,测试断路器-backendB",
        "demoBDto": {
            "desc": "demoB id=1,测试断路器-backendC",
            "bid": "1"
        },
        "aid": "1"
    }
    
  • 当服务B返回系统内部的时候,服务A使用默认返回值返回。

    {
        "desc": "demoA id=1,测试断路器-backendB",
        "demoBDto": {
            "desc": "无内容",
            "bid": "0"
        },
        "aid": "1"
    }
    

5、舱壁模式

使用舱壁模式,将一艘船分成多个小的隔间。舱壁用于密封船舶的各个部分,以防止在洪水泛滥时整艘船沉没。

在我们设计软件时也应该预料到类似的失败。应用程序应拆分为多个组件,并且资源应以一种组件的故障不会影响另一个组件的方式进行隔离。

例如:假设有两个服务A和B,服务A的资源非常有限(比如4个线程)。它的可用线程只能处理4个并发请求,服务A有两组API:

  • 接口1:需要依赖服务B的慢接口
  • 接口2:不依赖服务B

如果服务A有多个并发请求时,大量的线程资源请求到接口1上,由于被服务B的慢接口影响了,导致即使剩余请求是针对没有任何服务依赖关系的接口2,服务A也没有空闲线程来处理这些请求。

此行为会影响应用程序的整体性能,并可能导致较差的用户体验。服务 B 的缓慢也会间接影响服务 A 的性能。

舱壁模式帮助我们分配限制可用于特定服务的资源。这样可以减少资源枯竭。比如在上面的情况下,可以限制服务A的接口1去请求服务B的线程,避免这种情况出现,从而提高系统的吞吐量。

代码演示:

1、服务B提供一个慢接口查询

http://localhost:8093/bulkhead/backendC/

@GetMapping("{aId}")
public ResponseEntity<DemoBDto> getDemoBDto(@PathVariable String aId) throws InterruptedException {
    DemoBDto orDefault = this.map.getOrDefault(aId, new DemoBDto());
    try{
        Thread.sleep(3000);
    }catch (Exception e){
        e.printStackTrace();
    }
    log.info("服务B接口|舱壁模式|请求成功.第[{}]",count.incrementAndGet());
    return ResponseEntity.ok(orDefault);
}

2、配置服务A舱壁模式

#舱壁模式配置
resilience4j.bulkhead:
  instances:
    backendC:
      maxConcurrentCalls: 4 # 允许评级服务的最大并发调用数
      maxWaitDuration: 10ms # 任何额外的请求将等待给定的持续时间。否则它将采用默认/回退方法。

3、配置限制tomcat的线程数,观察效果

server:
  port: 8092
  tomcat:
    threads:
      max: 8

4、调用服务B接口BulkheadBackendBServiceClient

private final String serviceBApi = "http://localhost:8093/bulkhead/backendC";

@Bulkhead(name = "backendC", fallbackMethod = "getDefault")
public CompletionStage<DemoBDto> getDemoBDto(String aId){
    log.info("服务A接口|舱壁模式|通过RestTemplate调用API.");
    Supplier<DemoBDto> supplier = () -> this.restTemplate.getForEntity(this.serviceBApi + "/"+aId, DemoBDto.class).getBody();
    return CompletableFuture.supplyAsync(supplier);
}

private CompletionStage<DemoBDto> getDefault(String aId, Throwable throwable){
    log.info("服务A接口|舱壁模式|使用默认方法返回.");
    return CompletableFuture.supplyAsync(()->DemoBDto.of("0", "无内容"));
}

5、服务A提供两个接口,依赖于服务B/不依赖服务B

@GetMapping("{aId}")
public DemoADto getDemoADto(@PathVariable String aId) throws ExecutionException, InterruptedException {
    log.info("服务A接口|舱壁模式|请求慢接口");
    CompletionStage<DemoADto> demoADtoCompletionStage = this.bulkheadBackendBServiceClient.getDemoBDto(aId).thenApply(demoBDto -> DemoADto.of(aId, map.get(aId), demoBDto));
    return demoADtoCompletionStage.toCompletableFuture().get();
}


@GetMapping("getOnlyDemoA/{aId}")
public DemoADto getDemoADtoBYCircuitBreakerAndRetry(@PathVariable String aId) throws InterruptedException {
    log.info("服务A接口|舱壁模式|请求默认接口");
    Thread.sleep(50);
    return DemoADto.of(aId, map.get(aId), null);
}

为了更好的展示舱壁模式的效果,我用Jmeter在开启舱壁模式和没开启的情况下分别100个线程请求服务A的两个接口。

未开启-服务A-1接口,jmeter情况:平均耗时:23543毫秒。吞吐量2.2

未开启-服务A-2接口,jmeter情况:平均耗时:21796毫秒。吞吐量2.3

已开启-服务A-1接口,jmeter情况:平均耗时:2083毫秒。吞吐量19.2

已开启-服务A-2接口,jmeter情况:平均耗时:2025毫秒。吞吐量28.3

从测试结果可以看出,使用舱壁模式能降低接口的平时耗时,提高接口的吞吐量。

0

评论区