İçeriğe atla

Circuit Breaker Pattern: Zincirleme Hataları Önleyen Dayanıklı Mikroservisler

TypeScript ile Circuit Breaker pattern implementasyonu: üç state, P99'a göre timeout ayarı ve bağımlılık tipine göre eşik değerleri

Ayhan Sipahi Ayhan Sipahi

Yavaş bir bağımlılık, çökmüş olandan daha tehlikelidir: 30 saniye timeout bekleyen request’ler thread pool’ları tüketir ve hatayı sağlıklı servisler üzerinden upstream’e yayar. Bir kapsama mekanizması olmadan, tek bir degraded downstream çağrısı tüm dağıtık sistemi saniyeler içinde doyurabilir. Circuit Breaker pattern’i bu hatayı üç state ve iki sayıyla kapsama alır: request timeout’u ve breaker’ı trip eden error rate. En kritik olanı timeout: client kütüphanesinin varsayılan değeri yerine P99 latency’nizin iki-üç katı. Böyle ayarlandığında breaker, thread pool tükenmeden önce trip eder.

Problem: Yavaş Yanıt Veren Bağımlılıkların Bedeli#

Bir payment provider’ın API’sinin yavaş cevap vermeye başladığını düşünün. Down değil, sadece normal 200ms yerine request başına 20-30 saniye alıyor. Çağıran servis bekler ve gelen request’ler arkada birikir; thread pool tükenir, memory tırmanır. Sağlıklı servis sağlıksız hale gelir ve bozulma ona bağımlı her çağırana doğru upstream’e yayılır.

Bu hatayı fark etmek zordur çünkü monitoring yeşil kalır. Health check’ler geçer, dashboard’lar her servisi “up” gösterir. Tek belirti, çağıranların timeout’a düşmesidir.

Circuit Breaker: Sisteminizin Güvenlik Valfi#

Circuit Breaker pattern evinizdeki elektrik sigortası gibi çalışır. İşler ters gittiğinde atar, hasarın yayılmasını önler. Elektrik panosundakinden farklı olarak bu kendini sıfırlar: problemin düzelip düzelmediğini belirli aralıklarla test eder ve düzeldiyse trafiği geri açar.

Üç State#

enum CircuitState {
  CLOSED = 'CLOSED',  // Normal operasyon, request'ler akıyor
  OPEN = 'OPEN',  // Circuit atmış, request'ler hemen fail ediyor
  HALF_OPEN = 'HALF_OPEN' // Servis recover olmuş mu test ediyor
}

Bir kulüpteki bouncer gibi düşünün:

  • CLOSED: “Gelin, her şey yolunda”
  • OPEN: “Kimse giremiyor, içeride problem var”
  • HALF_OPEN: “Tek kişiyi alıp içerisi güvenli mi bakayım”

TypeScript Implementasyonu#

Aşağıdaki sınıf, failure’ları bir rolling window’da tutar ve birbirinden bağımsız iki trip koşulu uygular: mutlak failure sayısı ve minimum request hacmi üzerindeki error rate.

interface CircuitBreakerConfig {
  failureThreshold: number;  // Açılmadan önceki failure sayısı
  successThreshold: number;  // Half-open'dan kapanmak için success sayısı
  timeout: number;  // Request timeout ms cinsinden
  resetTimeout: number;  // Half-open denemeden önce bekleme
  volumeThreshold: number;  // Değerlendirmeden önce min request
  errorThresholdPercentage: number; // Trip için error yüzdesi
}

class CircuitBreaker {
  private state: CircuitState = CircuitState.CLOSED;
  private failureCount = 0;
  private successCount = 0;
  private lastFailureTime?: Date;
  private window = new RollingWindow(10000); // 10 saniyelik window

  constructor(private readonly config: CircuitBreakerConfig) {}

  getState(): CircuitState {
    return this.state;
  }

  async execute<T>(protectedFunction: () => Promise<T>): Promise<T> {
    // Half-open denemeli miyiz kontrol et
    if (this.state === CircuitState.OPEN) {
      if (this.shouldAttemptReset()) {
        this.state = CircuitState.HALF_OPEN;
      } else {
        throw new CircuitOpenError('Circuit breaker OPEN');
      }
    }

    try {
      const result = await this.executeWithTimeout(protectedFunction);
      this.onSuccess();
      return result;
    } catch (error) {
      this.onFailure();
      throw error;
    }
  }

