Java 專案中使用 Resilience4j 框架實現故障隔離

信碼由韁發表於2021-12-02

Java 專案中使用 Resilience4j 框架實現故障隔離

到目前為止,在本系列中,我們已經瞭解了 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

coreThreadPoolSizemaxThreadPoolSizekeepAliveDurationqueueCapacity 是與 ThreadPoolBulkhead 相關的主要配置。ThreadPoolBulkhead 內部使用這些配置來構造一個 ThreadPoolExecutor

internalThreadPoolExecutor 使用可用的空閒執行緒之一執行傳入的任務。 如果沒有執行緒可以自由執行傳入的任務,則該任務將排隊等待執行緒可用時稍後執行。如果已達到 queueCapacity,則遠端呼叫將被拒絕並返回 BulkheadFullException

ThreadPoolBulkhead 也有 writableStackTraceEnabled 配置來控制 BulkheadFullException 的堆疊跟蹤中的資訊量。

使用 Resilience4j 隔板模組

讓我們看看如何使用 resilience4j-bulkhead 模組中可用的各種功能。

我們將使用與本系列前幾篇文章相同的示例。假設我們正在為一家航空公司建立一個網站,以允許其客戶搜尋和預訂航班。我們的服務與 FlightSearchService 類封裝的遠端服務對話。

SemaphoreBulkhead

使用基於訊號量的隔板時,BulkheadRegistryBulkheadConfigBulkhead 是我們使用的主要抽象。

BulkheadRegistry 是一個用於建立和管理 Bulkhead 物件的工廠。

BulkheadConfig 封裝了 maxConcurrentCallsmaxWaitDurationwritableStackTraceEnabledfairCallHandlingEnabled 配置。每個 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-3
Searching for flights; current time = 11:42:13 187; current thread = ForkJoinPool.commonPool-worker-5
Flight search successful at 11:42:13 226
Flight search successful at 11:42:13 226
Received results
Received results
Searching for flights; current time = 11:42:14 239; current thread = ForkJoinPool.commonPool-worker-9
Searching for flights; current time = 11:42:14 239; current thread = ForkJoinPool.commonPool-worker-7
Flight search successful at 11:42:14 239
Flight search successful at 11:42:14 239
Received results
Received 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-3
Searching for flights; current time = 12:27:58 658; current thread = ForkJoinPool.commonPool-worker-5
io.github.resilience4j.bulkhead.BulkheadFullException: Bulkhead 'flightSearchService' is full and does not permit further calls
Flight search successful at 12:27:58 699
Flight search successful at 12:27:58 699
Received results
Received results

與我們見過的其他 Resilience4j 模組類似,Bulkhead 還提供了額外的方法,如 decorateCheckedSupplier()decorateCompletionStage()decorateRunnable()decorateConsumer() 等,因此我們可以在 Supplier 供應商之外的其他結構中提供我們的程式碼。

ThreadPoolBulkhead

當使用基於執行緒池的隔板時,
ThreadPoolBulkheadRegistryThreadPoolBulkheadConfigThreadPoolBulkhead 是我們使用的主要抽象。

ThreadPoolBulkheadRegistry 是用於建立和管理 ThreadPoolBulkhead 物件的工廠。

ThreadPoolBulkheadConfig 封裝了 coreThreadPoolSizemaxThreadPoolSizekeepAliveDurationqueueCapacity 配置。每個 ThreadPoolBulkhead 物件都與一個 ThreadPoolBulkheadConfig 相關聯。

第一步是建立一個 ThreadPoolBulkheadConfig

ThreadPoolBulkheadConfig config =
  ThreadPoolBulkheadConfig.ofDefaults();

這將建立一個 ThreadPoolBulkheadConfig,其預設值為 coreThreadPoolSize(可用處理器數量 -1)、maxThreadPoolSize(可用處理器最大數量)、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-1
Searching for flights; current time = 16:15:00 097; current thread = bulkhead-flightSearchService-2
Flight search successful at 16:15:00 136
Flight search successful at 16:15:00 135
Received results
Received results
Searching for flights; current time = 16:15:01 151; current thread = bulkhead-flightSearchService-2
Flight search successful at 16:15:01 151
Received 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-3
Searching for flights; current time = 12:27:58 658; current thread = ForkJoinPool.commonPool-worker-5
io.github.resilience4j.bulkhead.BulkheadFullException: Bulkhead 'flightSearchService' is full and does not permit further calls
Flight search successful at 12:27:58 699
Flight search successful at 12:27:58 699
Received results
Received 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 search
Setting trackingId 6b98d73c-a590-4a20-b19d-c85fea783caf on parent, main thread before calling flight search
Searching for flights; current time = 19:53:53 799; current thread = bulkhead-flightSearchService-1; Request Tracking Id = null
Flight search successful at 19:53:53 824
Received results
Searching for flights; current time = 19:53:54 836; current thread = bulkhead-flightSearchService-1; Request Tracking Id = null
Flight search successful at 19:53:54 836
Received results

為了解決這個問題,ThreadPoolBulkhead 提供了一個 ContextPropagatorContextPropagator 是一種用於跨執行緒邊界檢索、複製和清理值的抽象。它定義了一個介面,其中包含從當前執行緒 (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 search
Getting request tracking id from thread: main
Setting trackingId 5f9dd084-f2cb-4a20-804b-038828abc161 on parent, main thread before calling flight search
Getting request tracking id from thread: main
Setting request tracking id 71d44cb8-dab6-4222-8945-e7fd023528ba on thread: bulkhead-flightSearchService-1
Searching for flights; current time = 20:07:56 508; current thread = bulkhead-flightSearchService-1; Request Tracking Id = 71d44cb8-dab6-4222-8945-e7fd023528ba
Flight search successful at 20:07:56 538
Clearing request tracking id on thread: bulkhead-flightSearchService-1
Received results
Setting request tracking id 5f9dd084-f2cb-4a20-804b-038828abc161 on thread: bulkhead-flightSearchService-1
Searching for flights; current time = 20:07:57 542; current thread = bulkhead-flightSearchService-1; Request Tracking Id = 5f9dd084-f2cb-4a20-804b-038828abc161
Flight search successful at 20:07:57 542
Clearing request tracking id on thread: bulkhead-flightSearchService-1
Received 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 相同。

首先,我們像前面一樣建立 BulkheadConfigBulkheadRegistryBulkhead。然後,我們建立一個 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.0
The 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.sizeresilience4j.bulkhead.max.thread.pool.size),以及
  • 佇列的容量(resilience4j.bulkhead.queue.capacity)。

首先,我們像前面一樣建立 ThreadPoolBulkheadConfig
ThreadPoolBulkheadRegistryThreadPoolBulkhead。然後,我們建立一個 MeterRegistry 並將
ThreadPoolBulkheadRegistry 繫結到它:

MeterRegistry meterRegistry = new SimpleMeterRegistry();
TaggedThreadPoolBulkheadMetrics.ofThreadPoolBulkheadRegistry(registry).bindTo(meterRegistry);

執行幾次隔板裝飾操作後,我們將顯示捕獲的指標:

The queue capacity - resilience4j.bulkhead.queue.capacity: 5.0
The queue depth - resilience4j.bulkhead.queue.depth: 1.0
The thread pool size - resilience4j.bulkhead.thread.pool.size: 5.0
The maximum thread pool size - resilience4j.bulkhead.max.thread.pool.size: 5.0
The 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

相關文章