Spring Boot 3 Resilience4j Bulkhead Tutorial

In modern microservice architectures, managing system resources effectively ensures high availability and prevents cascading failures. Resilience4j, a lightweight fault-tolerance library, provides various patterns, including Circuit Breaker, Rate Limiter, Retry, and Bulkhead, to handle system resilience.

This tutorial will focus on the Bulkhead pattern in Spring Boot 3, which isolates critical system resources to prevent overload and ensure service continuity.

What is the Bulkhead Pattern?

The Bulkhead pattern is inspired by ship design, where a ship is divided into watertight compartments. If one compartment is compromised, the damage is contained within it. Similarly, a Bulkhead limits concurrent access to a service or resource in software systems, ensuring failures in one part don’t propagate and compromise the entire system.

In Resilience4j, Bulkhead can be implemented in two modes:

  1. Semaphore Bulkhead: Limits the number of concurrent calls to a service.
  2. ThreadPool Bulkhead: Allocates a separate thread pool for service calls, isolating resources.

Adding Resilience4j to Your Spring Boot 3 Application

First, add the Resilience4j dependency to your project.

Maven Dependency:

<dependency>
    <groupId>io.github.resilience4j</groupId>
    <artifactId>resilience4j-spring-boot3</artifactId>
    <version>2.2.0</version>
</dependency>

Configuring Resilience4j Bulkhead

To use Bulkhead, define its configuration in the application.yml file.

Basic Configuration:

resilience4j:
  bulkhead:
    instances:
      myBulkhead:
        maxConcurrentCalls: 5      # Maximum number of concurrent calls
        maxWaitDuration: 2s        # Maximum time to wait for a free slot
  thread-pool-bulkhead:
    instances:
      myThreadPoolBulkhead:
        coreThreadPoolSize: 3      # Number of core threads in the pool
        maxThreadPoolSize: 10      # Maximum number of threads
        queueCapacity: 5           # Queue capacity before rejecting tasks

Implementing Bulkhead in a Service

Create a service class that simulates resource-intensive processing. We’ll demonstrate both Semaphore Bulkhead and ThreadPool Bulkhead.

Service Implementation:

import io.github.resilience4j.bulkhead.annotation.Bulkhead;
import org.springframework.stereotype.Service;

import java.util.concurrent.TimeUnit;

@Service
public class MyBulkheadService {

    @Bulkhead(name = "myBulkhead", fallbackMethod = "bulkheadFallback")
    public String processWithSemaphoreBulkhead() {
        simulateLongProcessing();
        return "Processed by Semaphore Bulkhead!";
    }

    private void simulateLongProcessing() {
        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }

    public String bulkheadFallback(Throwable t) throws Exception{
        throw new Exception(t.getMessage());
    }

    @Bulkhead(name = "myThreadPoolBulkhead", fallbackMethod = "bulkheadFallbackThreadPool", type = Bulkhead.Type.THREADPOOL)
    public CompletableFuture<String> processWithThreadPoolBulkhead() {
        return CompletableFuture.supplyAsync(() -> {
            simulateLongProcessing();
            return "Processed asynchronously";
        });
    }

    // Fallback method for the ThreadPool bulkhead
    public CompletableFuture<String> bulkheadFallbackThreadPool(Throwable throwable) throws RuntimeException {
        throw new RuntimeException(throwable.getMessage());
    }

}

Creating a REST Controller

To expose these services, create a REST controller with endpoints for testing.

Controller Implementation:

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class MyBulkheadController {

    private final MyBulkheadService bulkheadService;

    public MyBulkheadController(MyBulkheadService bulkheadService) {
        this.bulkheadService = bulkheadService;
    }

    @GetMapping("/semaphore-bulkhead")
    public String testSemaphoreBulkhead() {
        return bulkheadService.processWithSemaphoreBulkhead();
    }

    @GetMapping("/threadpool-bulkhead")
    public CompletableFuture<String> testThreadPoolBulkhead() {
        return bulkheadService.processWithThreadPoolBulkhead();
    }
}

Testing the Bulkhead Configuration

You can test the Bulkhead configuration using Postman or any other REST client.

Semaphore Bulkhead Test:

  1. Send multiple concurrent requests to the /semaphore-bulkhead endpoint.
  2. Observe the behavior:
  • The first five requests (as per maxConcurrentCalls) are processed successfully.
  • The remaining requests wait (up to maxWaitDuration) or fallback if no slot becomes available.
run collection
test result
error rate
Response error rate when reaches 14.86 VU

