乐趣区

关于java:Java-项目中使用-Resilience4j-实现客户端-API-调用的限速节流机制


在本系列的上一篇文章中,咱们理解了 Resilience4j 以及如何应用其 [Retry 模块](
https://icodewalker.com/blog/…)。当初让咱们理解 RateLimiter – 它是什么,何时以及如何应用它,以及在施行速率限度(或者也称为“节流”)时要留神什么。

代码示例

本文附有 GitHub 上的工作代码示例。

什么是 Resilience4j?

请参阅上一篇文章中的形容,疾速理解 [Resilience4j 的个别工作原理](
https://icodewalker.com/blog/…)。

什么是限速?

咱们能够从两个角度来对待速率限度——作为服务提供者和作为服务消费者。

服务端限速

作为服务提供商,咱们施行速率限度以爱护咱们的资源免受过载和拒绝服务 (DoS) 攻打

为了满足咱们与所有消费者的服务水平协定 (SLA),咱们心愿确保一个导致流量激增的消费者不会影响咱们对别人的服务质量。

咱们通过设置在给定工夫单位内容许消费者收回多少申请的限度来做到这一点。咱们通过适当的响应回绝任何超出限度的申请,例如 HTTP 状态 429(申请过多)。这称为服务器端速率限度。

速率限度以每秒申请数 (rps)、每分钟申请数 (rpm) 或相似模式指定。某些服务在不同的持续时间(例如 50 rpm 且不超过 2500 rph)和一天中的不同工夫(例如,白天 100 rps 和早晨 150 rps)有多个速率限度。该限度可能实用于单个用户(由用户 ID、IP 地址、API 拜访密钥等标识)或多租户应用程序中的租户。

客户端限速

作为服务的消费者,咱们心愿确保咱们不会使服务提供者过载。此外,咱们不想导致意外的老本——无论是金钱上的还是服务质量方面的。

如果咱们生产的服务是有弹性的,就会产生这种状况。服务提供商可能不会限度咱们的申请,而是会因额定负载而向咱们收取额定费用。有些甚至在短时间内禁止行为不端的客户。消费者为避免此类问题而施行的速率限度称为客户端速率限度。

何时应用 RateLimiter?

resilience4j-ratelimiter 用于客户端速率限度。

服务器端速率限度须要诸如缓存和多个服务器实例之间的协调之类的货色,这是 resilience4j 不反对的。对于服务器端的速率限度,有 API 网关和 API 过滤器,例如 Kong API Gateway 和 Repose API Filter。Resilience4j 的 RateLimiter 模块并不打算取代它们。

Resilience4j RateLimiter 概念

想要调用近程服务的线程首先向 RateLimiter 申请许可。如果 RateLimiter 容许,则线程持续。否则,RateLimiter 会停放线程或将其置于期待状态。

RateLimiter 定期创立新权限。当权限可用时,线程会收到告诉,而后能够持续。

一段时间内容许的调用次数称为 limitForPeriod。RateLimiter 刷新权限的频率由 limitRefreshPeriod 指定。timeoutDuration 指定线程能够期待多长时间获取权限。如果在等待时间完结时没有可用的权限,RateLimiter 将抛出 RequestNotPermitted 运行时异样。

应用 Resilience4j RateLimiter 模块

RateLimiterRegistryRateLimiterConfigRateLimiter 是 resilience4j-ratelimiter 的次要形象。

RateLimiterRegistry 是一个用于创立和治理 RateLimiter 对象的工厂。

RateLimiterConfig 封装了 limitForPeriodlimitRefreshPeriodtimeoutDuration 配置。每个 RateLimiter 对象都与一个 RateLimiterConfig 相关联。

RateLimiter 提供辅助办法来为蕴含近程调用的函数式接口或 lambda 表达式创立装璜器。

让咱们看看如何应用 RateLimiter 模块中可用的各种性能。假如咱们正在为一家航空公司建设一个网站,以容许其客户搜寻和预订航班。咱们的服务与 FlightSearchService 类封装的近程服务对话。

根本示例

第一步是创立一个 RateLimiterConfig

RateLimiterConfig config = RateLimiterConfig.ofDefaults();

这将创立一个 RateLimiterConfig,其默认值为 limitForPeriod (50)、limitRefreshPeriod(500ns) 和 timeoutDuration (5s)。

假如咱们与航空公司服务的合同规定咱们能够以 1 rps 调用他们的搜寻 API。而后咱们将像这样创立 RateLimiterConfig

RateLimiterConfig config = RateLimiterConfig.custom()
  .limitForPeriod(1)
  .limitRefreshPeriod(Duration.ofSeconds(1))
  .timeoutDuration(Duration.ofSeconds(1))
  .build();

如果线程无奈在指定的 1 秒 timeoutDuration 内获取权限,则会出错。

而后咱们创立一个 RateLimiter 并装璜 searchFlights() 调用:

RateLimiterRegistry registry = RateLimiterRegistry.of(config);
RateLimiter limiter = registry.rateLimiter("flightSearchService");
// FlightSearchService and SearchRequest creation omitted
Supplier<List<Flight>> flightsSupplier =
  RateLimiter.decorateSupplier(limiter,
    () -> service.searchFlights(request));

最初,咱们屡次应用装璜过的 Supplier<List<Flight>>

for (int i=0; i<3; i++) {System.out.println(flightsSupplier.get());
}

示例输入中的工夫戳显示每秒收回一个申请:

Searching for flights; current time = 15:29:40 786
...
[Flight{flightNumber='XY 765', ...}, ... ]
Searching for flights; current time = 15:29:41 791
...
[Flight{flightNumber='XY 765', ...}, ... ]

如果超出限度,咱们会收到 RequestNotPermitted 异样:

Exception in thread "main" io.github.resilience4j.ratelimiter.RequestNotPermitted: RateLimiter 'flightSearchService' does not permit further calls at io.github.resilience4j.ratelimiter.RequestNotPermitted.createRequestNotPermitted(RequestNotPermitted.java:43)

 at io.github.resilience4j.ratelimiter.RateLimiter.waitForPermission(RateLimiter.java:580)

... other lines omitted ...

装璜办法抛出已检异样

假如咱们正在调用
FlightSearchService.searchFlightsThrowingException(),它能够抛出一个已检 Exception。那么咱们就不能应用
RateLimiter.decorateSupplier()。咱们将应用
RateLimiter.decorateCheckedSupplier() 代替:

CheckedFunction0<List<Flight>> flights =
  RateLimiter.decorateCheckedSupplier(limiter,
    () -> service.searchFlightsThrowingException(request));


try {System.out.println(flights.apply());
} catch (...) {// exception handling}

RateLimiter.decorateCheckedSupplier() 返回一个 CheckedFunction0,它示意一个没有参数的函数。请留神对 CheckedFunction0 对象的 apply() 调用以调用近程操作。

如果咱们不想应用 SuppliersRateLimiter 提供了更多的辅助装璜器办法,如 decorateFunction()decorateCheckedFunction()decorateRunnable()decorateCallable() 等,以与其余语言构造一起应用。decorateChecked* 办法用于装璜抛出已查看异样的办法。

利用多个速率限度

假如航空公司的航班搜寻有多个速率限度:2 rps 和 40 rpm。咱们能够通过创立多个 RateLimiters 在客户端利用多个限度:

RateLimiterConfig rpsConfig = RateLimiterConfig.custom().
  limitForPeriod(2).
  limitRefreshPeriod(Duration.ofSeconds(1)).
  timeoutDuration(Duration.ofMillis(2000)).build();


RateLimiterConfig rpmConfig = RateLimiterConfig.custom().
  limitForPeriod(40).
  limitRefreshPeriod(Duration.ofMinutes(1)).
  timeoutDuration(Duration.ofMillis(2000)).build();


RateLimiterRegistry registry = RateLimiterRegistry.of(rpsConfig);
RateLimiter rpsLimiter =
  registry.rateLimiter("flightSearchService_rps", rpsConfig);
RateLimiter rpmLimiter =
  registry.rateLimiter("flightSearchService_rpm", rpmConfig);  
而后咱们应用两个 RateLimiters 装璜 searchFlights() 办法:Supplier<List<Flight>> rpsLimitedSupplier =
  RateLimiter.decorateSupplier(rpsLimiter,
    () -> service.searchFlights(request));


Supplier<List<Flight>> flightsSupplier
  = RateLimiter.decorateSupplier(rpmLimiter, rpsLimitedSupplier);

示例输入显示每秒收回 2 个申请,并且限度为 40 个申请:

Searching for flights; current time = 15:13:21 246
...
Searching for flights; current time = 15:13:21 249
...
Searching for flights; current time = 15:13:22 212
...
Searching for flights; current time = 15:13:40 215
...
Exception in thread "main" io.github.resilience4j.ratelimiter.RequestNotPermitted:
RateLimiter 'flightSearchService_rpm' does not permit further calls
at io.github.resilience4j.ratelimiter.RequestNotPermitted.createRequestNotPermitted(RequestNotPermitted.java:43)
at io.github.resilience4j.ratelimiter.RateLimiter.waitForPermission(RateLimiter.java:580)

在运行时更改限度

如果须要,咱们能够在运行时更改 limitForPeriodtimeoutDuration 的值:

limiter.changeLimitForPeriod(2);
limiter.changeTimeoutDuration(Duration.ofSeconds(2));

例如,如果咱们的速率限度依据一天中的工夫而变动,则此性能很有用 – 咱们能够有一个打算线程来更改这些值。新值不会影响以后正在期待权限的线程。

RateLimiter 和 Retry 一起应用

假如咱们想在收到 RequestNotPermitted 异样时重试,因为它是一个暂时性谬误。咱们会像平常一样创立 RateLimiterRetry 对象。而后咱们装璜一个 Supplier 的供应商并用 Retry 包装它:

Supplier<List<Flight>> rateLimitedFlightsSupplier =
  RateLimiter.decorateSupplier(rateLimiter,
    () -> service.searchFlights(request));


Supplier<List<Flight>> retryingFlightsSupplier =
  Retry.decorateSupplier(retry, rateLimitedFlightsSupplier);

示例输入显示为 RequestNotPermitted 异样重试申请:

Searching for flights; current time = 15:29:39 847
Flight search successful
[Flight{flightNumber='XY 765', ...}, ... ]
Searching for flights; current time = 17:10:09 218
...
[Flight{flightNumber='XY 765', flightDate='07/31/2020', from='NYC', to='LAX'}, ...]
2020-07-27T17:10:09.484: Retry 'rateLimitedFlightSearch', waiting PT1S until attempt '1'. Last attempt failed with exception 'io.github.resilience4j.ratelimiter.RequestNotPermitted: RateLimiter'flightSearchService'does not permit further calls'.
Searching for flights; current time = 17:10:10 492
...
2020-07-27T17:10:10.494: Retry 'rateLimitedFlightSearch' recorded a successful retry attempt...
[Flight{flightNumber='XY 765', flightDate='07/31/2020', from='NYC', to='LAX'}, ...]

咱们创立装璜器的程序很重要。如果咱们将 RetryRateLimiter 包装在一起,它将不起作用。

RateLimiter 事件

RateLimiter 有一个 EventPublisher,它在调用近程操作时生成 RateLimiterOnSuccessEventRateLimiterOnFailureEvent 类型的事件,以批示获取权限是否胜利。咱们能够监听这些事件并记录它们,例如:

RateLimiter limiter = registry.rateLimiter("flightSearchService");
limiter.getEventPublisher().onSuccess(e -> System.out.println(e.toString()));
limiter.getEventPublisher().onFailure(e -> System.out.println(e.toString()));

日志输入示例如下:

RateLimiterEvent{type=SUCCESSFUL_ACQUIRE, rateLimiterName='flightSearchService', creationTime=2020-07-21T19:14:33.127+05:30}
... other lines omitted ...
RateLimiterEvent{type=FAILED_ACQUIRE, rateLimiterName='flightSearchService', creationTime=2020-07-21T19:14:33.186+05:30}

RateLimiter 指标

假如在施行客户端节流后,咱们发现 API 的响应工夫减少了。这是可能的 – 正如咱们所见,如果在线程调用近程操作时权限不可用,RateLimiter 会将线程置于期待状态。

如果咱们的申请解决线程常常期待取得许可,则可能意味着咱们的 limitForPeriod 太低。兴许咱们须要与咱们的服务提供商单干并首先取得额定的配额。

监控 RateLimiter 指标可帮忙咱们辨认此类容量问题,并确保咱们在 RateLimiterConfig 上设置的值运行良好。

RateLimiter 跟踪两个指标:可用权限的数量(
resilience4j.ratelimiter.available.permissions)和期待权限的线程数量(
resilience4j.ratelimiter.waiting.threads)。

首先,咱们像平常一样创立 RateLimiterConfigRateLimiterRegistryRateLimiter。而后,咱们创立一个 MeterRegistry 并将 RateLimiterRegistry 绑定到它:

MeterRegistry meterRegistry = new SimpleMeterRegistry();
TaggedRateLimiterMetrics.ofRateLimiterRegistry(registry)
  .bindTo(meterRegistry);

运行几次限速操作后,咱们显示捕捉的指标:

Consumer<Meter> meterConsumer = meter -> {String desc = meter.getId().getDescription();
  String metricName = meter.getId().getName();
  Double metricValue = StreamSupport.stream(meter.measure().spliterator(), false)
    .filter(m -> m.getStatistic().name().equals("VALUE"))
    .findFirst()
    .map(m -> m.getValue())
    .orElse(0.0);
  System.out.println(desc + "-" + metricName + ":" + metricValue);};meterRegistry.forEachMeter(meterConsumer);

这是一些示例输入:

The number of available permissions - resilience4j.ratelimiter.available.permissions: -6.0
The number of waiting threads - resilience4j.ratelimiter.waiting_threads: 7.0

resilience4j.ratelimiter.available.permissions 的负值显示为申请线程保留的权限数。在理论利用中,咱们会定期将数据导出到监控零碎,并在仪表板上进行剖析。

施行客户端速率限度时的陷阱和良好实际

使速率限制器成为单例

对给定近程服务的所有调用都应通过雷同的 RateLimiter 实例。对于给定的近程服务,RateLimiter 必须是单例。

如果咱们不强制执行此操作,咱们代码库的某些区域可能会绕过 RateLimiter 间接调用近程服务。为了避免这种状况,对近程服务的理论调用应该在外围、外部层和其余区域应该应用外部层裸露的限速装璜器。

咱们如何确保将来的新开发人员了解这一用意?查看 Tom 的文章,其中揭示了一种解决此类问题的办法,即通过组织包构造来明确此类用意。此外,它还展现了如何通过在 ArchUnit 测试中编码用意来强制执行此操作。

为多个服务器实例配置速率限制器

为配置找出正确的值可能很辣手。如果咱们在集群中运行多个服务实例,limitForPeriod 的值必须思考到这一点

例如,如果上游服务的速率限度为 100 rps,而咱们的服务有 4 个实例,那么咱们将配置 25 rps 作为每个实例的限度。

然而,这假如咱们每个实例上的负载大致相同。如果状况并非如此,或者如果咱们的服务自身具备弹性并且实例数量可能会有所不同,那么 Resilience4j 的 RateLimiter 可能不适宜。

在这种状况下,咱们须要一个速率限制器,将其数据保留在分布式缓存中,而不是像 Resilience4j RateLimiter 那样保留在内存中。但这会影响咱们服务的响应工夫。另一种抉择是实现某种自适应速率限度。只管 Resilience4j 可能会反对它,但尚不分明何时可用。

抉择正确的超时工夫

对于 timeoutDuration 配置值,咱们应该牢记 API 的预期响应工夫

如果咱们将 timeoutDuration 设置得太高,响应工夫和吞吐量就会受到影响。如果它太低,咱们的错误率可能会减少。

因为此处可能波及一些重复试验,因而一个好的做法是 将咱们在 RateLimiterConfig 中应用的值(如 timeoutDurationlimitForPeriodlimitRefreshPeriod)作为咱们服务之外的配置进行保护。而后咱们能够在不更改代码的状况下更改它们。

调优客户端和服务器端速率限制器

实现客户端速率限度并 不能 保障咱们永远不会受到上游服务的速率限度

假如咱们有来自上游服务的 2 rps 的限度,并且咱们将 limitForPeriod 配置为 2,将 limitRefreshPeriod 配置为 1s。如果咱们在第二秒的最初几毫秒收回两个申请,在此之前没有其余调用,RateLimiter 将容许它们。如果咱们在下一秒的前几毫秒内再进行两次调用,RateLimiter 也会容许它们,因为有两个新权限可用。然而上游服务可能会回绝这两个申请,因为服务器通常会实现基于滑动窗口的速率限度。

为了保障咱们永远不会从上游服务中取得超过速率,咱们须要将客户端中的固定窗口配置为短于服务中的滑动窗口。因而,如果咱们在后面的示例中将 limitForPeriod 配置为 1 并将 limitRefreshPeriod 配置为 500ms,咱们就不会呈现超出速率限度的谬误。然而,第一个申请之后的所有三个申请都会期待,从而减少响应工夫并升高吞吐量。

论断

在本文中,咱们学习了如何应用 Resilience4j 的 RateLimiter 模块来实现客户端速率限度。咱们通过理论示例钻研了配置它的不同办法。咱们学习了一些在施行速率限度时要记住的良好做法和注意事项。

您能够应用 [GitHub 上](
https://github.com/thombergs/…)的代码演示一个残缺的应用程序来阐明这些想法。


本文译自:[Implementing Rate Limiting with Resilience4j – Reflectoring](
https://reflectoring.io/rate-…)

退出移动版