  private async executeWithTimeout<T>(fn: () => Promise<T>): Promise<T> {
    return Promise.race([
      fn(),
      new Promise<T>((_, reject) =>
        setTimeout(() => reject(new TimeoutError()), this.config.timeout)
      )
    ]);
  }

  private onSuccess(): void {
    this.failureCount = 0;
    this.window.recordSuccess();

    if (this.state === CircuitState.HALF_OPEN) {
      this.successCount++;
      if (this.successCount >= this.config.successThreshold) {
        this.state = CircuitState.CLOSED;
        this.successCount = 0;
      }
    }
  }

  private onFailure(): void {
    this.failureCount++;
    this.lastFailureTime = new Date();
    this.window.recordFailure();

    if (this.state === CircuitState.HALF_OPEN) {
      this.state = CircuitState.OPEN;
      this.successCount = 0;
      return;
    }

    // Hem absolute hem percentage threshold'ları kontrol et
    const stats = this.window.getStats();
    if (stats.totalRequests >= this.config.volumeThreshold) {
      const errorRate = (stats.failures / stats.totalRequests) * 100;
      if (errorRate >= this.config.errorThresholdPercentage ||
          this.failureCount >= this.config.failureThreshold) {
        this.state = CircuitState.OPEN;
      }
    }
  }

  private shouldAttemptReset(): boolean {
    if (!this.lastFailureTime) return false;
    return Date.now() - this.lastFailureTime.getTime() >= this.config.resetTimeout;
  }
}

Production’dan Dersler#

1. Timeout En Önemli Setting’iniz#

Yavaş response’lar sert hatalardan daha çok zarar verir ve onları yakalayan tek ayar timeout’tur. Reddedilen bir bağlantı milisaniyeler içinde hata döner. Asılı kalan bir request ise başka bir şey pes edene kadar thread’ini tutar; breaker’ın var olma sebebi tam olarak bu durumdur.

const config = {
  timeout: 3000,  // 3 saniye, 1.2s'lik P99'a göre ayarlandı
  // NOT 30000!  // 30s beklemek, breaker tepki veremeden thread pool'u tüketir
};

Bir payment API’si için örnek hesap:

  • Normal P50: 180ms
  • Normal P99: 1.2s
  • Circuit breaker timeout: 3s (yaklaşık P99’un 2,5 katı)

2. Half-Open State Tuzağı#

Yaygın bir tuzak: half-open’a geçer, bir request gönderir, başarılı olur, circuit’i kapatır, sonra full traffic ile hemen tekrar fail eder. Çözüm: kapanmadan önce birden fazla success iste.

// Bunu yapma
if (testRequest.succeeded) {
  this.state = CircuitState.CLOSED; // Boom! Full traffic geri geliyor
}

// Bunun yerine bunu yap
if (++this.successCount >= this.config.successThreshold) {
  this.state = CircuitState.CLOSED; // Kademeli recovery
}

3. Retry Logic ile Kombine Et (Ama Dikkatli)#

Circuit breaker’lar ve retry’lar feedback loop yaratabilir. Güvenilir bir kombinasyon:

class ResilientClient {
  private readonly circuitBreaker = new CircuitBreaker(clientConfig);

  async callWithResilience(request: Request): Promise<Response> {
    // Circuit breaker retry logic'i wrap eder, tersi değil
    return this.circuitBreaker.execute(async () => {
      return await this.retryWithBackoff(request, {
        maxAttempts: 3,
        backoffMs: [100, 200, 400],
        shouldRetry: (error) => {
          // Circuit breaker error'larını retry etme
          if (error instanceof CircuitOpenError) return false;
          // Client error'larını retry etme
          if (error.statusCode >= 400 && error.statusCode < 500) return false;
          return true;
        }
      });
    });
  }
}

4. Doğru Metrikleri Monitor Et#

Neyi track etmeli (önem sırasına göre):

  1. Circuit state değişiklikleri - OPEN’da hemen alert
  2. Reset attempt sonuçları - Failed reset’ler = devam eden problem
  3. Request rejection rate - Business impact metriği
  4. OPEN state’te geçen süre - Reset timeout’u ayarlamaya yardımcı

Örnek CloudWatch metrikleri:

