Bulkhead Pattern: Kaynak İzolasyonu ile Dayanıklılık
Mikroservis mimarisinde, bir servisin yavaşlaması veya çökmesi, zincirleme bir etkiyle (cascade failure) tüm sistemi çökertebilir. Bir servisteki yavaşlık, thread havuzunu tüketebilir; bir veritabanı bağlantısının tıkanması, diğer servislerin de veritabanına erişememesine yol açabilir. İşte Bulkhead Pattern (Bölme Deseni), bu sorunu çözmek için kaynakları birbirinden izole ederek, bir arızanın diğer bileşenlere yayılmasını engeller.
1. Bulkhead Pattern Nedir?
Bulkhead Pattern, adını gemi mühendisliğinden alır. Bir geminin gövdesi, su geçirmez bölmelere (bulkhead) ayrılmıştır. Bir bölme su alırsa, diğer bölmeler kuru kalır ve gemi batmaz. Yazılım dünyasında bu desen, kaynakları (thread, connection, memory) izole ederek bir bileşendeki arızanın diğerlerine yayılmasını önler.
Temel Fikir: Her servis veya işlem, kendi kaynak havuzuna sahip olmalıdır. Bir havuz tükendiğinde, diğer havuzlar etkilenmez.
2. Bulkhead Türleri
| Tür | Açıklama | Kullanım Alanı |
|---|---|---|
| Thread Pool Bulkhead | Her servis/operasyon için ayrı thread havuzu | Farklı downstream servis çağrıları |
| Semaphore Bulkhead | Eşzamanlı istek sayısını sınırlar (thread oluşturmaz) | Hafif, hızlı işlemler |
| Connection Pool Bulkhead | Her servis için ayrı veritabanı bağlantı havuzu | Çoklu veritabanı erişimi |
| Process/Container Bulkhead | Her servisi ayrı container'da çalıştırma | Mikroservis izolasyonu |
| Tenant Bulkhead | Her kiracı için ayrı kaynak havuzu | Multi-tenant SaaS |
3. Thread Pool vs. Semaphore Bulkhead
Thread Pool Bulkhead:
-
Her işlem için ayrı bir thread havuzu oluşturur.
-
Yavaş bir servis, kendi thread havuzunu tüketir; diğer servislerin thread'lerini etkilemez.
-
Asenkron işlemler için idealdir.
-
Dezavantaj: Thread oluşturma maliyeti, context switching yükü.
Semaphore Bulkhead:
-
Eşzamanlı istek sayısını bir semaphore ile sınırlar.
-
Thread oluşturmaz; mevcut thread'leri kullanır.
-
Hafif ve hızlıdır.
-
Dezavantaj: Thread havuzunu bloke edebilir (senkron çağrılarda).
4. .NET'te Polly ile Bulkhead Uygulaması
Polly, .NET ekosisteminde Bulkhead Pattern'ı uygulamak için en yaygın kullanılan kütüphanedir.
A. Semaphore Bulkhead (Polly)
csharp
using Polly;
using Polly.Bulkhead;
// Semaphore Bulkhead: Aynı anda en fazla 10 eşzamanlı istek, kuyrukta 20 bekleyebilir
var bulkheadPolicy = Policy.BulkheadAsync(
maxParallelization: 10,
maxQueuingActions: 20,
onBulkheadRejectedAsync: (context) =>
{
Console.WriteLine("Bulkhead dolu, istek reddedildi.");
return Task.CompletedTask;
});
// Kullanım
try
{
var response = await bulkheadPolicy.ExecuteAsync(async () =>
{
return await _httpClient.GetAsync("https://api.example.com/data");
});
}
catch (BulkheadRejectedException ex)
{
// İstek reddedildi - kullanıcıya anlamlı mesaj dön
return StatusCode(503, "Servis şu anda yoğun, lütfen daha sonra tekrar deneyin.");
}
B. Her Servis İçin Ayrı Bulkhead
csharp
public class ServiceClient
{
private readonly AsyncBulkheadPolicy _orderServiceBulkhead;
private readonly AsyncBulkheadPolicy _paymentServiceBulkhead;
private readonly AsyncBulkheadPolicy _inventoryServiceBulkhead;
public ServiceClient()
{
// Her servis için ayrı bulkhead - biri tükenirse diğerleri etkilenmez
_orderServiceBulkhead = Policy.BulkheadAsync(10, 20);
_paymentServiceBulkhead = Policy.BulkheadAsync(5, 10);
_inventoryServiceBulkhead = Policy.BulkheadAsync(15, 30);
}
public async Task<Order> GetOrderAsync(int id)
{
return await _orderServiceBulkhead.ExecuteAsync(async () =>
await _httpClient.GetFromJsonAsync<Order>($"/api/orders/{id}"));
}
public async Task<Payment> ProcessPaymentAsync(PaymentRequest request)
{
return await _paymentServiceBulkhead.ExecuteAsync(async () =>
await _httpClient.PostAsJsonAsync("/api/payments", request));
}
}
C. Polly v8 ile Bulkhead (Resilience Pipeline)
Polly v8, yeni Resilience Pipeline API'si ile daha temiz bir yapılandırma sunar.
csharp
using Polly;
using Polly.Bulkhead;
var pipeline = new ResiliencePipelineBuilder()
.AddBulkhead(new BulkheadStrategyOptions
{
MaxParallelization = 10,
MaxQueuingActions = 20,
ShouldHandle = new PredicateBuilder().Handle<HttpRequestException>(),
OnBulkheadRejected = args =>
{
Console.WriteLine("Bulkhead reddetti!");
return default;
}
})
.AddTimeout(TimeSpan.FromSeconds(5))
.AddRetry(new RetryStrategyOptions
{
MaxRetryAttempts = 3,
Delay = TimeSpan.FromSeconds(1)
})
.Build();
var result = await pipeline.ExecuteAsync(async token =>
{
return await _httpClient.GetStringAsync("https://api.example.com/data", token);
});
5. HTTP Client Factory ile Bulkhead Entegrasyonu
.NET'in IHttpClientFactory yapısı ile her downstream servis için ayrı HttpClient ve bulkhead tanımlanabilir.
csharp
// Program.cs
builder.Services.AddHttpClient("OrderService", client =>
{
client.BaseAddress = new Uri("https://orders.example.com");
})
.AddResilienceHandler("order-bulkhead", builder =>
{
builder.AddBulkhead(new BulkheadStrategyOptions
{
MaxParallelization = 20,
MaxQueuingActions = 50
});
builder.AddTimeout(TimeSpan.FromSeconds(10));
});
builder.Services.AddHttpClient("PaymentService", client =>
{
client.BaseAddress = new Uri("https://payments.example.com");
})
.AddResilienceHandler("payment-bulkhead", builder =>
{
// Ödeme servisi daha kritik - daha düşük paralellik, daha uzun timeout
builder.AddBulkhead(new BulkheadStrategyOptions
{
MaxParallelization = 5,
MaxQueuingActions = 10
});
builder.AddTimeout(TimeSpan.FromSeconds(30));
});
6. Kubernetes ve Container Seviyesinde Bulkhead
Bulkhead Pattern sadece kod seviyesinde değil, altyapı seviyesinde de uygulanabilir.
A. Kaynak Limitleri (Resource Limits)
Her pod için CPU ve bellek limitleri tanımlayarak, bir pod'un tüm node kaynaklarını tüketmesini engelleyin.
yaml
apiVersion: v1
kind: Pod
metadata:
name: order-service
spec:
containers:
- name: order-service
image: myregistry/order-service:latest
resources:
requests:
memory: "256Mi"
cpu: "250m"
limits:
memory: "512Mi"
cpu: "500m" # Bu pod en fazla 500m CPU kullanabilir
B. Ayrı Node Pool'lar
Kritik servisleri ayrı node pool'larda çalıştırarak fiziksel izolasyon sağlayın.
yaml
# Kritik servisler için ayrı node pool nodeSelector: node-pool: critical-services
C. Network Policy ile İzolasyon
Servisler arası ağ trafiğini kısıtlayarak, bir servisin diğerlerini etkilemesini önleyin.
yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: order-service-policy
spec:
podSelector:
matchLabels:
app: order-service
policyTypes:
- Ingress
- Egress
ingress:
- from:
- podSelector:
matchLabels:
app: api-gateway
7. Bulkhead vs. Circuit Breaker vs. Rate Limiting
Bu üç desen sıklıkla karıştırılır. Farkları:
| Desen | Amaç | Mekanizma |
|---|---|---|
| Bulkhead | Kaynak izolasyonu | Her servis için ayrı havuz |
| Circuit Breaker | Hatalı servisi geçici olarak devre dışı bırakma | Hata eşiği aşıldığında devreyi aç |
| Rate Limiting | Aşırı kullanımı önleme | Zaman biriminde istek sayısını sınırla |
Birlikte Kullanım: En iyi dayanıklılık stratejisi, üçünü birlikte kullanmaktır.
csharp
var pipeline = new ResiliencePipelineBuilder()
.AddRateLimiter(new SlidingWindowRateLimiter(...)) // 1. Rate Limiting
.AddBulkhead(new BulkheadStrategyOptions { ... }) // 2. Bulkhead
.AddCircuitBreaker(new CircuitBreakerStrategyOptions { ... }) // 3. Circuit Breaker
.AddRetry(new RetryStrategyOptions { ... }) // 4. Retry
.AddTimeout(TimeSpan.FromSeconds(10)) // 5. Timeout
.Build();
8. Multi-Tenant SaaS'da Bulkhead
Çok kiracılı (multi-tenant) SaaS uygulamalarında, bir kiracının aşırı kullanımı diğerlerini etkilememelidir.
csharp
public class TenantBulkheadManager
{
private readonly ConcurrentDictionary<string, ResiliencePipeline> _pipelines = new();
public ResiliencePipeline GetPipelineForTenant(string tenantId)
{
return _pipelines.GetOrAdd(tenantId, _ =>
{
return new ResiliencePipelineBuilder()
.AddBulkhead(new BulkheadStrategyOptions
{
MaxParallelization = 10, // Her kiracı en fazla 10 eşzamanlı istek
MaxQueuingActions = 20
})
.AddTimeout(TimeSpan.FromSeconds(30))
.Build();
});
}
}
// Kullanım
var pipeline = _bulkheadManager.GetPipelineForTenant(tenantId);
var result = await pipeline.ExecuteAsync(async token =>
{
return await _service.ProcessAsync(token);
});
9. En İyi Pratikler
| Pratik | Açıklama |
|---|---|
| Kritiklik Seviyesine Göre Ayırın | Kritik servisler (ödeme) için daha küçük, daha korumalı havuzlar. |
| Her Downstream Servis İçin Ayrı Havuz | Bir servisin yavaşlaması diğerlerini etkilemesin. |
| Doğru Boyutlandırma | Havuz boyutunu, servis kapasitesine ve beklenen yüke göre belirleyin. |
| Reddedilen İstekleri Yönetin | BulkheadRejectedException durumunda kullanıcıya anlamlı mesaj dönün (503). |
| Monitoring ve Alerting | Bulkhead reddetme oranlarını izleyin; yüksekse havuz boyutunu gözden geçirin. |
| Circuit Breaker ile Birleştirin | Bulkhead tükendiğinde, circuit breaker ile geçici olarak devreyi açın. |
| Kubernetes Limitleri Kullanın | Container seviyesinde CPU/bellek limitleri ile fiziksel izolasyon sağlayın. |
| Test Edin | Yük testleri ile bulkhead davranışını doğrulayın; reddetme senaryolarını test edin. |
10. Gerçek Dünya Senaryosu
Senaryo: Bir e-ticaret uygulamasında, ürün listeleme sayfası üç servisten veri çekiyor: Ürün Servisi, Stok Servisi ve Yorum Servisi.
Problem: Yorum Servisi yavaşladığında, tüm thread havuzu tükeniyor ve ürün listeleme sayfası çöküyor.
Çözüm: Her servis için ayrı bulkhead tanımlayın.
csharp
public class ProductPageComposer
{
private readonly ResiliencePipeline _productPipeline;
private readonly ResiliencePipeline _stockPipeline;
private readonly ResiliencePipeline _reviewPipeline;
public ProductPageComposer()
{
_productPipeline = CreateBulkhead(20, 50); // Ürün servisi kritik
_stockPipeline = CreateBulkhead(10, 20); // Stok servisi orta
_reviewPipeline = CreateBulkhead(5, 10); // Yorum servisi kritik değil
}
public async Task<ProductPageDto> ComposeAsync(int productId)
{
var productTask = _productPipeline.ExecuteAsync(async token =>
await _productService.GetAsync(productId, token));
var stockTask = _stockPipeline.ExecuteAsync(async token =>
await _stockService.GetAsync(productId, token));
// Yorum servisi başarısız olursa sayfayı bozmasın
var reviewTask = _reviewPipeline.ExecuteAsync(async token =>
await _reviewService.GetAsync(productId, token))
.AsTask()
.ContinueWith(t => t.IsFaulted ? new List<Review>() : t.Result);
await Task.WhenAll(productTask, stockTask, reviewTask);
return new ProductPageDto
{
Product = await productTask,
Stock = await stockTask,
Reviews = await reviewTask
};
}
}
Bu sayede Yorum Servisi yavaşlasa bile, ürün ve stok bilgileri sorunsuz görüntülenir; yorumlar boş döner.
Sonuç
Bulkhead Pattern, mikroservis mimarisinde dayanıklılığın temel yapı taşlarından biridir. Kaynakları izole ederek, bir bileşendeki arızanın tüm sistemi çökertmesini engeller. Thread pool ve semaphore tabanlı bulkhead'ler kod seviyesinde; resource limits ve network policy ise altyapı seviyesinde izolasyon sağlar. Polly, .NET ekosisteminde bulkhead uygulamak için olgun ve güçlü bir kütüphanedir. Circuit Breaker, Rate Limiting ve Retry ile birlikte kullanıldığında, sisteminiz dalgalanmalara ve arızalara karşı çok daha dirençli hale gelir. Unutmayın: Güçlü bir zincir, en zayıf halkası kadar güçlüdür; bulkhead, her halkayı ayrı ayrı güçlendirmenizi sağlar.