Phần 23 kết bằng một câu: timeout không dừng được việc đang chạy, nên nó không giảm tải. Bài này đo thứ duy nhất làm được điều đó.

Tình huống

Một dịch vụ hạ nguồn treo — còn sống, nhận kết nối, nhưng không bao giờ trả lời. Đây là kiểu hỏng khó chịu nhất, và phần 11 đo được rằng nếu không có phép chờ thì nó làm mọi thứ về không.

Ta so hai cách, cùng 30 request, phép chờ 500 ms cho cả hai:

Kết quả Tổng thời gian Số lần thật sự chạm hạ nguồn
Chỉ có timeout 0 thành công / 30 lỗi 15,1 s 30 / 30
Có circuit breaker 0 thành công / 30 lỗi 2,5 s 5 / 30

Cả hai đều thất bại hết — hạ nguồn đang chết, không ai cứu được điều đó. Khác biệt nằm ở hai cột phải:

Nhanh hơn sáu lần khi báo thất bại. Sau năm lần hỏng, mạch mở và mọi lời gọi sau đó thất bại ngay lập tức thay vì chờ đủ 500 ms.

Đập vào hạ nguồn ít hơn sáu lần. Đây mới là điều quan trọng. Một dịch vụ đang ốm cần được để yên để hồi phục; ba mươi request nữa dội vào là cách chắc chắn để nó không bao giờ đứng dậy nổi.

Đây chính xác là thứ mà timeout không làm được. Timeout bảo vệ bên gọi; circuit breaker bảo vệ bên bị gọi. Hai việc khác nhau, và bạn cần cả hai.

Ba trạng thái, đo bằng chính máy chủ

CircuitBreaker cb = CircuitBreaker.create("ha-nguon", vertx, new CircuitBreakerOptions()
        .setMaxFailures(5)
        .setTimeout(500)
        .setResetTimeout(2000));
hạ nguồn chết   -> trạng thái đã đi qua: [MỞ]
hạ nguồn sống lại, sau resetTimeout -> [ĐÓNG], 20/20 thành công, trung vị 1 ms

Đóng là bình thường. Đủ maxFailures lần hỏng thì sang mở — mọi lời gọi bị từ chối ngay, không chạm tới hạ nguồn. Sau resetTimeout, mạch sang nửa mở: cho một lời gọi thăm dò đi qua. Thành công thì đóng lại, hỏng thì mở tiếp.

Phép đo cho thấy vòng hồi phục hoạt động đúng: hạ nguồn sống lại, đợi qua resetTimeout, và 20 request tiếp theo thành công hết với độ trễ 1 ms.

Ba mặc định nên đổi ngay

maxFailures  mặc định = 5
timeout      mặc định = 10 000 ms
resetTimeout mặc định = 30 000 ms

timeout mười giây dài hơn phép chờ của gần như mọi client đứng trước bạn. Đặt nó theo ngân sách độ trễ thật, thường là vài trăm mili giây — và phần 23 nhắc rằng phép chờ phải giảm dần từ ngoài vào trong.

resetTimeout ba mươi giây nghĩa là sau khi hạ nguồn hồi phục, bạn còn phải chờ tới nửa phút nữa mới thử lại. Với dịch vụ hay chớp tắt, con số vài giây hợp lý hơn.

maxFailures bằng 5 hợp lý cho lưu lượng thấp, nhưng ở mười nghìn request mỗi giây thì năm lần hỏng xảy ra trong nửa mili giây — mạch sẽ mở vì một trục trặc thoáng qua. Cân nhắc tăng lên, hoặc dùng setFailuresRollingWindow.

Fallback chỉ chạy khi mạch đã mở

lần 1: null               sau  301 ms
lần 2: null               sau  301 ms
lần 3: gia-tri-du-phong   sau    0 ms
lần 4: gia-tri-du-phong   sau    0 ms

Hai lần đầu vẫn thất bại thật; chỉ từ lần thứ ba — khi mạch đã mở — fallback mới trả về giá trị dự phòng, và trả tức thì.

Lý do nằm ở một mặc định: isFallbackOnFailure = false. Muốn fallback che cả những lần hỏng trước khi mạch mở thì phải bật nó lên:

new CircuitBreakerOptions().setFallbackOnFailure(true);

Đây là lựa chọn thật sự: bật thì người dùng không bao giờ thấy lỗi, nhưng bạn cũng khó nhận ra hạ nguồn đang hỏng vì mọi thứ trông vẫn "thành công".

Đặt ở đâu

Một breaker cho mỗi phụ thuộc, không phải một cái dùng chung. Nếu dịch vụ thanh toán ốm thì lời gọi sang dịch vụ kho không việc gì phải bị chặn — và đó cũng là ý tưởng của bulkhead ở bài 31.

Bọc lời gọi ra ngoài, đừng bọc logic nội bộ. Circuit breaker dành cho thứ có thể ốm độc lập với bạn.

Ghi log mỗi lần đổi trạng thái. openHandler bắn đúng lúc bạn cần biết, và nó là tín hiệu sớm hơn mọi biểu đồ độ trễ.

Bài sau: Vert.x Config — nhiều nguồn cấu hình, thứ tự ưu tiên, và một dòng thiếu làm việc nạp nóng không bao giờ chạy.

Thử ba mươi giây

cb.openHandler(v -> log.error("MACH MO: {} dang hong", ten))
  .closeHandler(v -> log.info("MACH DONG: {} da hoi phuc", ten));

Hai dòng này biến circuit breaker thành hệ thống cảnh báo sớm của bạn. Nếu log của bạn chưa bao giờ có dòng nào như vậy, hoặc là mọi thứ đang rất tốt — hoặc là bạn chưa bọc lời gọi nào cả.