- Circuit Breaker คืออะไร
- 3 สถานะหลัก
- Circuit Breaker ต่างจาก Retry อย่างไร
- Laravel รองรับอะไรบ้าง
- Workshop: สร้าง Circuit Breaker ด้วย Laravel + Redis
- Production Hardening
- Circuit Breaker กับ Queue
- Architecture ที่แนะนำ
- แนวทางตั้งค่าเริ่มต้น
- สรุป
#Circuit Breaker คืออะไร
Circuit Breaker เป็น resilience pattern สำหรับระบบที่ต้องเรียกบริการภายนอก เช่น Payment Gateway, AI/LLM API, REST API หรือ Microservice โดยหยุดส่ง request ชั่วคราวเมื่อปลายทางล้มเหลวต่อเนื่อง แทนที่จะ retry อย่างไม่มีขอบเขต
แนวคิดคล้ายเบรกเกอร์ไฟฟ้า: เมื่อพบความผิดปกติเกิน threshold วงจรจะ "ตัด" เพื่อป้องกันความเสียหายลุกลาม
#3 สถานะหลัก
- CLOSED --- ระบบทำงานปกติ request ถูกส่งไปยัง service ปลายทาง
- OPEN --- failure ถึง threshold แล้ว ระบบหยุดเรียก service และตอบ fallback ทันที
- HALF-OPEN --- หลัง cooldown ระบบอนุญาต probe request จำนวนจำกัดเพื่อทดสอบว่าปลายทางกลับมาหรือยัง
failures >= threshold
┌─────────────────────────────────────┐
│ ▼
┌────────┐ ┌────────┐
│ CLOSED │ │ OPEN │
└────────┘ └────────┘
▲ │
│ success │ cooldown
│ ▼
└───────────────────────────────┌───────────┐
│ HALF-OPEN │
└───────────┘
│
failure ──────┴───── success
OPEN CLOSED
#Circuit Breaker ต่างจาก Retry อย่างไร
- Retry เหมาะกับ transient failure และลอง request ใหม่ในช่วงสั้น ๆ
- Circuit Breaker หยุดเรียก dependency ที่มีแนวโน้มเสียต่อเนื่อง
- ใน production มักใช้ร่วมกับ timeout + bounded retry + circuit breaker + fallback
ไม่ควร retry ทุก HTTP error เช่น 400/401/403 เพราะมักไม่ใช่ transient failure
#Laravel รองรับอะไรบ้าง
Laravel HTTP Client มี timeout, connectTimeout และ retry สำหรับ
outbound HTTP requests แต่ Circuit Breaker สามารถประกอบขึ้นเองด้วย
Cache/Redis เพื่อแชร์ state ระหว่าง application instances ได้ นอกจากนี้ queued
jobs มี ThrottlesExceptions ซึ่งมีพฤติกรรมใกล้เคียงการลดแรงกระแทกจาก
third-party service ที่ไม่เสถียร
#Workshop: สร้าง Circuit Breaker ด้วย Laravel + Redis
#1. ตั้งค่า Cache
ตัวอย่าง .env
CACHE_STORE=redis
REDIS_HOST=127.0.0.1
REDIS_PORT=6379
ในระบบหลาย instance ควรใช้ shared cache เช่น Redis เพื่อให้ทุก instance เห็นสถานะ breaker เดียวกัน
#2. สร้าง CircuitBreaker Service
สร้างไฟล์ app/Services/CircuitBreaker.php
<?php
namespace App\Services;
use Closure;
use Illuminate\Support\Facades\Cache;
use RuntimeException;
use Throwable;
class CircuitBreaker
{
public function __construct(
private readonly string $name,
private readonly int $failureThreshold = 5,
private readonly int $cooldownSeconds = 30,
) {}
private function failuresKey(): string
{
return "cb:{$this->name}:failures";
}
private function openUntilKey(): string
{
return "cb:{$this->name}:open_until";
}
public function execute(Closure $operation, ?Closure $fallback = null): mixed
{
$openUntil = Cache::get($this->openUntilKey());
if ($openUntil && now()->timestamp < $openUntil) {
return $this->fallback($fallback, 'Circuit is OPEN');
}
try {
// เมื่อ cooldown หมด request นี้ทำหน้าที่เป็น probe แบบง่าย
$result = $operation();
Cache::forget($this->failuresKey());
Cache::forget($this->openUntilKey());
return $result;
} catch (Throwable $e) {
$failures = Cache::increment($this->failuresKey());
// ให้ counter มี TTL เพื่อไม่สะสม failure เก่าไปตลอด
if ($failures === 1) {
Cache::put(
$this->failuresKey(),
1,
now()->addMinutes(5)
);
}
if ($failures >= $this->failureThreshold) {
Cache::put(
$this->openUntilKey(),
now()->addSeconds($this->cooldownSeconds)->timestamp,
$this->cooldownSeconds
);
}
if ($fallback) {
return $fallback($e);
}
throw $e;
}
}
private function fallback(?Closure $fallback, string $message): mixed
{
if ($fallback) {
return $fallback(new RuntimeException($message));
}
throw new RuntimeException($message);
}
}
ตัวอย่างนี้เหมาะสำหรับการเรียนรู้ ใน production ควรใช้ atomic lock สำหรับ HALF-OPEN probe เพื่อป้องกันหลาย instance ยิง probe พร้อมกัน และกำหนด failure policy ให้ชัดเจน
#3. เรียก External API
สร้าง app/Services/PaymentService.php
<?php
namespace App\Services;
use Illuminate\Http\Client\Response;
use Illuminate\Support\Facades\Http;
use RuntimeException;
class PaymentService
{
public function charge(array $payload): array
{
$breaker = new CircuitBreaker(
name: 'payment-api',
failureThreshold: 5,
cooldownSeconds: 30,
);
return $breaker->execute(
operation: function () use ($payload) {
$response = Http::connectTimeout(2)
->timeout(5)
->retry(2, 200)
->post(config('services.payment.url').'/charge', $payload);
if ($response->serverError()) {
throw new RuntimeException(
"Payment API failed: {$response->status()}"
);
}
// 4xx ควรจัดการเป็น business/client error แยกจาก breaker
$response->throw();
return $response->json();
},
fallback: fn ($e) => [
'status' => 'pending',
'message' => 'Payment service is temporarily unavailable.',
],
);
}
}
#4. Flow การทำงาน
flowchart TD
A[Laravel Application] --> B{Circuit State}
B -->|CLOSED| C[Call External API]
B -->|OPEN| F[Fallback]
C -->|Success| D[Reset Failure Counter]
C -->|Failure| E[Increment Failure Counter]
E --> G{Threshold reached?}
G -->|No| H[Return error/fallback]
G -->|Yes| I[OPEN Circuit]
I --> J[Cooldown]
J --> K[HALF-OPEN Probe]
K -->|Success| D
K -->|Failure| I
#Production Hardening
ควรเพิ่มองค์ประกอบต่อไปนี้:
- ใช้ Redis/shared cache เพื่อให้ circuit state สอดคล้องกันทุก node
- ใช้ atomic lock ตอน HALF-OPEN เพื่อให้มี probe request จำกัด
- นับเฉพาะ failure ที่บ่งชี้ dependency failure เช่น timeout, connection error, HTTP 5xx และ 429 ตาม policy
- ไม่เปิด breaker จาก validation error หรือ authentication error โดยอัตโนมัติ
- ใช้ timeout สั้นและ retry แบบจำกัด พร้อม exponential backoff/jitter เมื่อเหมาะสม
- ทำ fallback เช่น cached data, queue งานไว้ภายหลัง หรือ degraded response
- เก็บ metrics: state, failure count, open count, rejected calls, recovery time
- log state transition
CLOSED → OPEN → HALF-OPEN → CLOSED - แยก circuit ตาม dependency หรือ endpoint ที่มี failure domain ต่างกัน
#Circuit Breaker กับ Queue
สำหรับ queued jobs ที่เรียก third-party API สามารถใช้
Illuminate\Queue\Middleware\ThrottlesExceptions เพื่อลดความถี่ในการ retry
หลังเกิด exception ต่อเนื่อง ซึ่งเหมาะกับ dependency ที่ไม่เสถียร แม้แนวคิดและ API
จะไม่เหมือน Circuit Breaker state machine เต็มรูปแบบ
#Architecture ที่แนะนำ
Client
│
▼
Laravel API
│
├── Timeout
│
├── Bounded Retry
│
├── Circuit Breaker ───── Redis
│ │
│ ├── CLOSED
│ ├── OPEN ────── Fallback / Queue / Cache
│ └── HALF-OPEN
│
▼
External Service
#แนวทางตั้งค่าเริ่มต้น
สำหรับ API ทั่วไปอาจเริ่มทดลองด้วย:
connect timeout : 1–3 s
request timeout : 3–10 s
retry : 1–2 ครั้ง
failure threshold: 5 ครั้ง
cooldown : 30–60 s
ค่าจริงควร derive จาก SLO, latency distribution, error budget และพฤติกรรมของ dependency ไม่ควรใช้ค่าชุดเดียวกับทุก service
#สรุป
Circuit Breaker ช่วยให้ Laravel fail fast เมื่อ dependency มีปัญหา ลด request ที่ไร้ประโยชน์ ลด resource exhaustion และป้องกัน cascading failure ในระบบ distributed system
สูตรที่เหมาะสำหรับ production คือ:
Timeout → Bounded Retry → Circuit Breaker → Fallback → Observability
โดย Laravel มี building blocks สำคัญ เช่น HTTP Client, Cache/Redis, atomic locks และ queue exception throttling ที่สามารถนำมาประกอบเป็น resilience layer ได้