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
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):
- Circuit state değişiklikleri - OPEN’da hemen alert
- Reset attempt sonuçları - Failed reset’ler = devam eden problem
- Request rejection rate - Business impact metriği
- 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#
- Circuit Breaker - Martin Fowler (yeni sekmede açılır) - Kapalı, açık ve yarı açık üç durumu ile hata eşiklerini içeren circuit breaker pattern’in kanonik tanımı
- Circuit Breaker Pattern - Azure Architecture Center (yeni sekmede açılır) - Durum geçişleri, uygulama kaygıları ve retry mantığıyla entegrasyonu kapsayan Microsoft’un üretime yönelik kılavuzu
- opossum - Node.js Circuit Breaker (yeni sekmede açılır) - Node.js için en yaygın kullanılan circuit breaker uygulaması olan opossum kütüphanesinin resmi dokümantasyonu
- opossum - npm (yeni sekmede açılır) - Kurulum talimatları, API özeti ve haftalık indirme istatistikleri içeren npm paket sayfası
- Mikroservisler için Tasarım Pattern’leri - Azure Architecture Center (yeni sekmede açılır) - Bulkhead ve retry dahil circuit breaker’ı tamamlayan daha geniş dayanıklılık pattern’leri katalogu
İlgili yazılar
Mimari ağırlığını runtime'ın init-amortismanına göre seç: single-purpose Lambda'da yalın handler, Lambdalith'te orta, tam OOP/DI yalnızca uzun ömürlü runtime'da.
architecture · lambda · serverless +3
SOLID prensiplerinin modern JavaScript'te uygulanışı: TypeScript, React hooks ve fonksiyonel pattern'lerle pratik örnekler, ayrıca ne zaman gereksiz.
typescript · javascript · react +4
Effect'i adım adım öğrenmek ve AWS Lambda ile entegre etmek için pratik bir rehber: gerçek kod örnekleri, yaygın hatalar ve üretim desenleri.
typescript · functional-programming · lambda +4
Kural tabanlı chatbot'lardan otonom AI agent'larına mimari evrim: ReAct, Plan-and-Execute ve çoklu-agent desenleri TypeScript örnekleriyle anlatılıyor.
ai-agents · llm · architecture +2
Singleton, Factory, Builder ve Prototype pattern'lerinin TypeScript'te evrimi: ES modülleri singleton'ı ne zaman, factory function'lar class'ı ne zaman geçer.
typescript · design-patterns · architecture +1