// Her state değişiminde gönderilen custom metrikler (AWS SDK v3)
await cloudwatch.send(new PutMetricDataCommand({
  Namespace: 'CircuitBreakers',
  MetricData: [
    {
      MetricName: 'StateChange',
      Value: 1,
      Unit: 'Count',
      Dimensions: [
        { Name: 'ServiceName', Value: this.serviceName },
        { Name: 'FromState', Value: oldState },
        { Name: 'ToState', Value: newState }
      ]
    },
    {
      MetricName: 'RejectedRequests',
      Value: rejectedCount,
      Unit: 'Count',
      Dimensions: [{ Name: 'ServiceName', Value: this.serviceName }]
    }
  ]
}));

İleri Seviye Pattern’ler: Temel Breaker’ın Ötesi#

Bulkheading: İzole Circuit Breaker’lar#

Tüm servis için tek circuit breaker kullanma. Kritik path’leri izole et:

class PaymentService {
  private readonly chargeBreaker = new CircuitBreaker(chargeConfig);
  private readonly refundBreaker = new CircuitBreaker(refundConfig);
  private readonly queryBreaker = new CircuitBreaker(queryConfig);

  async chargeCard(request: ChargeRequest): Promise<ChargeResponse> {
    // Charge failure'ları refund'ları etkilemiyor
    return this.chargeBreaker.execute(() => this.api.charge(request));
  }

  async refundPayment(request: RefundRequest): Promise<RefundResponse> {
    // Charge'lar fail ederken bile refund'lar çalışıyor
    return this.refundBreaker.execute(() => this.api.refund(request));
  }
}

Bu pattern yoğun trafik dönemlerinde bir endpoint overwhelm olduğunda diğerlerinin çalışmaya devam etmesini sağlar.

Fallback Stratejileri#

Her failure eşit değil. Bazen graceful degrade edebilirsin:

async getProductRecommendations(userId: string): Promise<Product[]> {
  try {
    return await this.recommendationBreaker.execute(
      () => this.mlService.getRecommendations(userId)
    );
  } catch (error) {
    if (error instanceof CircuitOpenError) {
      // Basit popularity-based recommendation'lara fallback
      return this.getPopularProducts();
    }
    throw error;
  }
}

Circuit Breaker Inheritance#

Mikroservisler başka mikroservisleri çağırırken, circuit state’i inherit et:

// API Gateway
if (paymentServiceBreaker.getState() === CircuitState.OPEN) {
  // Payment'a bağımlı order service'i çağırmayı deneme bile
  return { error: 'Payment service unavailable', status: 503 };
}

Bağımlılık Tipine Göre Konfigürasyon Örnekleri#

Sık karşılaşılan üç bağımlılık sınıfı için başlangıç değerleri. Sayıları olduğu gibi kopyalamak yerine kendi latency profilinize göre ayarlayın:

// External API (payment provider'lar, third-party servisler)
const externalAPIConfig: CircuitBreakerConfig = {
  failureThreshold: 5,  // 5 ardışık failure
  successThreshold: 2,  // Recovery için 2 success
  timeout: 5000,  // 5 saniye timeout
  resetTimeout: 30000,  // 30s sonra recovery dene
  volumeThreshold: 10,  // Minimum 10 request gerekli
  errorThresholdPercentage: 50  // 50% error rate trip eder
};

// Internal mikroservis
const internalServiceConfig: CircuitBreakerConfig = {
  failureThreshold: 10,  // Daha toleranslı
  successThreshold: 3,
  timeout: 3000,  // Daha hızlı timeout
  resetTimeout: 10000,  // Daha hızlı recovery attempt'leri
  volumeThreshold: 20,
  errorThresholdPercentage: 30  // Error rate'lere daha hassas
};

// Database connection'ları
const databaseConfig: CircuitBreakerConfig = {
  failureThreshold: 3,  // Hızlı trip
  successThreshold: 5,  // Yavaş recover
  timeout: 1000,  // Çok hızlı timeout
  resetTimeout: 5000,  // Hızlı retry
  volumeThreshold: 5,
  errorThresholdPercentage: 20  // Çok hassas
};

Circuit Breaker’ları Test Etmek: Chaos Engineering#

Test etmediğin circuit breaker’a güvenemezsin. Chaos testi, mock bir bağımlılığı kademeli bozulmadan geçirir ve breaker’ın doğru noktada trip ettiğini doğrular:

describe('Circuit Breaker Chaos Testleri', () => {
  it('kademeli degradation senaryosunu handle etmeli', async () => {
    const scenarios = [
      { latency: 100, errorRate: 0 },  // Normal
      { latency: 500, errorRate: 0.1 },  // Hafif degradation
      { latency: 2000, errorRate: 0.3 }, // Büyük degradation
      { latency: 5000, errorRate: 0.7 }, // Neredeyse failure
    ];

    for (const scenario of scenarios) {
      mockService.setScenario(scenario);
      await runLoadTest(1000); // 1000 request

      if (scenario.errorRate > 0.5) {
        expect(breaker.getState()).toBe(CircuitState.OPEN);
      }
    }
  });
});

