| Communication Between Microservices :👈 | 👉:Strangler Fig Pattern |
๐งฑ Bulkhead Pattern in .NET Core |
๐งฑ The Bulkhead Pattern
The Bulkhead pattern is a design pattern used in software architecture to improve the resilience and fault tolerance of a system. It works by partitioning components or resources into isolated pools or "bulkheads" so that a failure or overload in one area does not cause a cascade of failures throughout the entire system. The name comes from the watertight compartments on a ship's hull, which contain flooding to a single section to prevent the ship from sinking.
โ๏ธ In a software context, bulkheads can be implemented in various ways, such as:
- ๐งต Thread pools: Isolating calls to different downstream services using separate, limited thread pools.
- ๐๏ธ Database connection pools: Assigning dedicated connection pools for different services that share a database.
- ๐งฉ Processes or containers: Deploying different microservices in separate containers or processes to isolate their resources.
๐งฑ What Is the Bulkhead Pattern?
- Purpose: Isolate parts of the system to prevent a single failure from affecting others.
- Analogy: Like watertight compartments in a ship.
- Use Case: Protect external API calls, database access, or any resource-intensive operation.
๐ก Example: Bulkhead with Polly in .NET Core
A common way to implement the Bulkhead pattern in .NET Core is by using the Polly library. This example shows how to use Polly's BulkheadPolicy to isolate API calls to different downstream services.
Scenario: An e-commerce API needs to fetch product details and user reviews from separate, potentially unreliable microservices. The product details are critical for the user interface, while the reviews are secondary. We use a bulkhead to ensure that a slow or failing review service does not block all available threads and cause the entire API to become unresponsive.
1๏ธโฃ Install NuGet packages
Install the necessary Polly packages for your project.
dotnet add package Polly
dotnet add package Microsoft.Extensions.Http.Polly
2๏ธโฃ Configure bulkheads in Program.cs
Use IHttpClientFactory to apply different Bulkhead policies for different downstream services.
using Polly;
var builder = WebApplication.CreateBuilder(args);
// Bulkhead for the critical Product Service
builder.Services.AddHttpClient("productClient", client =>
{
client.BaseAddress = new Uri("https://product-service.example.com/");
}).AddPolicyHandler(Policy.BulkheadAsync<HttpResponseMessage>(
maxParallelization: 10, // Max 10 concurrent requests
maxQueuingActions: 50, // Queue up to 50 requests
onBulkheadRejectedAsync: context =>
{
// Log the rejection
Console.WriteLine("Request to Product Service was rejected by bulkhead.");
return Task.CompletedTask;
}));
// Bulkhead for the non-critical Review Service
builder.Services.AddHttpClient("reviewClient", client =>
{
client.BaseAddress = new Uri("https://review-service.example.com/");
}).AddPolicyHandler(Policy.BulkheadAsync<HttpResponseMessage>(
maxParallelization: 3, // Only 3 concurrent requests
maxQueuingActions: 10, // Queue up to 10 requests
onBulkheadRejectedAsync: context =>
{
// Log the rejection and return a fallback
Console.WriteLine("Request to Review Service was rejected by bulkhead.");
return Task.CompletedTask;
}));
var app = builder.Build();
// Other app configuration...
app.Run();
3๏ธโฃ Use the clients in a controller
Inject IHttpClientFactory into your controller or service and create clients based on the named bulkheads.
public class ProductDetailsController : ControllerBase
{
private readonly IHttpClientFactory _clientFactory;
public ProductDetailsController(IHttpClientFactory clientFactory)
{
_clientFactory = clientFactory;
}
[HttpGet("product/{id}")]
public async Task<IActionResult> GetProductDetails(int id)
{
// Get the client for the critical Product Service
var productClient = _clientFactory.CreateClient("productClient");
var reviewClient = _clientFactory.CreateClient("reviewClient");
var productTask = productClient.GetAsync($"products/{id}");
var reviewsTask = reviewClient.GetAsync($"reviews/{id}");
// Wait for product details, but don't hold up for reviews
var productResponse = await productTask;
// The reviews task might be rejected, so handle the exception
try
{
var reviewResponse = await reviewsTask;
// Process reviews...
}
catch (Polly.Bulkhead.BulkheadRejectedException)
{
// Handle the case where the reviews request was rejected
// Return product details without reviews
}
return Ok(await productResponse.Content.ReadAsStringAsync());
}
}
โ Advantages
- ๐งฉ Prevents cascading failures: Isolating failures within one component prevents a misbehaving dependency from impacting the entire application.
- โ๏ธ Improves stability: Ensures that the system remains stable and responsive for all other operations, even when one dependency is failing or slow.
- ๐ฏ Fair resource allocation: Bulkheads prevent a single, poorly behaving component from monopolizing all shared resources, like threads, connections, or memory.
- ๐ฆ Prioritization: Allows for prioritizing access to resources. You can configure higher concurrency limits for critical services and lower limits for less important ones.
โ ๏ธ Disadvantages
- ๐งฎ Increased complexity: Designing and managing separate resource pools adds complexity to the system architecture and its configuration.
- ๐ค Resource underutilization: By statically partitioning resources, you might have idle resources in one bulkhead while another is under heavy load.
- ๐ Requires careful tuning: Determining the right number of concurrent calls and queue size for each bulkhead requires careful monitoring and tuning.
๐ When to use
- ๐ Interacting with unreliable dependencies: When calling external APIs, databases, or microservices prone to latency or failure.
- ๐ง Preventing resource exhaustion: In multi-threaded applications, to protect the application's thread pool from being consumed by a failing service.
- ๐ Prioritizing workloads: When you have critical vs. non-critical traffic that should be isolated.
๐ซ When not to use
- ๐๏ธ For simple, monolith applications: The added complexity is not justified for apps without external dependencies.
- โ For permanent failures: Bulkheads protect against resource exhaustion but don't solve the root issue.
- ๐ช When resources are abundant: If performance impact is minimal, simpler patterns like Retry might suffice.
๐ Best practices
- ๐ Combine with other resilience patterns like Circuit Breaker, Retries, and Timeouts.
- ๐ Tune, monitor, and adjust using Prometheus, Grafana, etc.
- ๐ Log bulkhead rejections for insights and fine-tuning.
- โ๏ธ Use IHttpClientFactory to apply policies consistently.
๐ง Precautions
- โ ๏ธ Watch for dependency conflicts: Ensure proper isolation.
- ๐งต Understand semaphore vs. thread pool: Choose based on performance needs.
- ๐ง Beware of shared resources: Bulkheads fail if the resource is shared (e.g., same DB pool).
๐ Limitations
- ๐ซ Doesn't prevent the original failure: It only contains damage.
- ๐ Static resource allocation: Requires manual tuning in Polly.
- ๐ No guarantee of availability: Prevents cascades but not recovery.
๐ก Tips
- โ๏ธ Start with conservative limits and adjust as needed.
- ๐ช Use fallback strategies like cached responses or defaults.
- ๐ฏ Prioritize critical calls: Allocate more resources to them.
| Communication Between Microservices :👈 | 👉:Strangler Fig Pattern |
Comments and Discussion