The test result shows that when the user reaches 13.2 VU, the client gets an error response from Bulkhead.

ThreadPool Bulkhead Test:

  1. Send multiple concurrent requests to the /threadpool-bulkhead endpoint.
  2. Observe the behavior:
  • The requests are queued and processed by a thread pool with a capacity defined by coreThreadPoolSize and queueCapacity.
  • Requests exceeding the pool capacity trigger the fallback method.
run collection
test result
Start error rate
Error rate when Virtual users 20 VU
Error tab

The test result shows that when the user reaches 17.4 VU, the client gets an error response from the Bulkhead thread pool.

Monitoring and Metrics

Spring Boot Actuator and Micrometer can be used to monitor Bulkhead metrics.

Enable Actuator:

Add the dependency:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

Expose the actuator endpoints in application.yml:

management:
  endpoints:
    web:
      exposure:
        include: "*"

Metrics Endpoint:

Access the /actuator/metrics endpoint to view Bulkhead metrics. Metrics like resilience4j.bulkhead.callsresilience4j.threadpool_bulkhead.queue.size, etc., can help monitor performance.

http://localhost:8080/actuator/
{
   "bulkheads":{
      "href":"http://localhost:8080/actuator/bulkheads",
      "templated":false
   },
   "bulkheadevents-bulkheadName-eventType":{
      "href":"http://localhost:8080/actuator/bulkheadevents/{bulkheadName}/{eventType}",
      "templated":true
   },
   "bulkheadevents":{
      "href":"http://localhost:8080/actuator/bulkheadevents",
      "templated":false
   },
   "bulkheadevents-bulkheadName":{
      "href":"http://localhost:8080/actuator/bulkheadevents/{bulkheadName}",
      "templated":true
   }
}

BulkheadEvents

{
   "bulkheadEvents":[
      {
         "bulkheadName":"myThreadPoolBulkhead",
         "type":"CALL_PERMITTED",
         "creationTime":"2024-11-28T16:14:18.850391300+07:00[Asia/Bangkok]"
      },
      {
         "bulkheadName":"myThreadPoolBulkhead",
         "type":"CALL_FINISHED",
         "creationTime":"2024-11-28T16:14:20.856723+07:00[Asia/Bangkok]"
      },
      {
         "bulkheadName":"myThreadPoolBulkhead",
         "type":"CALL_PERMITTED",
         "creationTime":"2024-11-28T16:14:22.043573100+07:00[Asia/Bangkok]"
      },
      {
         "bulkheadName":"myThreadPoolBulkhead",
         "type":"CALL_FINISHED",
         "creationTime":"2024-11-28T16:14:24.046501100+07:00[Asia/Bangkok]"
      },
      {
         "bulkheadName": "myBulkhead",
         "type": "CALL_FINISHED",
         "creationTime": "2024-11-28T16:28:35.983890700+07:00[Asia/Bangkok]"
      }
   ]
}

Common Mistakes and Best Practices

1. Ignoring Fallbacks:

Always provide a fallback method to handle rejected requests in a graceful manner.

2. Misconfigured Limits:

Set realistic values for maxConcurrentCalls and maxWaitDuration (or thread pool size) based on your system’s capacity.

3. Overuse of ThreadPool Bulkhead:

Thread pools can isolate resources but may introduce latency. Use them judiciously.

4. Lack of Metrics Monitoring:

Regularly monitor Bulkhead metrics to adjust configurations for optimal performance.

Advanced Bulkhead Features

Combining Bulkhead with Other Patterns:

Resilience4j allows chaining Bulkhead with other patterns, such as Circuit Breaker or Retry. For example:

resilience4j:
  retry:
    instances:
      myRetry:
        maxAttempts: 3
        waitDuration: 2s

Use annotations to combine them:

@Retry(name = "myRetry")
@Bulkhead(name = "myBulkhead", fallbackMethod = "bulkheadFallback")
public String combinedResiliencePatterns() {
    // Logic here
}

Conclusion

The Bulkhead pattern is a tool to prevent overload and ensure resilience in microservice systems. With Spring Boot 3 and Resilience4j, you can implement and maintain applications with ease. The developer can build web services that handle high-load scenarios without compromising the availability of critical system resources and effectively monitor performance.

By following this tutorial, you’ve learned:

  • What the Bulkhead pattern is.
  • How to implement Semaphore and ThreadPool Bulkhead.
  • How to test, monitor, and avoid common mistakes.

Start using Resilience4j Bulkhead today to improve the reliability and scalability of your Spring Boot applications!

Leave a Comment

Your email address will not be published. Required fields are marked *