Production’da AWS Fault Injection Simulator kullanarak random failure’lar inject edilip circuit breaker’ların doğru respond ettiği verify edilebilir.

Yaygın Hatalar ve Sonuçları#

Hata 1: Sadece Client-Side Circuit Breaking#

Circuit breaker’ları yalnızca client’larda implement etmek, server’ın downstream sorun yaşadığında kendini koruyamamasına neden olur:

// Kötü: Client kendini koruyor ama server hala overwhelm
class Client {
  private breaker = new CircuitBreaker(clientConfig);
  async call() { return this.breaker.execute(() => fetch('/api')); }
}

// İyi: Server da kendini koruyor
class Server {
  private downstreamBreaker = new CircuitBreaker(databaseConfig);
  async handleRequest(req, res) {
    try {
      const data = await this.downstreamBreaker.execute(() =>
        this.database.query(req.query)
      );
      res.json(data);
    } catch (error) {
      if (error instanceof CircuitOpenError) {
        res.status(503).json({ error: 'Servis geçici olarak kullanılamıyor' });
      }
    }
  }
}

Hata 2: İlgisiz Operasyonlar İçin Circuit Breaker Paylaşmak#

“Database operasyonları” için tek circuit breaker kullanmak, write’lar fail ettiğinde read’lerin de bloklanmasına yol açar:

// Kötü: Her şey için tek breaker
class UserService {
  private dbBreaker = new CircuitBreaker(databaseConfig);

  async getUser(id) {
    return this.dbBreaker.execute(() => db.query('SELECT...'));
  }

  async createUser(data) {
    return this.dbBreaker.execute(() => db.query('INSERT...'));
  }
}

// İyi: Farklı operasyonlar için ayrı breaker'lar
class UserService {
  private readBreaker = new CircuitBreaker(readConfig);
  private writeBreaker = new CircuitBreaker(writeConfig);

  async getUser(id) {
    return this.readBreaker.execute(() => db.query('SELECT...'));
  }

  async createUser(data) {
    return this.writeBreaker.execute(() => db.query('INSERT...'));
  }
}

Hata 3: Business Impact’i Düşünmemek#

Tüm servislere eşit davranmak, metrics collection gibi düşük öncelikli servisler geçerken payment processing’in bloklanması sonucunu doğurur. Business kritikliği circuit breaker konfigürasyonunu yönlendirmelidir.

Implementasyon Checklist’i#

Circuit breaker’ları implement ederken kullanışlı bir checklist:

  • Timeout’u P99 latency’nin 2-3 katına ayarla
  • Half-open’dan kapanmadan önce birden fazla success iste
  • Read/write operasyonları için ayrı breaker’lar implement et
  • Business-critical path’ler için fallback davranışı ekle
  • State değişiklikleri ve rejection’lar için metrik export et
  • Production’dan önce chaos engineering ile test et
  • Timeout ve threshold seçimlerini dokümante et
  • Individual failure’larda değil circuit OPEN’da alert et
  • Konfigürasyonda business priority’yi düşün
  • Instant değil gradual recovery implement et

Circuit Breaker’ı Ne Zaman Kullanmalı#

Breaker, senkron bir çağrı process sınırını geçtiğinde ve çağıran taraf beklerken kıt bir kaynağı elinde tuttuğunda yerini hak eder: bir thread, bir connection, bir Lambda invocation. Payment provider’lar, servisler arası çağrılar ve database pool’ları bu tanıma girer. Önce timeout’u, sonra eşikleri belirleyin; konfigürasyonun geri kalanı yalnızca bu iki sayıya ince ayar yapar.

Yanlış araç olduğu durumlar da var. Kuyruktan çekilen işlerde back-pressure zaten vardır; breaker koruma eklemeden state ekler. Tek çağıranı olan ve retry trafiği bulunmayan bir bağımlılık için sade bir timeout genelde daha iyi çalışır. Alakasız sebeplerle aralıklı hata veren bir bağımlılıkta ise breaker state’ler arasında gidip gelir ve bu, kazandırdığından fazla erişilebilirlik kaybettirir.

Kaynaklar#

İlgili yazılar