18.3 Circuit Breaker
TL;DR Circuit Breaker protege contra falhas em cascata em microsserviços. 3 estados: CLOSED (funciona normal), OPEN (fail fast sem chamar o serviço), HALF-OPEN (testa se voltou). Se failures > threshold, vai para OPEN; depois do timeout tenta HALF-OPEN. Fallback strategies: valor default, dados em cache, degraded mode, queue. Bulkhead Pattern para isolar recursos.
Conteúdo
- O que é
- O problema sem Circuit Breaker
- Estados do Circuit Breaker
- Implementação em PHP
- Laravel Package
- Fallback Strategy
- Bulkhead Pattern (complemento)
- Monitoring
- Boas práticas
- Circuit Breaker + Retry
- Exercícios práticos
O que é
Circuit Breaker: Padrão para proteger de falhas em cascata em sistemas distribuídos. Impede chamadas a um serviço fora do ar.
Para quê:
- Proteção contra cascade failures
- Fail fast em vez de timeout
- Dar tempo do serviço se recuperar
- Graceful degradation
Analogia: Como o disjuntor na rede elétrica. Se der curto-circuito → o disjuntor abre o circuito.
O problema sem Circuit Breaker
Cenário:
Order Service → Payment Service (lento/caiu)
↓
Cada request espera 30 segundos de timeout
↓
Threads travam
↓
Order Service para de responder
↓
Cascade failure: o sistema inteiro caiuCom Circuit Breaker:
Order Service → Circuit Breaker → Payment Service
↓
se o Payment caiu:
open circuit → fail fast
↓
Order Service continua funcionandoEstados do Circuit Breaker
┌───────────┐
│ CLOSED │ ← Funcionamento normal
│ (working) │
└─────┬─────┘
│
Failures > threshold
│
↓
┌───────────┐
│ OPEN │ ← Todos os requests fail fast
│ (broken) │
└─────┬─────┘
│
After timeout
│
↓
┌───────────┐
│ HALF-OPEN │ ← Tenta 1 request
│ (testing)│
└─────┬─────┘
│
┌───────┴───────┐
Success Failure
│ │
↓ ↓
CLOSED OPEN1. CLOSED (Fechado)
Funcionamento normal:
- Requests passam para o serviço
- Conta as failures
- Se failures > threshold → OPEN
2. OPEN (Aberto)
Serviço fora do ar:
- Todos os requests fail fast (sem chamar o serviço)
- Não sobrecarrega o serviço caído
- Depois do timeout → HALF-OPEN
3. HALF-OPEN (Meio aberto)
Teste de recuperação:
- Deixa passar 1 request de teste
- Se success → CLOSED
- Se failure → OPEN
Implementação em PHP
Implementação básica:
class CircuitBreaker
{
private string $service;
private int $failureThreshold = 5;
private int $timeout = 60; // segundos
public function call(callable $callback)
{
$state = $this->getState();
// OPEN: fail fast
if ($state === 'open') {
if ($this->shouldAttemptReset()) {
return $this->attemptReset($callback);
}
throw new CircuitBreakerOpenException("Serviço {$this->service} indisponível");
}
// CLOSED ou HALF-OPEN: tenta a chamada
try {
$result = $callback();
// Success: zera as failures
$this->onSuccess();
return $result;
} catch (Exception $e) {
// Failure: incrementa o contador
$this->onFailure();
throw $e;
}
}
private function getState(): string
{
$failures = Cache::get("circuit:{$this->service}:failures", 0);
$openedAt = Cache::get("circuit:{$this->service}:opened_at");
if ($failures >= $this->failureThreshold) {
return 'open';
}
return 'closed';
}
private function shouldAttemptReset(): bool
{
$openedAt = Cache::get("circuit:{$this->service}:opened_at");
if (!$openedAt) {
return false;
}
return (time() - $openedAt) >= $this->timeout;
}
private function attemptReset(callable $callback)
{
try {
$result = $callback();
// Success: fecha o circuit
$this->reset();
return $result;
} catch (Exception $e) {
// Ainda falhando: reabre
Cache::put("circuit:{$this->service}:opened_at", time(), 3600);
throw $e;
}
}
private function onSuccess(): void
{
$this->reset();
}
private function onFailure(): void
{
$failures = Cache::increment("circuit:{$this->service}:failures");
if ($failures >= $this->failureThreshold) {
Cache::put("circuit:{$this->service}:opened_at", time(), 3600);
}
}
private function reset(): void
{
Cache::forget("circuit:{$this->service}:failures");
Cache::forget("circuit:{$this->service}:opened_at");
}
}Uso:
$circuitBreaker = new CircuitBreaker('payment-service');
try {
$result = $circuitBreaker->call(function () {
return Http::timeout(5)
->post('http://payment-service/api/charge', [...])
->throw()
->json();
});
} catch (CircuitBreakerOpenException $e) {
// Circuit OPEN: fail fast
Log::warning('Serviço de pagamento indisponível');
return response()->json([
'error' => 'Serviço de pagamento temporariamente indisponível'
], 503);
} catch (Exception $e) {
// Outro erro
Log::error('Pagamento falhou', ['error' => $e->getMessage()]);
return response()->json([
'error' => 'Pagamento falhou'
], 500);
}Laravel Package
Composer:
composer require opis/circuit-breakerUso:
use Opis\CircuitBreaker\CircuitBreaker;
$breaker = new CircuitBreaker();
$result = $breaker->call('payment-service', function () {
return Http::post('http://payment-service/api/charge', [...])->json();
}, [
'failure_threshold' => 5,
'success_threshold' => 2,
'timeout' => 60,
]);Fallback Strategy
O que fazer quando o circuit está OPEN?
1. Devolver valor default
try {
$recommendations = $circuitBreaker->call(function () {
return Http::get('http://recommendation-service/api/products')->json();
});
} catch (CircuitBreakerOpenException $e) {
// Fallback: produtos populares
$recommendations = Product::orderBy('views', 'desc')->limit(10)->get();
}2. Cached data
try {
$user = $circuitBreaker->call(function () use ($userId) {
return Http::get("http://user-service/api/users/{$userId}")->json();
});
} catch (CircuitBreakerOpenException $e) {
// Fallback: dados do cache
$user = Cache::get("user:{$userId}");
if (!$user) {
throw new UserNotFoundException();
}
}3. Degraded mode
try {
$analytics = $circuitBreaker->call(function () {
return Http::get('http://analytics-service/api/stats')->json();
});
} catch (CircuitBreakerOpenException $e) {
// Degraded: mostra a página sem analytics
$analytics = null;
}
return view('dashboard', [
'analytics' => $analytics, // pode ser null
]);4. Queue para retry
try {
$circuitBreaker->call(function () use ($email) {
Http::post('http://notification-service/api/send', ['email' => $email]);
});
} catch (CircuitBreakerOpenException $e) {
// Fallback: coloca na queue
SendEmailJob::dispatch($email)->delay(now()->addMinutes(5));
}Bulkhead Pattern (complemento)
Problema: Um serviço lento come todas as threads.
Solução: Isolar recursos por serviço.
class BulkheadCircuitBreaker extends CircuitBreaker
{
private int $maxConcurrentCalls = 10;
public function call(callable $callback)
{
$currentCalls = Cache::get("bulkhead:{$this->service}:calls", 0);
if ($currentCalls >= $this->maxConcurrentCalls) {
throw new BulkheadFullException("Chamadas concorrentes demais para {$this->service}");
}
Cache::increment("bulkhead:{$this->service}:calls");
try {
return parent::call($callback);
} finally {
Cache::decrement("bulkhead:{$this->service}:calls");
}
}
}Monitoring
Métricas:
class CircuitBreaker
{
private function onSuccess(): void
{
$this->reset();
// Métricas
Metrics::increment("circuit_breaker.{$this->service}.success");
}
private function onFailure(): void
{
$failures = Cache::increment("circuit:{$this->service}:failures");
// Métricas
Metrics::increment("circuit_breaker.{$this->service}.failure");
if ($failures >= $this->failureThreshold) {
Cache::put("circuit:{$this->service}:opened_at", time(), 3600);
// Alert
Log::critical("Circuit breaker aberto para {$this->service}");
Metrics::increment("circuit_breaker.{$this->service}.opened");
}
}
}Dashboard (Grafana):
Métricas:
- circuit_breaker.{service}.success_rate (%)
- circuit_breaker.{service}.state (closed/open/half-open)
- circuit_breaker.{service}.failures (count)Boas práticas
✓ Timeout: use um timeout razoável (3-5 segundos)
✓ Failure threshold: 5-10 failures
✓ Reset timeout: 30-60 segundos
✓ Exponential backoff: aumenta o timeout depois de cada falha
✓ Fallback: sempre tenha um plano B
✓ Monitoring: acompanhe o estado dos circuits
✓ Alerts: avise quando o circuit abre
✓ Granularity: um circuit por serviço/endpointCircuit Breaker + Retry
Combinação:
class ResilientHttpClient
{
public function call(string $service, callable $callback, int $maxRetries = 3)
{
$circuitBreaker = new CircuitBreaker($service);
for ($attempt = 1; $attempt <= $maxRetries; $attempt++) {
try {
return $circuitBreaker->call($callback);
} catch (CircuitBreakerOpenException $e) {
// Circuit OPEN: não faz retry
throw $e;
} catch (Exception $e) {
if ($attempt === $maxRetries) {
throw $e;
}
// Exponential backoff
sleep(pow(2, $attempt));
}
}
}
}
// Uso
$client = new ResilientHttpClient();
$result = $client->call('payment-service', function () {
return Http::post('http://payment-service/api/charge', [...])->json();
});Exercícios práticos
Exercício 1: Circuit Breaker básico
Enunciado: Implemente um Circuit Breaker simples com três estados para proteger o Payment Service de falhas.
Solução:
class SimpleCircuitBreaker
{
private string $service;
private int $failureThreshold = 3;
private int $timeout = 30; // segundos
public function __construct(string $service)
{
$this->service = $service;
}
public function call(callable $callback)
{
$failures = Cache::get("cb:{$this->service}:failures", 0);
$openedAt = Cache::get("cb:{$this->service}:opened_at");
// OPEN state
if ($failures >= $this->failureThreshold) {
if ($openedAt && (time() - $openedAt) >= $this->timeout) {
// HALF-OPEN: tenta um request
return $this->attemptReset($callback);
}
throw new CircuitBreakerOpenException("Serviço {$this->service} fora do ar");
}
// CLOSED: operação normal
try {
$result = $callback();
Cache::forget("cb:{$this->service}:failures");
return $result;
} catch (Exception $e) {
$failures = Cache::increment("cb:{$this->service}:failures");
if ($failures >= $this->failureThreshold) {
Cache::put("cb:{$this->service}:opened_at", time(), 3600);
Log::error("Circuit breaker aberto para {$this->service}");
}
throw $e;
}
}
private function attemptReset(callable $callback)
{
try {
$result = $callback();
// Success: fecha o circuit
Cache::forget("cb:{$this->service}:failures");
Cache::forget("cb:{$this->service}:opened_at");
Log::info("Circuit breaker fechado para {$this->service}");
return $result;
} catch (Exception $e) {
Cache::put("cb:{$this->service}:opened_at", time(), 3600);
throw $e;
}
}
}
// Uso
$cb = new SimpleCircuitBreaker('payment-service');
try {
$result = $cb->call(fn() => Http::timeout(5)->post('http://payment-service/api/charge')->json());
} catch (CircuitBreakerOpenException $e) {
// Fallback
return ['status' => 'queued'];
}Exercício 2: Fallback Strategy com cache
Enunciado: Implemente um Circuit Breaker com fallback para dados em cache quando o serviço estiver fora do ar.
Solução:
class RecommendationService
{
public function __construct(private CircuitBreaker $cb) {}
public function getRecommendations(int $userId): array
{
$cacheKey = "recommendations:{$userId}";
try {
$recommendations = $this->cb->call(function () use ($userId) {
return Http::timeout(3)
->get("http://recommendation-service/api/users/{$userId}/recommendations")
->json();
});
// Atualiza o cache se deu certo
Cache::put($cacheKey, $recommendations, 3600);
return $recommendations;
} catch (CircuitBreakerOpenException $e) {
// Fallback: dados do cache
$cached = Cache::get($cacheKey);
if ($cached) {
Log::info("Usando recommendations em cache para o usuário {$userId}");
return $cached;
}
// Fallback final: produtos populares
return Product::orderBy('views', 'desc')->limit(10)->pluck('id')->toArray();
}
}
}Exercício 3: Circuit Breaker com Retry e Exponential Backoff
Enunciado: Combine Circuit Breaker com retry e exponential backoff.
Solução:
class ResilientHttpClient
{
public function call(string $service, callable $callback, int $maxRetries = 3)
{
$circuitBreaker = new CircuitBreaker($service);
for ($attempt = 1; $attempt <= $maxRetries; $attempt++) {
try {
return $circuitBreaker->call($callback);
} catch (CircuitBreakerOpenException $e) {
// Circuit OPEN: não faz retry
Log::warning("Circuit breaker aberto para {$service}, pulando retry");
throw $e;
} catch (RequestException $e) {
if ($attempt === $maxRetries) {
Log::error("Todas as {$maxRetries} tentativas falharam para {$service}");
throw $e;
}
// Exponential backoff: 2^attempt segundos
$delay = pow(2, $attempt);
Log::info("Retry {$attempt}/{$maxRetries} para {$service} em {$delay}s");
sleep($delay);
}
}
}
}
// Uso
$client = new ResilientHttpClient();
$result = $client->call('user-service', function () use ($userId) {
return Http::timeout(5)->get("http://user-service/api/users/{$userId}")->throw()->json();
}, maxRetries: 3);Na entrevista
"Circuit Breaker protege contra falhas em cascata. 3 estados: CLOSED (funciona), OPEN (fail fast), HALF-OPEN (testa se voltou). Parâmetros: failure threshold (5-10), timeout (30-60s). Transições: CLOSED → OPEN se failures > threshold, OPEN → HALF-OPEN depois do timeout, HALF-OPEN → CLOSED se success. Fallback strategies: valor default, dados em cache, degraded mode, queue para retry. Bulkhead Pattern para isolar recursos. Monitoring: métricas de sucesso/failures, alerts quando abre. Best practices: timeout, exponential backoff, circuits granulares, fallbacks."
Parte do PHP/Laravel Interview Handbook | Feito com ❤️ pela equipe CodeMate