Circuit breaker, retry, timeout, bulkheadCircuit breakers, retries, timeouts, bulkheads
Checkout API gọi hai dependency qua hai lớp bảo vệ riêng: recommendation không cốt lõi nên có fallback từ cache, payment là cốt lõi nên fail fast. Bật “Recommendation chậm” để xem timeout và breaker mở; bật “Breaker phục hồi” để xem trạng thái HALF_OPEN.The checkout API calls two dependencies through separate protection layers: recommendations are non-critical and fall back to a cache, payment is critical and fails fast. Turn on “Recommendation slow” to see timeouts open the breaker; turn on “Breaker recovers” to see the HALF_OPEN state.
1Người dùng mở trang checkout của đơn 9812.A user opens checkout for order 9812.
Service chết trả lỗi ngay (connection refused). Service chậm giữ thread và connection cho đến khi pool cạn, rồi kéo cả API chết theo. Mọi lời gọi mạng phải có connect timeout và read timeout; không dùng giá trị mặc định vô hạn.A dead service fails at once (connection refused). A slow one holds threads and connections until the pool is drained, then takes the whole API down. Every network call needs a connect timeout and a read timeout; never keep the infinite default.
API hết 800 ms và trả lỗi, nhưng các service phía dưới vẫn làm tiếp cho một request không ai chờ. Truyền deadline qua context.Context hoặc header (grpc-timeout, X-Request-Deadline); mỗi tầng chỉ dùng phần ngân sách còn lại.The API gives up at 800 ms, but downstream services keep working on a request nobody is waiting for. Pass the deadline through context.Context or a header (grpc-timeout, X-Request-Deadline); each hop uses only the remaining budget.
Ba tầng, mỗi tầng thử lại 3 lần, biến 1 lời gọi thành 27. Chỉ retry ở một tầng, chỉ với lỗi tạm thời (timeout kết nối, 503) và thao tác idempotent; dùng exponential backoff + jitter và retry budget (ví dụ retry không quá 10% lưu lượng).Three layers that each retry 3 times turn one call into 27. Retry at one layer only, only for transient errors (connect timeouts, 503) and idempotent operations; use exponential backoff with jitter and a retry budget (for example, retries capped at 10% of traffic).
Circuit breaker đếm tỉ lệ lỗi và tỉ lệ gọi chậm trong cửa sổ trượt. Vượt ngưỡng thì OPEN và từ chối ngay; sau thời gian chờ thì HALF_OPEN cho vài lời gọi thử; thành công thì CLOSED. Java dùng Resilience4j, Go dùng sony/gobreaker.A circuit breaker tracks the failure rate and slow-call rate over a sliding window. Above the threshold it goes OPEN and rejects at once; after a wait it goes HALF_OPEN and lets a few probes through; on success it returns to CLOSED. Java uses Resilience4j, Go uses sony/gobreaker.
Bulkhead giới hạn số lời gọi đồng thời cho từng dependency (thread pool riêng hoặc semaphore). Recommendation đầy 20 chỗ thì lời gọi thứ 21 bị từ chối ngay, payment vẫn còn 40 chỗ của mình.A bulkhead caps concurrent calls per dependency (its own thread pool or a semaphore). When recommendations fill their 20 slots, call 21 is rejected at once, while payment keeps its own 40.
Tính năng không cốt lõi (gợi ý, review, banner) dùng fallback: cache cũ, giá trị mặc định hoặc ẩn đi. Tính năng cốt lõi như thanh toán phải fail fast và báo lỗi rõ; một fallback giả vờ “thanh toán thành công” còn tệ hơn lỗi.Non-critical features (suggestions, reviews, banners) use a fallback: a stale cache, a default value, or hiding the widget. Critical ones such as payment must fail fast with a clear error; a fallback that pretends “payment succeeded” is worse than the error.
Liveness chỉ kiểm tra process còn sống. Readiness kiểm tra dependency bắt buộc (DB) để rút instance khỏi load balancer, nhưng không gồm dependency có fallback. Nếu readiness phụ thuộc recommendation, nó chậm là toàn bộ pod checkout bị rút.Liveness only checks that the process is alive. Readiness checks required dependencies (the DB) so the instance leaves the load balancer, but excludes dependencies that have a fallback. If readiness depended on recommendations, one slow service would pull every checkout pod.
Chaos testing chủ động tiêm độ trễ, lỗi 503, mất kết nối (Toxiproxy, Chaos Mesh, AWS FIS) ở staging rồi production có kiểm soát, để xác nhận timeout, breaker và fallback hoạt động đúng như cấu hình.Chaos testing deliberately injects latency, 503s and dropped connections (Toxiproxy, Chaos Mesh, AWS FIS) in staging and then, under control, in production, to confirm that timeouts, breakers and fallbacks behave as configured.
| BảngTable | Cột chínhKey columns | Ghi chúNotes |
|---|---|---|
breaker_state | name, state, failure_rate, slow_call_rate, buffered_calls, opened_at | Trạng thái trong bộ nhớ từng instance, xuất ra metric resilience4j_circuitbreaker_state; CLOSED → OPEN → HALF_OPEN → CLOSEDIn-memory per instance, exported as the resilience4j_circuitbreaker_state metric; CLOSED → OPEN → HALF_OPEN → CLOSED |
resilience_policy | dependency, timeout_ms, max_concurrent, failure_rate_threshold, slow_call_ms, wait_open_s, half_open_calls, max_retries | Cấu hình theo dependency (application.yml hoặc config service), ví dụ reco: 300 / 20 / 50% / 250 / 30 / 3 / 0Per-dependency config (application.yml or a config service), e.g. reco: 300 / 20 / 50% / 250 / 30 / 3 / 0 |
fallback_cache | key, payload, refreshed_at, ttl_s | Redis key reco:bestsellers, job làm mới mỗi 10 phút; fallback chấp nhận dữ liệu cũ tới 24 giờRedis key reco:bestsellers, refreshed every 10 minutes; the fallback accepts data up to 24 hours old |
payment_attempt | idempotency_key, order_id, status, response, created_at | unique(idempotency_key): retry không tạo lần trừ tiền thứ haiunique(idempotency_key): a retry never creates a second charge |
Recommendation được bọc bởi timeout, breaker và bulkhead, lỗi thì trả danh sách từ cache. Payment chỉ retry vì có idempotency key và không có fallback.Recommendations are wrapped in a timeout, a breaker and a bulkhead, and fall back to a cached list. Payment retries only because it carries an idempotency key, and has no fallback.
@Slf4j
@Service
@RequiredArgsConstructor
public class CheckoutClients {
private final RestClient reco;
private final RestClient payment;
private final RecoCache cache;
// Aspect order: Retry(CircuitBreaker(TimeLimiter(Bulkhead(call))))
@TimeLimiter(name = "reco") // timeoutDuration: 300ms
@CircuitBreaker(name = "reco", fallbackMethod = "cachedPicks")
@Bulkhead(name = "reco", type = Bulkhead.Type.THREADPOOL)
public CompletableFuture<List<Item>> topPicks(long userId) {
List<Item> items = reco.get()
.uri("/users/{id}/picks", userId)
.retrieve()
.body(new ParameterizedTypeReference<List<Item>>() {});
return CompletableFuture.completedFuture(items);
}
private CompletableFuture<List<Item>> cachedPicks(long userId, Throwable t) {
log.warn("reco degraded for user {}: {}", userId, t.toString());
return CompletableFuture.completedFuture(cache.bestsellers());
}
@Retry(name = "payment") // 3 attempts, backoff + jitter
@CircuitBreaker(name = "payment") // no fallback: fail fast
public PaymentResult charge(ChargeRequest req) {
return payment.post()
.uri("/charges")
.header("Idempotency-Key", "order-" + req.orderId())
.body(req)
.retrieve()
.body(PaymentResult.class);
}
}
/* application.yml
resilience4j.circuitbreaker.instances.reco:
slidingWindowSize: 50
failureRateThreshold: 50
slowCallDurationThreshold: 250ms
slowCallRateThreshold: 60
waitDurationInOpenState: 30s
permittedNumberOfCallsInHalfOpenState: 3
resilience4j.retry.instances.payment:
maxAttempts: 3
waitDuration: 200ms
enableExponentialBackoff: true
enableRandomizedWait: true
retryExceptions: [java.io.IOException]
*/package checkout
import (
"context"
"log/slog"
"time"
"github.com/sony/gobreaker/v2"
)
type RecoClient struct {
cb *gobreaker.CircuitBreaker[[]Item]
slots chan struct{} // bulkhead: at most 20 calls in flight
api RecoAPI
cache *RecoCache
}
func NewRecoClient(api RecoAPI, cache *RecoCache) *RecoClient {
cb := gobreaker.NewCircuitBreaker[[]Item](gobreaker.Settings{
Name: "reco",
MaxRequests: 3, // probes allowed in HALF_OPEN
Interval: 60 * time.Second, // reset counts while CLOSED
Timeout: 30 * time.Second, // OPEN -> HALF_OPEN
ReadyToTrip: func(c gobreaker.Counts) bool {
return c.Requests >= 20 &&
float64(c.TotalFailures)/float64(c.Requests) >= 0.5
},
OnStateChange: func(name string, from, to gobreaker.State) {
slog.Warn("breaker", "name", name, "from", from, "to", to)
},
})
return &RecoClient{cb: cb, slots: make(chan struct{}, 20), api: api, cache: cache}
}
func (c *RecoClient) TopPicks(ctx context.Context, userID int64) []Item {
select {
case c.slots <- struct{}{}:
defer func() { <-c.slots }()
default:
return c.cache.Bestsellers() // bulkhead full: fail fast
}
ctx, cancel := context.WithTimeout(ctx, 300*time.Millisecond)
defer cancel()
items, err := c.cb.Execute(func() ([]Item, error) {
return c.api.Picks(ctx, userID)
})
if err != nil { // timeout, 5xx or gobreaker.ErrOpenState
return c.cache.Bestsellers()
}
return items
}| Fallback (giảm cấp)Fallback (degrade) | Fail fast (báo lỗi)Fail fast (return an error) | |
|---|---|---|
| Hợp vớiGood for | Gợi ý, review, banner, số đếm: thiếu vẫn mua được hàngSuggestions, reviews, banners, counters: the purchase still works without them | Thanh toán, tồn kho, xác thực: sai còn tệ hơn lỗiPayment, stock, authentication: being wrong is worse than failing |
| Người dùng thấyWhat the user sees | Trang vẫn chạy, nội dung kém cá nhân hoá hoặc cũA working page with less personal or stale content | Thông báo lỗi rõ ràng và nút thử lạiA clear error and a retry button |
| Rủi roRisk | Lỗi bị che giấu nếu không có metric tỉ lệ fallbackFailures stay hidden without a fallback-rate metric | Tỉ lệ lỗi tăng thấy rõ khi dependency gặp sự cốA visible error spike while the dependency is down |
Mỗi pattern mở bài học tương ứng trong menu Pattern.Each one opens its lesson in the Patterns menu.
Mỗi request giữ một thread và một connection trong 10 giây; pool cạn rồi mọi endpoint đều xếp hàng, kể cả endpoint không gọi service đó. Chặn bằng timeout ngắn theo ngân sách, bulkhead riêng cho từng dependency, và breaker có ngưỡng gọi chậm để ngừng gọi hẳn.Each request holds a thread and a connection for 10 seconds; the pool drains and every endpoint queues, even ones that never call that service. Stop it with short budget-based timeouts, a bulkhead per dependency, and a breaker with a slow-call threshold so calls stop entirely.
Khi thao tác không idempotent (POST trừ tiền không có idempotency key), khi lỗi là lỗi nghiệp vụ hoặc 4xx, khi breaker đang OPEN, hoặc khi deadline còn lại không đủ cho một lần thử nữa. Retry ở nhiều tầng cùng lúc cũng phải tránh vì nhân số lời gọi.When the operation is not idempotent (a charging POST without an idempotency key), when the error is a business error or a 4xx, when the breaker is OPEN, or when the remaining deadline cannot fit another attempt. Retrying at several layers at once is also out, since it multiplies calls.
Ở phía gọi (client hoặc sidecar như Envoy), mỗi instance một breaker cho mỗi dependency. Server tự bảo vệ bằng rate limit và load shedding, còn breaker bảo vệ phía gọi khỏi chờ vô ích.On the calling side (the client or a sidecar such as Envoy), one breaker per dependency per instance. The server protects itself with rate limiting and load shedding; the breaker protects the caller from waiting for nothing.
Đo: metric trạng thái breaker, tỉ lệ fallback, số lần bulkhead từ chối, p99 theo dependency. Rồi kiểm chứng bằng chaos test tiêm độ trễ và lỗi, xác nhận API vẫn trong SLO.Measure breaker state, fallback rate, bulkhead rejections and p99 per dependency. Then prove it with chaos tests that inject latency and errors, and confirm the API stays within its SLO.
Phím 1–4 để chọn đáp án, Enter để kiểm tra.Press 1–4 to choose an answer, Enter to check.
Ôn tập ngắt quãng (spaced repetition): thẻ được lặp lại sau 1 → 3 → 7 → 14 → 30 ngày để củng cố trí nhớ dài hạn.Spaced repetition: cards are repeated after 1 → 3 → 7 → 14 → 30 days to build long-term memory.