Mikroservis Mimarisi & Dayanıklılık 7 dk okuma 3 okunma

Bulkhead ile Kaynak Tükenmesini Önleme

Bulkhead Pattern, bir bileşendeki kaynak tükenmesinin (thread, connection, bellek) diğer bileşenlere yayılmasını engelleyen bir dayanıklılık desenidir. Gemi bölmelerinden ilham alan bu desen, her servis veya operasyon için ayrı kaynak havuzları oluşturur. Polly, thread pool ve semaphore tabanlı bulkhead'ler ile .NET'te uygulama stratejileri ele alınır.

Bulkhead ile Kaynak Tükenmesini Önleme

Bulkhead ile Kaynak Tükenmesini Önleme

Mikroservis mimarisinde en sinsi arıza türü, bir servisin çökmesi değil, yavaşlamasıdır. Yavaşlayan bir servis, kendisine ayrılan thread'leri, bağlantıları ve belleği tüketmeye başlar; bu kaynaklar tükendiğinde ise zincirleme bir çöküş (cascade failure) yaşanır. Tüm sistem, tek bir yavaş servis yüzünden kullanılamaz hale gelir. İşte Bulkhead Pattern (Bölme Deseni), bu sorunu çözmek için kaynakları birbirinden izole eder.


1. Kaynak Tükenmesi (Resource Exhaustion) Nedir?

Bir servis yavaşladığında veya çöktüğünde, ona bağlanan tüm istemciler beklemeye başlar. Bu bekleyen istekler, aşağıdaki kaynakları tüketir:

Kaynak Tükenme Sonucu
Thread Pool Yeni istekler işlenemez; tüm thread'ler bekliyor.
Connection Pool Veritabanı bağlantısı alınamaz; yeni sorgular çalışamaz.
Bellek Bekleyen istekler bellekte birikir; OOM (Out of Memory) hatası.
Socket/Port Yeni bağlantı kurulamaz; ağ hataları.
CPU Aşırı context switching; sistem yavaşlar.

Kritik Nokta: Bu kaynaklar paylaşımlıdır. Bir servis için ayrılan thread'ler tükenirse, diğer servisler de bu thread havuzunu kullanamaz. İşte Bulkhead, bu paylaşımı izole havuzlara bölerek sorunu çözer.


2. 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, her servis veya operasyon için ayrı kaynak havuzu oluşturarak, bir bileşendeki arızanın diğerlerine yayılmasını önler.

Temel Fikir: Her downstream servis çağrısı, kendi thread havuzuna veya bağlantı havuzuna sahip olmalıdır. Bir havuz tükendiğinde, diğer havuzlar etkilenmez.


3. Bulkhead Türleri

Tür Mekanizma Kullanım Alanı
Thread Pool Bulkhead Her operasyon için ayrı thread havuzu Asenkron, I/O-bound işlemler
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

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 (Temel)

csharp

using Polly;
using Polly.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 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

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 kaynak tükenmesini önlemenin en etkili yoludur. 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, Retry ve Timeout 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.

Tüm yazılar Soru ya da yorumun mu var? Yaz