到目前为止,在本系列中,咱们曾经理解了 Resilience4j 及其 [Retry](
https://icodewalker.com/blog/...), [RateLimiter](
https://icodewalker.com/blog/...) 和 [TimeLimiter](
https://icodewalker.com/blog/...) 模块。在本文中,咱们将探讨 Bulkhead 模块。咱们将理解它解决了什么问题,何时以及如何应用它,并查看一些示例。
代码示例
本文附有 [GitHub 上](
https://github.com/thombergs/...)的工作代码示例。
什么是 Resilience4j?
请参阅上一篇文章中的形容,疾速理解 [Resilience4j 的个别工作原理]
(https://icodewalker.com/blog/...)。
什么是故障隔离?
几年前,咱们遇到了一个生产问题,其中一台服务器进行响应健康检查,负载均衡器将服务器从池中取出。
就在咱们开始考察这个问题的时候,还有第二个警报——另一台服务器曾经进行响应健康检查,也被从池中取出。
几分钟后,每台服务器都进行响应衰弱探测,咱们的服务齐全敞开。
咱们应用 Redis 为应用程序反对的几个性能缓存一些数据。正如咱们起初发现的那样,Redis 集群同时呈现了一些问题,它已进行承受新连贯。咱们应用 Jedis 库连贯到 Redis,该库的默认行为是无限期地阻塞调用线程,直到建设连贯。
咱们的服务托管在 Tomcat 上,它的默认申请解决线程池大小为 200 个线程。因而,通过连贯到 Redis 的代码门路的每个申请最终都会无限期地阻塞线程。
几分钟之内,集群中的所有 2000 个线程都无限期地阻塞了——甚至没有闲暇线程来响应负载均衡器的健康检查。
该服务自身反对多项性能,并非所有性能都须要拜访 Redis 缓存。然而当这一方面呈现问题时,它最终影响了整个服务。
这正是故障隔离要解决的问题——它能够避免某个服务区域的问题影响整个服务。
尽管咱们的服务产生的事件是一个极其的例子,但咱们能够看到迟缓的上游依赖如何影响调用服务的不相干区域。
如果咱们在每个服务器实例上对 Redis 设置了 20 个并发申请的限度,那么当 Redis 连贯问题产生时,只有这些线程会受到影响。残余的申请解决线程能够持续为其余申请提供服务。
故障隔离背地的想法是对咱们对近程服务进行的并发调用数量设置限度。咱们将对不同近程服务的调用视为不同的、隔离的池,并对能够同时进行的调用数量设置限度。
术语舱壁自身来自它在船舶中的应用,其中船舶的底部被分成彼此离开的局部。如果有裂缝,并且水开始流入,则只有该局部会充斥水。这能够避免整艘船沉没。
Resilience4j 隔板概念
resilience4j-bulkhead 的工作原理相似于其余 Resilience4j 模块。咱们为它提供了咱们想要作为函数结构执行的代码——一个进行近程调用的 lambda 表达式或一个从近程服务中检索到的某个值的 Supplier,等等——并且隔板用代码装璜它以管制并发调用数。
Resilience4j 提供两种类型的隔板 - SemaphoreBulkhead
和 ThreadPoolBulkhead
。
SemaphoreBulkhead
外部应用java.util.concurrent.Semaphore
来管制并发调用的数量并在以后线程上执行咱们的代码。
ThreadPoolBulkhead
应用线程池中的一个线程来执行咱们的代码。它外部应用java.util.concurrent.ArrayBlockingQueue
和java.util.concurrent.ThreadPoolExecutor
来管制并发调用的数量。
SemaphoreBulkhead
让咱们看看与信号量隔板相干的配置及其含意。
maxConcurrentCalls
确定咱们能够对近程服务进行的最大并发调用数。咱们能够将此值视为初始化信号量的许可数。
任何尝试超过此限度调用近程服务的线程都能够立刻取得 BulkheadFullException
或期待一段时间以期待另一个线程开释许可。这由 maxWaitDuration 值决定。
当有多个线程在期待许可时,fairCallHandlingEnabled
配置确定期待的线程是否以先进先出的程序获取许可。
最初, writableStackTraceEnabled
配置让咱们能够在 BulkheadFullException
产生时缩小堆栈跟踪中的信息量。这很有用,因为如果没有它,当异样屡次产生时,咱们的日志可能会充斥许多相似的信息。通常在读取日志时,只晓得产生了 BulkheadFullException
就足够了。
ThreadPoolBulkhead
coreThreadPoolSize
、 maxThreadPoolSize
、 keepAliveDuration
和 queueCapacity
是与 ThreadPoolBulkhead
相干的次要配置。ThreadPoolBulkhead
外部应用这些配置来结构一个 ThreadPoolExecutor
。
internalThreadPoolExecutor
应用可用的闲暇线程之一执行传入的工作。 如果没有线程能够自在执行传入的工作,则该工作将排队期待线程可用时稍后执行。如果已达到 queueCapacity
,则近程调用将被回绝并返回 BulkheadFullException
。
ThreadPoolBulkhead
也有 writableStackTraceEnabled
配置来管制 BulkheadFullException
的堆栈跟踪中的信息量。
应用 Resilience4j 隔板模块
让咱们看看如何应用 resilience4j-bulkhead 模块中可用的各种性能。
咱们将应用与本系列前几篇文章雷同的示例。假如咱们正在为一家航空公司建设一个网站,以容许其客户搜寻和预订航班。咱们的服务与 FlightSearchService
类封装的近程服务对话。
SemaphoreBulkhead
应用基于信号量的隔板时,BulkheadRegistry
、BulkheadConfig
和 Bulkhead
是咱们应用的次要形象。
BulkheadRegistry
是一个用于创立和治理 Bulkhead 对象的工厂。
BulkheadConfig
封装了 maxConcurrentCalls
、maxWaitDuration
、writableStackTraceEnabled
和 fairCallHandlingEnabled
配置。每个 Bulkhead
对象都与一个 BulkheadConfig
相关联。
第一步是创立一个 BulkheadConfig
:
BulkheadConfig config = BulkheadConfig.ofDefaults();
这将创立一个 BulkheadConfig
,其默认值为 maxConcurrentCalls
(25)、maxWaitDuration
(0s)、writableStackTraceEnabled
(true) 和 fairCallHandlingEnabled
(true)。
假如咱们心愿将并发调用的数量限度为 2,并且咱们违心期待 2 秒让线程取得许可:
BulkheadConfig config = BulkheadConfig.custom() .maxConcurrentCalls(2) .maxWaitDuration(Duration.ofSeconds(2)) .build();
而后咱们创立一个 Bulkhead
:
BulkheadRegistry registry = BulkheadRegistry.of(config);Bulkhead bulkhead = registry.bulkhead("flightSearchService");
当初让咱们表白咱们的代码以作为 Supplier
运行航班搜寻并应用 bulkhead
装璜它:
BulkheadRegistry registry = BulkheadRegistry.of(config);Bulkhead bulkhead = registry.bulkhead("flightSearchService");
最初,让咱们调用几次装璜操作来理解隔板的工作原理。咱们能够应用 CompletableFuture
来模仿来自用户的并发航班搜寻申请:
for (int i=0; i<4; i++) { CompletableFuture .supplyAsync(decoratedFlightsSupplier) .thenAccept(flights -> System.out.println("Received results"));}
输入中的工夫戳和线程名称显示,在 4 个并发申请中,前两个申请立刻通过:
Searching for flights; current time = 11:42:13 187; current thread = ForkJoinPool.commonPool-worker-3Searching for flights; current time = 11:42:13 187; current thread = ForkJoinPool.commonPool-worker-5Flight search successful at 11:42:13 226Flight search successful at 11:42:13 226Received resultsReceived resultsSearching for flights; current time = 11:42:14 239; current thread = ForkJoinPool.commonPool-worker-9Searching for flights; current time = 11:42:14 239; current thread = ForkJoinPool.commonPool-worker-7Flight search successful at 11:42:14 239Flight search successful at 11:42:14 239Received resultsReceived results
第三个和第四个申请仅在 1 秒后就可能取得许可,在之前的申请实现之后。
如果线程无奈在咱们指定的 2s maxWaitDuration
内取得许可,则会抛出 BulkheadFullException
:
Caused by: io.github.resilience4j.bulkhead.BulkheadFullException: Bulkhead 'flightSearchService' is full and does not permit further calls at io.github.resilience4j.bulkhead.BulkheadFullException.createBulkheadFullException(BulkheadFullException.java:49) at io.github.resilience4j.bulkhead.internal.SemaphoreBulkhead.acquirePermission(SemaphoreBulkhead.java:164) at io.github.resilience4j.bulkhead.Bulkhead.lambda$decorateSupplier$5(Bulkhead.java:194) at java.base/java.util.concurrent.CompletableFuture$AsyncSupply.run(CompletableFuture.java:1700) ... 6 more
除了第一行,堆栈跟踪中的其余行没有减少太多价值。如果 BulkheadFullException
产生屡次,这些堆栈跟踪即将在咱们的日志文件中反复。
咱们能够通过将 writableStackTraceEnabled
配置设置为 false
来缩小堆栈跟踪中生成的信息量:
BulkheadConfig config = BulkheadConfig.custom() .maxConcurrentCalls(2) .maxWaitDuration(Duration.ofSeconds(1)) .writableStackTraceEnabled(false).build();
当初,当 BulkheadFullException
产生时,堆栈跟踪中只存在一行:
Searching for flights; current time = 12:27:58 658; current thread = ForkJoinPool.commonPool-worker-3Searching for flights; current time = 12:27:58 658; current thread = ForkJoinPool.commonPool-worker-5io.github.resilience4j.bulkhead.BulkheadFullException: Bulkhead 'flightSearchService' is full and does not permit further callsFlight search successful at 12:27:58 699Flight search successful at 12:27:58 699Received resultsReceived results
与咱们见过的其余 Resilience4j 模块相似,Bulkhead
还提供了额定的办法,如 decorateCheckedSupplier()
、decorateCompletionStage()
、decorateRunnable()
、decorateConsumer()
等,因而咱们能够在 Supplier
供应商之外的其余构造中提供咱们的代码。
ThreadPoolBulkhead
当应用基于线程池的隔板时,ThreadPoolBulkheadRegistry
、ThreadPoolBulkheadConfig
和 ThreadPoolBulkhead
是咱们应用的次要形象。
ThreadPoolBulkheadRegistry
是用于创立和治理 ThreadPoolBulkhead
对象的工厂。
ThreadPoolBulkheadConfig
封装了 coreThreadPoolSize
、 maxThreadPoolSize
、 keepAliveDuration
和 queueCapacity
配置。每个 ThreadPoolBulkhead
对象都与一个 ThreadPoolBulkheadConfig 相关联。
第一步是创立一个 ThreadPoolBulkheadConfig
:
ThreadPoolBulkheadConfig config = ThreadPoolBulkheadConfig.ofDefaults();
这将创立一个 ThreadPoolBulkheadConfig
,其默认值为 coreThreadPoolSize
(可用处理器数量 -1)、maxThreadPoolSiz
e(可用处理器最大数量)、keepAliveDuration
(20ms)和 queueCapacity
(100)。
假如咱们要将并发调用的数量限度为 2:
ThreadPoolBulkheadConfig config = ThreadPoolBulkheadConfig.custom() .maxThreadPoolSize(2) .coreThreadPoolSize(1) .queueCapacity(1) .build();
而后咱们创立一个 ThreadPoolBulkhead
:
ThreadPoolBulkheadRegistry registry = ThreadPoolBulkheadRegistry.of(config);ThreadPoolBulkhead bulkhead = registry.bulkhead("flightSearchService");
当初让咱们表白咱们的代码以作为 Supplier
运行航班搜寻并应用 bulkhead
装璜它:
Supplier<List<Flight>> flightsSupplier = () -> service.searchFlightsTakingOneSecond(request);Supplier<CompletionStage<List<Flight>>> decoratedFlightsSupplier = ThreadPoolBulkhead.decorateSupplier(bulkhead, flightsSupplier);
与返回一个 Supplier<List<Flight>>
的SemaphoreBulkhead.decorateSupplier()
不同,ThreadPoolBulkhead.decorateSupplier()
返回一个 Supplier<CompletionStage<List<Flight>>
。这是因为 ThreadPoolBulkHead
不会在以后线程上同步执行代码。
最初,让咱们调用几次装璜操作来理解隔板的工作原理:
for (int i=0; i<3; i++) { decoratedFlightsSupplier .get() .whenComplete((r,t) -> { if (r != null) { System.out.println("Received results"); } if (t != null) { t.printStackTrace(); } });}
输入中的工夫戳和线程名称显示,尽管前两个申请立刻执行,但第三个申请已排队,稍后由开释的线程之一执行:
Searching for flights; current time = 16:15:00 097; current thread = bulkhead-flightSearchService-1Searching for flights; current time = 16:15:00 097; current thread = bulkhead-flightSearchService-2Flight search successful at 16:15:00 136Flight search successful at 16:15:00 135Received resultsReceived resultsSearching for flights; current time = 16:15:01 151; current thread = bulkhead-flightSearchService-2Flight search successful at 16:15:01 151Received results
如果队列中没有闲暇线程和容量,则抛出 BulkheadFullException
:
Exception in thread "main" io.github.resilience4j.bulkhead.BulkheadFullException: Bulkhead 'flightSearchService' is full and does not permit further calls at io.github.resilience4j.bulkhead.BulkheadFullException.createBulkheadFullException(BulkheadFullException.java:64) at io.github.resilience4j.bulkhead.internal.FixedThreadPoolBulkhead.submit(FixedThreadPoolBulkhead.java:157)... other lines omitted ...
咱们能够应用 writableStackTraceEnabled
配置来缩小堆栈跟踪中生成的信息量:
ThreadPoolBulkheadConfig config = ThreadPoolBulkheadConfig.custom() .maxThreadPoolSize(2) .coreThreadPoolSize(1) .queueCapacity(1) .writableStackTraceEnabled(false) .build();
当初,当 BulkheadFullException
产生时,堆栈跟踪中只存在一行:
Searching for flights; current time = 12:27:58 658; current thread = ForkJoinPool.commonPool-worker-3Searching for flights; current time = 12:27:58 658; current thread = ForkJoinPool.commonPool-worker-5io.github.resilience4j.bulkhead.BulkheadFullException: Bulkhead 'flightSearchService' is full and does not permit further callsFlight search successful at 12:27:58 699Flight search successful at 12:27:58 699Received resultsReceived results
上下文流传
有时咱们将数据存储在 ThreadLocal
变量中并在代码的不同区域中读取它。咱们这样做是为了防止在办法链之间显式地将数据作为参数传递,尤其是当该值与咱们正在实现的外围业务逻辑没有间接关系时。
例如,咱们可能心愿将以后用户 ID 或事务 ID 或某个申请跟踪 ID 记录到每个日志语句中,以便更轻松地搜寻日志。对于此类场景,应用 ThreadLocal
是一种有用的技术。
应用 ThreadPoolBulkhead
时,因为咱们的代码不在以后线程上执行,因而咱们存储在 ThreadLocal 变量中的数据在其余线程中将不可用。
让咱们看一个例子来了解这个问题。首先咱们定义一个 RequestTrackingIdHolder
类,一个围绕 ThreadLocal
的包装类:
class RequestTrackingIdHolder { static ThreadLocal<String> threadLocal = new ThreadLocal<>(); static String getRequestTrackingId() { return threadLocal.get(); } static void setRequestTrackingId(String id) { if (threadLocal.get() != null) { threadLocal.set(null); threadLocal.remove(); } threadLocal.set(id); } static void clear() { threadLocal.set(null); threadLocal.remove(); }}
静态方法能够轻松设置和获取存储在 ThreadLocal
上的值。咱们接下来在调用隔板装璜的航班搜寻操作之前设置一个申请跟踪 ID:
for (int i=0; i<2; i++) { String trackingId = UUID.randomUUID().toString(); System.out.println("Setting trackingId " + trackingId + " on parent, main thread before calling flight search"); RequestTrackingIdHolder.setRequestTrackingId(trackingId); decoratedFlightsSupplier .get() .whenComplete((r,t) -> { // other lines omitted });}
示例输入显示此值在隔板治理的线程中不可用:
Setting trackingId 98ff99df-466a-47f7-88f7-5e31fc8fcb6b on parent, main thread before calling flight searchSetting trackingId 6b98d73c-a590-4a20-b19d-c85fea783caf on parent, main thread before calling flight searchSearching for flights; current time = 19:53:53 799; current thread = bulkhead-flightSearchService-1; Request Tracking Id = nullFlight search successful at 19:53:53 824Received resultsSearching for flights; current time = 19:53:54 836; current thread = bulkhead-flightSearchService-1; Request Tracking Id = nullFlight search successful at 19:53:54 836Received results
为了解决这个问题,ThreadPoolBulkhead
提供了一个 ContextPropagator
。ContextPropagator
是一种用于跨线程边界检索、复制和清理值的形象。它定义了一个接口,其中蕴含从以后线程 (retrieve()
) 获取值、将其复制到新的执行线程 (copy()
) 并最终在执行线程 (clear()
) 上进行清理的办法。
让咱们实现一个RequestTrackingIdPropagator
:
class RequestTrackingIdPropagator implements ContextPropagator { @Override public Supplier<Optional> retrieve() { System.out.println("Getting request tracking id from thread: " + Thread.currentThread().getName()); return () -> Optional.of(RequestTrackingIdHolder.getRequestTrackingId()); } @Override Consumer<Optional> copy() { return optional -> { System.out.println("Setting request tracking id " + optional.get() + " on thread: " + Thread.currentThread().getName()); optional.ifPresent(s -> RequestTrackingIdHolder.setRequestTrackingId(s.toString())); }; } @Override Consumer<Optional> clear() { return optional -> { System.out.println("Clearing request tracking id on thread: " + Thread.currentThread().getName()); optional.ifPresent(s -> RequestTrackingIdHolder.clear()); }; }}
咱们通过在 ThreadPoolBulkheadConfig
上的设置来为 ThreadPoolBulkhead
提供 ContextPropagator
:
class RequestTrackingIdPropagator implements ContextPropagator { @Override public Supplier<Optional> retrieve() { System.out.println("Getting request tracking id from thread: " + Thread.currentThread().getName()); return () -> Optional.of(RequestTrackingIdHolder.getRequestTrackingId()); } @Override Consumer<Optional> copy() { return optional -> { System.out.println("Setting request tracking id " + optional.get() + " on thread: " + Thread.currentThread().getName()); optional.ifPresent(s -> RequestTrackingIdHolder.setRequestTrackingId(s.toString())); }; } @Override Consumer<Optional> clear() { return optional -> { System.out.println("Clearing request tracking id on thread: " + Thread.currentThread().getName()); optional.ifPresent(s -> RequestTrackingIdHolder.clear()); }; }}
当初,示例输入显示申请跟踪 ID 在隔板治理的线程中可用:
Setting trackingId 71d44cb8-dab6-4222-8945-e7fd023528ba on parent, main thread before calling flight searchGetting request tracking id from thread: mainSetting trackingId 5f9dd084-f2cb-4a20-804b-038828abc161 on parent, main thread before calling flight searchGetting request tracking id from thread: mainSetting request tracking id 71d44cb8-dab6-4222-8945-e7fd023528ba on thread: bulkhead-flightSearchService-1Searching for flights; current time = 20:07:56 508; current thread = bulkhead-flightSearchService-1; Request Tracking Id = 71d44cb8-dab6-4222-8945-e7fd023528baFlight search successful at 20:07:56 538Clearing request tracking id on thread: bulkhead-flightSearchService-1Received resultsSetting request tracking id 5f9dd084-f2cb-4a20-804b-038828abc161 on thread: bulkhead-flightSearchService-1Searching for flights; current time = 20:07:57 542; current thread = bulkhead-flightSearchService-1; Request Tracking Id = 5f9dd084-f2cb-4a20-804b-038828abc161Flight search successful at 20:07:57 542Clearing request tracking id on thread: bulkhead-flightSearchService-1Received results
Bulkhead事件
Bulkhead
和 ThreadPoolBulkhead
都有一个 EventPublisher
来生成以下类型的事件:
- BulkheadOnCallPermittedEvent
- BulkheadOnCallRejectedEvent 和
- BulkheadOnCallFinishedEvent
咱们能够监听这些事件并记录它们,例如:
Bulkhead bulkhead = registry.bulkhead("flightSearchService");bulkhead.getEventPublisher().onCallPermitted(e -> System.out.println(e.toString()));bulkhead.getEventPublisher().onCallFinished(e -> System.out.println(e.toString()));bulkhead.getEventPublisher().onCallRejected(e -> System.out.println(e.toString()));
示例输入显示了记录的内容:
2020-08-26T12:27:39.790435: Bulkhead 'flightSearch' permitted a call.... other lines omitted ...2020-08-26T12:27:40.290987: Bulkhead 'flightSearch' rejected a call.... other lines omitted ...2020-08-26T12:27:41.094866: Bulkhead 'flightSearch' has finished a call.
Bulkhead 指标
SemaphoreBulkhead
Bulkhead
裸露了两个指标:
- 可用权限的最大数量(
resilience4j.bulkhead.max.allowed.concurrent.calls
),和 - 容许的并发调用数(
resilience4j.bulkhead.available.concurrent.calls
)。
bulkhead.available
指标与咱们在 BulkheadConfig
上配置的 maxConcurrentCalls
雷同。
首先,咱们像后面一样创立 BulkheadConfig
、BulkheadRegistry
和 Bulkhead
。而后,咱们创立一个 MeterRegistry
并将 BulkheadRegistry
绑定到它:
MeterRegistry meterRegistry = new SimpleMeterRegistry();TaggedBulkheadMetrics.ofBulkheadRegistry(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 maximum number of available permissions - resilience4j.bulkhead.max.allowed.concurrent.calls: 8.0The number of available permissions - resilience4j.bulkhead.available.concurrent.calls: 3.0
ThreadPoolBulkhead
ThreadPoolBulkhead
裸露五个指标:
- 队列的以后长度(
resilience4j.bulkhead.queue.depth
), - 以后线程池的大小(
resilience4j.bulkhead.thread.pool.size
), - 线程池的外围和最大容量(
resilience4j.bulkhead.core.thread.pool.size
和resilience4j.bulkhead.max.thread.pool.size
),以及 - 队列的容量(
resilience4j.bulkhead.queue.capacity
)。
首先,咱们像后面一样创立 ThreadPoolBulkheadConfig
、ThreadPoolBulkheadRegistry
和 ThreadPoolBulkhead
。而后,咱们创立一个 MeterRegistry
并将ThreadPoolBulkheadRegistry
绑定到它:
MeterRegistry meterRegistry = new SimpleMeterRegistry();TaggedThreadPoolBulkheadMetrics.ofThreadPoolBulkheadRegistry(registry).bindTo(meterRegistry);
运行几次隔板装璜操作后,咱们将显示捕捉的指标:
The queue capacity - resilience4j.bulkhead.queue.capacity: 5.0The queue depth - resilience4j.bulkhead.queue.depth: 1.0The thread pool size - resilience4j.bulkhead.thread.pool.size: 5.0The maximum thread pool size - resilience4j.bulkhead.max.thread.pool.size: 5.0The core thread pool size - resilience4j.bulkhead.core.thread.pool.size: 3.0
在理论利用中,咱们会定期将数据导出到监控零碎并在仪表板上进行剖析。
施行隔板时的陷阱和良好实际
使隔板成为单例
对给定近程服务的所有调用都应通过同一个 Bulkhead
实例。对于给定的近程服务,Bulkhead
必须是单例。
如果咱们不强制执行此操作,咱们代码库的某些区域可能会绕过 Bulkhead 间接调用近程服务。为了避免这种状况,近程服务的理论调用应该在一个外围、外部层和其余区域应该应用外部层裸露的隔板装璜器。
咱们如何确保将来的新开发人员了解这一用意? 查看 Tom 的文章,该文章展现了解决此类问题的一种办法,即通过组织包构造来明确此类用意。此外,它还展现了如何通过在 ArchUnit 测试中编码用意来强制执行此操作。
与其余 Resilience4j 模块联合
将隔板与一个或多个其余 Resilience4j 模块(如重试和速率限制器)联合应用会更无效。例如,如果有 BulkheadFullException,咱们可能心愿在一些提早后重试。
论断
在本文中,咱们学习了如何应用 Resilience4j 的 Bulkhead 模块对咱们对近程服务进行的并发调用设置限度。咱们理解了为什么这很重要,还看到了一些无关如何配置它的理论示例。
您能够应用 [GitHub 上](
https://github.com/thombergs/...)的代码演示一个残缺的应用程序。
本文译自:Implementing Bulkhead with Resilience4j - Reflectoring