Resiliente Systeme bauen: Patterns und Practices
Essenzielle Muster zum Bau von Systemen, die Fehler elegant behandeln - Circuit Breakers, Bulkheads, Retries, Timeouts und mehr, mit praktischen Implementierungsbeispielen.
Resiliente Systeme bauen: Patterns und Practices
"Everything fails, all the time" - Werner Vogels, CTO Amazon. Diese scheinbar pessimistische Aussage ist die wichtigste Erkenntnis für den Bau moderner verteilter Systeme. In meiner Arbeit mit dutzenden Organisationen habe ich ein wiederkehrendes Pattern beobachtet: Die meisten Systeme werden für den Happy-Path designed. Sie funktionieren hervorragend, solange alle Dependencies verfügbar sind, Netzwerke nicht verzögern, und externe Services antworten. Doch die Realität ist brutal anders.
Ein einziger fehlgeschlagener Microservice kann innerhalb von Minuten ein gesamtes System lahmlegen. Ein überlasteter Datenbankserver kann kaskadierend alle Application-Server blockieren. Ein langsames externes Payment-Gateway kann Ihren gesamten Checkout zum Stillstand bringen. Diese Failures sind nicht theoretische Edge-Cases – sie sind die tägliche Realität in Production-Systemen. Die Frage ist nicht ob Failures auftreten, sondern wie Ihr System damit umgeht.
Dieser Artikel zeigt die essenziellen Resilience-Patterns, die ich in robusten Production-Systemen implementiert habe, mit praktischen Code-Beispielen, Konfigurationsempfehlungen, und realen Fallstudien. Resiliente Systeme sind nicht solche, die nie fehlschlagen – sie sind solche, die mit Fehlern elegant umgehen, schnell recovern, und partielle Funktionalität aufrechterhalten, wenn Komponenten ausfallen.
Was ist Resilienz wirklich?
Definition und Abgrenzung
Resilienz ist die Fähigkeit eines Systems, bei Teilausfällen funktionsfähig zu bleiben und sich selbst zu heilen. Es ist wichtig, Resilienz von verwandten aber unterschiedlichen Konzepten abzugrenzen:
Robustheit versucht, Fehler zu vermeiden durch defensive Programmierung, Input-Validierung, und rigorose Testing. Robustheit sagt: "Ich verhindere, dass Fehler auftreten." Resilienz sagt: "Wenn Fehler auftreten – und sie werden – behandle ich sie elegant."
Verfügbarkeit (Availability) misst den Prozentsatz der Zeit, in der ein System operational ist. 99.9% Verfügbarkeit bedeutet 8.76 Stunden Downtime pro Jahr. Hohe Verfügbarkeit wird oft durch Redundanz erreicht (Load-Balanced-Instances, Multi-Region-Deployments). Resilienz geht weiter: Selbst wenn eine Komponente ausfällt, degradiert das System gracefully statt komplett auszufallen.
Fault Tolerance erreicht Verfügbarkeit durch Redundanz: Wenn Server A ausfällt, übernimmt Server B. Resilienz umfasst Fault Tolerance, aber auch: Was passiert während des Failovers? Wie verhält sich das System bei partiellen Failures (einige Requests funktionieren, andere nicht)? Was wenn B auch ausfällt?
Resilienz umfasst:
- Graceful Degradation: Reduzierte Funktionalität statt totaler Ausfall
- Fail Fast: Schnell fehlschlagen ist besser als langsam timeout
- Self-Healing: Automatische Recovery ohne manuelle Intervention
- Chaos Tolerance: System funktioniert auch unter unvorhersehbaren Bedingungen
Die fundamentalen Prinzipien
1. Assume Failure
Design mit der Annahme, dass jede Komponente fehlschlagen kann und wird. Netzwerke sind unzuverlässig. Festplatten sterben. Datenbankverbindungen brechen ab. APIs sind zeitweise nicht verfügbar. Diese Annahme muss jede Architektur-Entscheidung informieren.
Praktische Implikation: Jeder externe Call braucht Timeout, Retry-Logik, Fallback-Strategie, und Circuit Breaking.
2. Fail Fast
Ein Request, der 30 Sekunden auf einen Timeout wartet, blockiert einen Thread für 30 Sekunden. Multipliziert über hunderte Requests führt das zu Resource-Exhaustion und System-Collapse. Besser: Fail innerhalb von 1-2 Sekunden, gib einen Fehler zurück oder nutze Fallback-Daten, und der Thread ist frei für andere Requests.
Praktische Implikation: Aggressive Timeouts (oft kürzer als man denkt), und sofortige Rejection wenn Ressourcen erschöpft sind.
3. Isolate Failures
Ein Fehler in Komponente A darf nicht Komponente B, C, und D mit runterreißen. Isolation verhindert kaskadierende Failures – das gefährlichste Pattern in verteilten Systemen.
Praktische Implikation: Bulkhead-Pattern, getrennte Thread-Pools, Circuit-Breakers, und Resource-Limits pro Dependency.
4. Degrade Gracefully
Partial functionality ist besser als total failure. Wenn das Recommendation-System ausfällt, sollte die Product-Page trotzdem anzeigbar sein (nur ohne Recommendations). Wenn Payment-Processing langsam ist, sollten Browse und Search weiter funktionieren.
Praktische Implikation: Identifiziere "critical path" vs. "nice to have" Features, und design Fallbacks für Non-Critical-Features.
Pattern 1: Circuit Breaker - Verhindere kaskadierende Ausfälle
Das Problem: Der kaskadierende Systemausfall
Stellen Sie sich vor: Ihr E-Commerce-System ruft einen externen Inventory-Service auf. Dieser Service hat ein Problem und antwortet sehr langsam (15 Sekunden statt 200ms). Ihr Application-Server hat 200 Threads. Jeder Request zu einer Product-Page ruft Inventory auf. Innerhalb von Minuten:
Minute 1: Inventory-Service antwortet langsam (15s)
├── Request 1-10: warten auf Inventory-Response
├── Request 11-20: warten auf Inventory-Response
└── Nach 1 Minute: 60 Requests warten, 60 Threads blockiert
Minute 2: Problem verschärft sich
├── 120 Threads blockiert (wartend auf Inventory)
├── 80 Threads verfügbar
└── Neue Requests: beginnen zu queuen
Minute 3: Totaler Ausfall
├── Alle 200 Threads blockiert
├── Neue Requests: 503 Service Unavailable
└── ENTIRE APPLICATION DOWN wegen eines langsamen Dependency
Dies ist ein klassischer kaskadierender Ausfall. Ein partieller Fehler (langsamer Inventory-Service) hat Ihr gesamtes System lahmgelegt.
Die Lösung: Circuit Breaker Pattern
Der Circuit Breaker funktioniert wie ein elektrischer Sicherungsschalter: Wenn zu viele Fehler detektiert werden, "öffnet" der Breaker und verhindert weitere Calls zu dem fehlerhaften Service. Dadurch:
- Werden Threads nicht mehr blockiert (Fail Fast)
- Hat der fehlerhafte Service Zeit zu recovern (weniger Last)
- Kann Ihre Application Fallback-Logik nutzen
Zustände und Übergänge:
┌─────────────┐
┌──→│ CLOSED │──┐ Fehler-Threshold
│ │ (Normal) │ │ überschritten
│ └─────────────┘ │
│ │ ↓
Success│ ┌─────────────┐
nach │ │ OPEN │
Half- │ │ (Blocking) │
Open │ └─────────────┘
│ │ Timeout abgelaufen
│ ↓
│ ┌─────────────┐
└───│ HALF_OPEN │
│ (Testing) │
└─────────────┘
CLOSED: Normaler Betrieb, alle Requests werden durchgelassen
├── Bei jedem fehlgeschlagenen Request: Increment failure-counter
└── Wenn failure-counter >= threshold: Übergang zu OPEN
OPEN: Circuit ist "geöffnet", alle Requests werden sofort rejected
├── Fail Fast: Keine Calls zum fehlerhaften Service
├── Fallback-Logik wird aktiviert
└── Nach timeout-period: Übergang zu HALF_OPEN
HALF_OPEN: Test-Zustand
├── Erlaubt einen Test-Request durch
├── Success: Übergang zu CLOSED (Service hat recovered)
└── Failure: Zurück zu OPEN (Service noch nicht bereit)
Implementierung in C#
public class CircuitBreaker
{
private readonly int _failureThreshold;
private readonly TimeSpan _openTimeout;
private readonly TimeSpan _halfOpenTimeout;
private int _failureCount;
private DateTime _lastFailureTime;
private CircuitBreakerState _state;
private readonly object _lock = new object();
public enum CircuitBreakerState
{
Closed,
Open,
HalfOpen
}
public CircuitBreaker(
int failureThreshold = 5,
TimeSpan? openTimeout = null,
TimeSpan? halfOpenTimeout = null)
{
_failureThreshold = failureThreshold;
_openTimeout = openTimeout ?? TimeSpan.FromSeconds(60);
_halfOpenTimeout = halfOpenTimeout ?? TimeSpan.FromSeconds(30);
_state = CircuitBreakerState.Closed;
_failureCount = 0;
}
public async Task<T> ExecuteAsync<T>(Func<Task<T>> operation)
{
lock (_lock)
{
if (_state == CircuitBreakerState.Open)
{
if (DateTime.UtcNow - _lastFailureTime > _openTimeout)
{
_state = CircuitBreakerState.HalfOpen;
}
else
{
throw new CircuitBreakerOpenException(
$"Circuit breaker is OPEN. Service unavailable. " +
$"Will retry after {_openTimeout.TotalSeconds}s");
}
}
}
try
{
var result = await operation();
// Success
lock (_lock)
{
if (_state == CircuitBreakerState.HalfOpen)
{
_state = CircuitBreakerState.Closed;
}
_failureCount = 0;
}
return result;
}
catch (Exception ex)
{
lock (_lock)
{
_failureCount++;
_lastFailureTime = DateTime.UtcNow;
if (_failureCount >= _failureThreshold)
{
_state = CircuitBreakerState.Open;
}
if (_state == CircuitBreakerState.HalfOpen)
{
_state = CircuitBreakerState.Open;
}
}
throw;
}
}
}
// Usage Example:
public class InventoryService
{
private readonly HttpClient _httpClient;
private readonly CircuitBreaker _circuitBreaker;
public InventoryService(HttpClient httpClient)
{
_httpClient = httpClient;
_circuitBreaker = new CircuitBreaker(
failureThreshold: 5,
openTimeout: TimeSpan.FromMinutes(1),
halfOpenTimeout: TimeSpan.FromSeconds(30)
);
}
public async Task<InventoryStatus> CheckInventory(string productId)
{
try
{
return await _circuitBreaker.ExecuteAsync(async () =>
{
var response = await _httpClient.GetAsync(
$"https://inventory-api.example.com/products/{productId}",
new CancellationTokenSource(TimeSpan.FromSeconds(2)).Token
);
response.EnsureSuccessStatusCode();
return await response.Content.ReadAsAsync<InventoryStatus>();
});
}
catch (CircuitBreakerOpenException)
{
// Fallback: Assume available (or use cached data)
return new InventoryStatus { Available = true, Quantity = 0 };
}
}
}
Konfigurations-Guidelines
Die richtigen Threshold- und Timeout-Werte hängen von der Kritikalität und den Charakteristiken des Dependency ab:
Critical Service (Payment, Auth):
├── Failure Threshold: 10 failures
│ (Höher, weil false-positives sehr teuer sind)
├── Open Timeout: 60-120 seconds
│ (Genug Zeit für Recovery, aber nicht zu lange down)
└── Half-Open Timeout: 30 seconds
Normal Service (Catalog, Search):
├── Failure Threshold: 5 failures
├── Open Timeout: 60 seconds
└── Half-Open Timeout: 20 seconds
Non-Critical Service (Recommendations, Analytics):
├── Failure Threshold: 3 failures
│ (Schnell öffnen, Impact ist gering)
├── Open Timeout: 30 seconds
│ (Kurz, weil nicht kritisch)
└── Half-Open Timeout: 10 seconds
Pattern 2: Bulkhead - Isoliere Ressourcen
Das Problem: Thread Pool Exhaustion
Ein einzelner fehlerhafter Endpoint kann Ihr gesamtes Thread-Pool monopolisieren:
Application Thread Pool: 200 Threads total
Szenario: Slow external API
├── Endpoint /recommendations calls slow API (15s response)
├── High traffic: 100 requests/minute zu /recommendations
├── Result: 25 Threads permanent blockiert (wartend auf API)
Impact auf andere Endpoints:
├── /products: 175 Threads verfügbar (okay)
├── /cart: 175 Threads verfügbar (okay)
└── Wenn /recommendations Traffic steigt:
├── 100 Threads blockiert
├── 100 Threads für ALLE anderen Endpoints
└── Gesamtes System verlangsamt sich
Die Lösung: Bulkhead Isolation
Inspiriert von Schiffsbau-Technik: Bulkheads sind Trennwände, die verhindern, dass Wasser das gesamte Schiff flutet. In Software: Dedizierte Ressourcen-Pools pro Dependency verhindern, dass ein fehlerhafter Service alle Ressourcen monopolisiert.
Application: 200 Threads total, aufgeteilt:
├── Payment Service Pool: 20 Threads
│ └── Nur für Payment-Calls, isoliert
├── Inventory Service Pool: 30 Threads
│ └── Nur für Inventory-Calls, isoliert
├── Recommendation Service Pool: 20 Threads
│ └── Nur für Recommendation-Calls, isoliert
├── External API Pool: 30 Threads
│ └── Für diverse externe APIs
└── General Pool: 100 Threads
└── Für interne Operationen
Wenn Recommendation Service fehlschlägt:
├── Recommendation Pool: 20 Threads blockiert
└── Payment, Inventory, General: Unaffected, funktionieren weiter
Implementierung in C#
public class Bulkhead
{
private readonly SemaphoreSlim _semaphore;
private readonly int _maxConcurrent;
public Bulkhead(int maxConcurrent)
{
_maxConcurrent = maxConcurrent;
_semaphore = new SemaphoreSlim(maxConcurrent, maxConcurrent);
}
public async Task<T> ExecuteAsync<T>(
Func<Task<T>> operation,
TimeSpan? timeout = null)
{
var actualTimeout = timeout ?? TimeSpan.FromSeconds(30);
// Try to acquire semaphore (non-blocking check)
if (!await _semaphore.WaitAsync(0))
{
throw new BulkheadRejectedException(
$"Bulkhead full. Max concurrent: {_maxConcurrent}");
}
try
{
using var cts = new CancellationTokenSource(actualTimeout);
return await operation();
}
finally
{
_semaphore.Release();
}
}
}
// Service-specific Bulkheads:
public class ResilientServiceFactory
{
private readonly Dictionary<string, Bulkhead> _bulkheads;
public ResilientServiceFactory()
{
_bulkheads = new Dictionary<string, Bulkhead>
{
["payment"] = new Bulkhead(maxConcurrent: 20),
["inventory"] = new Bulkhead(maxConcurrent: 30),
["recommendations"] = new Bulkhead(maxConcurrent: 20),
["notifications"] = new Bulkhead(maxConcurrent: 50),
["external-api"] = new Bulkhead(maxConcurrent: 30)
};
}
public async Task<T> ExecuteWithBulkhead<T>(
string serviceName,
Func<Task<T>> operation)
{
if (!_bulkheads.TryGetValue(serviceName, out var bulkhead))
{
throw new ArgumentException($"Unknown service: {serviceName}");
}
try
{
return await bulkhead.ExecuteAsync(operation);
}
catch (BulkheadRejectedException)
{
// Log metric, alert if sustained
throw new ServiceOverloadedException(
$"{serviceName} service overloaded. Try again later.");
}
}
}
// Usage:
public class PaymentController : ControllerBase
{
private readonly ResilientServiceFactory _serviceFactory;
[HttpPost("process")]
public async Task<IActionResult> ProcessPayment(PaymentRequest request)
{
try
{
var result = await _serviceFactory.ExecuteWithBulkhead(
"payment",
async () => await _paymentService.Charge(request)
);
return Ok(result);
}
catch (ServiceOverloadedException ex)
{
return StatusCode(503, new { error = ex.Message });
}
}
}
Pattern 3: Retry with Exponential Backoff - Handle Transient Failures
Das Problem: Transient vs. Persistent Failures
Nicht alle Failures sind gleich. Transient Failures sind temporär und selbstheilend (Netzwerk-Glitch, kurze Datenbank-Überlastung, Race-Condition). Persistent Failures sind strukturell (Service ist down, Code-Bug, falsche Credentials).
Sofortiger Retry bei Transient Failure verschwendet die Chance auf Success. Kein Retry bei Transient Failure erzeugt unnötige User-Errors. Blindes Retry bei Persistent Failure verschlimmert die Situation.
Die Lösung: Intelligente Retry-Strategie
Exponential Backoff: Verdopple die Wartezeit bei jedem Retry
- Retry 1: 1 Sekunde warten
- Retry 2: 2 Sekunden warten
- Retry 3: 4 Sekunden warten
- Retry 4: 8 Sekunden warten
Jitter: Füge Randomness hinzu, um "Thundering Herd" zu vermeiden
- Ohne Jitter: 1000 Clients retrying gleichzeitig nach genau 1s, 2s, 4s
- Mit Jitter: Clients retry zwischen 0.5-1.5s, 1-3s, 2-6s (verteilt)
Implementierung mit Polly
using Polly;
using Polly.Retry;
public class ResilientHttpClient
{
private readonly HttpClient _httpClient;
private readonly AsyncRetryPolicy<HttpResponseMessage> _retryPolicy;
public ResilientHttpClient(HttpClient httpClient)
{
_httpClient = httpClient;
_retryPolicy = Policy
.HandleResult<HttpResponseMessage>(r =>
!r.IsSuccessStatusCode &&
r.StatusCode != HttpStatusCode.NotFound && // Don't retry 404
r.StatusCode != HttpStatusCode.BadRequest) // Don't retry 400
.Or<HttpRequestException>()
.Or<TaskCanceledException>()
.WaitAndRetryAsync(
retryCount: 3,
sleepDurationProvider: retryAttempt =>
{
// Exponential backoff with jitter
var baseDelay = TimeSpan.FromSeconds(Math.Pow(2, retryAttempt));
var jitter = TimeSpan.FromMilliseconds(
Random.Shared.Next(0, 1000));
return baseDelay + jitter;
},
onRetry: (outcome, timespan, retryCount, context) =>
{
var statusCode = outcome.Result?.StatusCode.ToString() ?? "Exception";
Console.WriteLine(
$"Retry {retryCount} after {timespan.TotalSeconds}s. " +
$"Status: {statusCode}");
});
}
public async Task<T> GetAsync<T>(string url)
{
var response = await _retryPolicy.ExecuteAsync(
() => _httpClient.GetAsync(url));
response.EnsureSuccessStatusCode();
return await response.Content.ReadAsAsync<T>();
}
}
Retry-Strategie-Matrix
Error Type | Retry? | Max Retries | Strategy
------------------------|--------|-------------|---------------------------
Network Timeout | ✅ Yes | 3 | Exponential Backoff + Jitter
5xx Server Error | ✅ Yes | 3 | Exponential Backoff + Jitter
503 Service Unavailable | ✅ Yes | 5 | Exponential Backoff, longer delays
429 Too Many Requests | ✅ Yes | 3 | Backoff nach Retry-After header
500 Internal Error | ✅ Yes | 2 | Short backoff (may be transient)
408 Request Timeout | ✅ Yes | 2 | Short backoff
Connection Refused | ✅ Yes | 3 | Mit Circuit Breaker kombinieren
4xx Client Errors | ❌ No | 0 | Code-Fix erforderlich
401 Unauthorized | ❌ No | 0 | Auth-Fix erforderlich
404 Not Found | ❌ No | 0 | Ressource existiert nicht
400 Bad Request | ❌ No | 0 | Input-Validierung fehlgeschlagen
Polly: Resilience-Patterns in .NET
Polly ist die De-Facto-Standard-Library für Resilience-Patterns in .NET. Sie ermöglicht deklarative Policy-Definition und -Composition.
Kombinierte Policies
public class ResilientOrderService
{
private readonly IAsyncPolicy<Order> _resiliencePolicy;
public ResilientOrderService()
{
// Timeout Policy
var timeoutPolicy = Policy
.TimeoutAsync<Order>(
TimeSpan.FromSeconds(5),
TimeoutStrategy.Pessimistic);
// Retry Policy mit Exponential Backoff
var retryPolicy = Policy<Order>
.Handle<HttpRequestException>()
.Or<TimeoutRejectedException>()
.WaitAndRetryAsync(
retryCount: 3,
sleepDurationProvider: attempt =>
TimeSpan.FromSeconds(Math.Pow(2, attempt)) +
TimeSpan.FromMilliseconds(Random.Shared.Next(0, 1000)));
// Circuit Breaker Policy
var circuitBreakerPolicy = Policy<Order>
.Handle<HttpRequestException>()
.CircuitBreakerAsync(
handledEventsAllowedBeforeBreaking: 5,
durationOfBreak: TimeSpan.FromMinutes(1),
onBreak: (result, duration) =>
{
Console.WriteLine($"Circuit broken for {duration.TotalSeconds}s");
},
onReset: () =>
{
Console.WriteLine("Circuit reset");
});
// Fallback Policy
var fallbackPolicy = Policy<Order>
.Handle<Exception>()
.FallbackAsync(
fallbackValue: Order.CreateDefault(),
onFallbackAsync: async (result, context) =>
{
Console.WriteLine("Using fallback order");
await Task.CompletedTask;
});
// Combine Policies (Order matters!)
_resiliencePolicy = Policy.WrapAsync(
fallbackPolicy, // Outer: Final safety net
circuitBreakerPolicy, // Prevent cascading failures
retryPolicy, // Handle transient failures
timeoutPolicy // Inner: First line of defense
);
}
public async Task<Order> GetOrder(string orderId)
{
return await _resiliencePolicy.ExecuteAsync(async () =>
{
var response = await _httpClient.GetAsync(
$"https://order-api.example.com/orders/{orderId}");
response.EnsureSuccessStatusCode();
return await response.Content.ReadAsAsync<Order>();
});
}
}
Real-World Example: E-Commerce Checkout mit vollständiger Resilience
public class ResilientCheckoutService
{
private readonly IHttpClientFactory _httpClientFactory;
private readonly IAsyncPolicy _paymentPolicy;
private readonly IAsyncPolicy _inventoryPolicy;
private readonly IAsyncPolicy _notificationPolicy;
public ResilientCheckoutService(IHttpClientFactory httpClientFactory)
{
_httpClientFactory = httpClientFactory;
// Payment: Critical, aggressive resilience
_paymentPolicy = Policy.WrapAsync(
Policy.Handle<Exception>()
.FallbackAsync(async ct => throw new PaymentFailedException()),
Policy.Handle<HttpRequestException>()
.CircuitBreakerAsync(5, TimeSpan.FromMinutes(2)),
Policy.Handle<HttpRequestException>()
.WaitAndRetryAsync(3, attempt => TimeSpan.FromSeconds(Math.Pow(2, attempt))),
Policy.TimeoutAsync(TimeSpan.FromSeconds(10))
);
// Inventory: Moderate resilience, fallback to "assume available"
_inventoryPolicy = Policy.WrapAsync(
Policy.Handle<Exception>()
.FallbackAsync(async ct =>
{
// Assume available rather than blocking checkout
return new InventoryResponse { Available = true };
}),
Policy.Handle<HttpRequestException>()
.CircuitBreakerAsync(3, TimeSpan.FromSeconds(30)),
Policy.Handle<HttpRequestException>()
.WaitAndRetryAsync(2, _ => TimeSpan.FromSeconds(1)),
Policy.TimeoutAsync(TimeSpan.FromSeconds(2))
);
// Notifications: Fire-and-forget, minimal resilience
_notificationPolicy = Policy.WrapAsync(
Policy.Handle<Exception>()
.FallbackAsync(async ct =>
{
// Queue for later retry
await QueueNotificationForRetry();
}),
Policy.TimeoutAsync(TimeSpan.FromSeconds(5))
);
}
public async Task<CheckoutResult> ProcessCheckout(CheckoutRequest request)
{
// 1. Check Inventory (with fallback)
var inventoryAvailable = await _inventoryPolicy.ExecuteAsync(
async () => await CheckInventory(request.Items));
if (!inventoryAvailable)
{
return CheckoutResult.OutOfStock();
}
// 2. Process Payment (critical, no fallback)
PaymentResult payment;
try
{
payment = await _paymentPolicy.ExecuteAsync(
async () => await ProcessPayment(request.Payment));
}
catch (PaymentFailedException)
{
return CheckoutResult.PaymentFailed();
}
// 3. Create Order
var order = await CreateOrder(request, payment);
// 4. Send Notification (fire-and-forget)
_ = _notificationPolicy.ExecuteAsync(
async () => await SendOrderConfirmation(order));
return CheckoutResult.Success(order);
}
}
Monitoring und Alerting für Resilience
Resilience-Patterns sind wertlos ohne Visibility. Sie müssen wissen:
- Wie oft öffnen Circuit Breakers?
- Wie viele Requests werden rejected?
- Wie oft greifen Fallbacks?
- Wie hoch ist die Retry-Rate?
Key Metrics
public class ResilientServiceMetrics
{
private readonly IMetricsPublisher _metrics;
// Circuit Breaker Metrics
public void RecordCircuitBreakerState(string service, CircuitState state)
{
_metrics.Gauge($"circuit_breaker.{service}.state", (int)state);
// 0 = Closed, 1 = Open, 2 = HalfOpen
}
public void RecordCircuitBreakerRejection(string service)
{
_metrics.Increment($"circuit_breaker.{service}.rejections");
// Alert if > 100/minute
}
// Retry Metrics
public void RecordRetry(string service, int attemptNumber)
{
_metrics.Increment($"retry.{service}.attempts");
_metrics.Histogram($"retry.{service}.attempt_number", attemptNumber);
// Alert if retry-rate > 10%
}
// Bulkhead Metrics
public void RecordBulkheadRejection(string service)
{
_metrics.Increment($"bulkhead.{service}.rejections");
// Alert if rejections occur
}
// Fallback Metrics
public void RecordFallbackUsed(string service, string reason)
{
_metrics.Increment($"fallback.{service}.used", new { reason });
// Alert if fallback-rate > 5%
}
// Latency Metrics (P50, P95, P99)
public void RecordLatency(string service, long milliseconds)
{
_metrics.Histogram($"latency.{service}", milliseconds);
// Alert if P95 > threshold
}
}
Alerting-Regeln
Critical Alerts (Immediate Response):
├── Circuit Breaker OPEN for > 5 minutes
├── Payment service error-rate > 1%
├── Bulkhead rejections > 10/minute
└── P99 latency > 5 seconds
Warning Alerts (Investigate):
├── Retry-rate > 10%
├── Fallback-usage > 5%
├── Circuit Breaker flapping (Open/Close cycles)
└── P95 latency increasing trend
Fazit: Defense in Depth
Resiliente Systeme erfordern mehrere Verteidigungslinien:
Layer 1: Timeouts - Verhindere unbegrenztes Warten Layer 2: Retries - Handle transient failures Layer 3: Circuit Breakers - Verhindere kaskadierende Failures Layer 4: Bulkheads - Isoliere Fehler Layer 5: Fallbacks - Graceful degradation Layer 6: Monitoring - Visibility und schnelle Response
Die wichtigsten Takeaways:
- Design for Failure von Anfang an - Resilience als Afterthought funktioniert nicht
- Combine Multiple Patterns - Kein einzelnes Pattern ist ausreichend
- Test in Production - Chaos Engineering zeigt echte Schwachstellen
- Monitor Relentlessly - Ohne Metrics sind Resilience-Patterns Blackboxes
- Fail Fast, Recover Fast - Schnelle Failures sind besser als langsame
Perfect Availability ist eine Illusion. Resilience ist Realität. Der Unterschied zwischen einem fragilen und einem resilienten System ist nicht ob Fehler auftreten, sondern wie elegant das System damit umgeht.