[Microservices Series] Bài 6: Circuit Breaker – Chiếc "Aptomat" cắt điện để cứu cả tòa nhà Microservices
Chào anh em! Ở cuối bài 5, mình đã đặt ra một câu hỏi: Lỡ một Service bị treo thì sao? Trong thế giới Microservices, một Service "chết hẳn" (crash và sập luôn) đôi khi lại không đáng sợ bằng việc nó "ngắc ngoải" (sống nhưng phản hồi cực kỳ chậm).
Hôm nay chúng ta sẽ tìm hiểu về một cơn ác mộng mang tên Cascading Failure (Sụp đổ dây chuyền) và cách dùng Circuit Breaker (Aptomat tự ngắt) để cứu vãn tình thế.
1. Cơn ác mộng mang tên "Sụp đổ dây chuyền"
Hãy tưởng tượng bạn có 3 Service: Gate (Mở cổng), Billing (Trừ tiền) và Notification (Gửi SMS/App push). Luồng đi là: Khách quẹt thẻ -> Gate gọi Billing -> Billing gọi Notification.
Một ngày đẹp trời, hệ thống SMS của đối tác bị lỗi, khiến Service Notification thay vì phản hồi trong 50ms thì giờ mất tới 10 giây mới timeout. Điều tồi tệ gì sẽ xảy ra? Bởi vì Notification chậm, nên Billing phải đứng chờ nó 10 giây. Bởi vì Billing đứng chờ, nên Gate cũng phải đứng khựng lại 10 giây. Lúc này, mỗi request đi vào sẽ ngốn một Thread (tiểu trình) hoặc Connection trên server. Với hàng ngàn lượt khách quẹt thẻ mỗi phút, toàn bộ Thread Pool và RAM của Gate lẫn Billing sẽ bị vắt kiệt chỉ trong vài giây. Kết quả? Toàn bộ hệ thống Metro tê liệt, khách hàng kẹt cứng ở cổng soát vé chỉ vì... hệ thống SMS bị lỗi!
Đó chính là Cascading Failure. Một mắt xích yếu rớt mạng kéo theo toàn bộ hệ thống sập tiệm.
2. Circuit Breaker – Ngắt điện để bảo toàn tính mạng
Trong điện dân dụng, khi một thiết bị chập mạch gây quá tải, chiếc Aptomat sẽ lập tức "nhảy" (ngắt dòng điện) để chống cháy nổ cả ngôi nhà. Trong Microservices, mô hình Circuit Breaker hoạt động y hệt vậy với 3 trạng thái:
-
Closed (Đóng mạch): Đây là trạng thái bình thường. Dòng điện (Request) đi qua suôn sẻ từ Service A sang Service B.
-
Open (Ngắt mạch): Khi Service B bắt đầu lỗi hoặc timeout vượt quá một ngưỡng cho phép (ví dụ: lỗi 50% trong 100 request gần nhất), Circuit Breaker sẽ "nhảy" sang Open. Lúc này, bất kỳ request nào từ A gọi sang B đều bị chặn lại ngay lập tức (Fail-fast) mà không cần phải chờ đi qua mạng. A sẽ trả về lỗi ngay lập tức hoặc chạy logic dự phòng (Fallback). Nhờ vậy, A không bị cạn kiệt tài nguyên.
-
Half-Open (Thử tải): Sau một khoảng thời gian chờ (ví dụ 30 giây), Circuit Breaker sẽ hé mở một chút. Nó cho phép một lượng nhỏ request đi qua để "thăm dò" xem Service B đã sống lại chưa. Nếu request thành công, nó sẽ đóng lại (Closed) để hệ thống hoạt động bình thường. Nếu vẫn lỗi, nó dập xuống lại (Open) và tiếp tục chờ.
3. Thực chiến tại hệ thống AFC (Metro)
Áp dụng vào case study ở trên, khi mình thiết kế Gate Service (viết bằng Go) gọi sang các Service khác, mình luôn bọc các request HTTP/gRPC qua một thư viện Circuit Breaker (như gobreaker trong Go).
Khi hệ thống Notification bị lỗi:
-
Vài chục request đầu tiên bị timeout. Circuit Breaker đếm số lượng lỗi.
-
Đạt ngưỡng, Circuit Breaker "nhảy" sang Open.
-
Các lượt quẹt thẻ tiếp theo gọi tới, Gate Service thấy mạch đang Open nên lập tức chạy vào hàm Fallback.
-
Trong hàm Fallback, thay vì bắt khách chờ, Gate bỏ qua luôn bước gửi tin nhắn SMS, chỉ ghi log lại và lập tức mở cổng cho khách qua.
-
Nhờ vậy, Gate Service vẫn xử lý được hàng ngàn request/giây mà không bị kẹt Thread, giảm thiểu ùn tắc tại nhà ga.
Kinh nghiệm "xương máu"
-
Luôn phải có Fallback: Cắt điện rồi thì phải có phương án dự phòng. Nếu Service Khuyến mãi lỗi, hàm Fallback nên trả về giá mặc định (không khuyến mãi) thay vì báo lỗi cho khách hàng. Nếu Service Lịch sử giao dịch lỗi, hãy trả về danh sách rỗng kèm dòng chữ "Dữ liệu đang được cập nhật" chứ đừng quăng màn hình trắng bóc.
-
Thiết lập Timeout hợp lý: Circuit Breaker chỉ đếm lỗi khi request thực sự bị timeout hoặc văng exception. Nếu bạn để timeout HTTP là 30 giây (như mặc định của PHP/Nginx) thì hệ thống đã sập trước khi Circuit Breaker kịp nhảy rồi. Hãy ép timeout nội bộ xuống mức rất thấp (1-2 giây).
Tạm kết
Microservices là chấp nhận sự không hoàn hảo. Network sẽ chậm, Server sẽ sập, và Bug sẽ xuất hiện. Việc của một Architect là thiết kế sao cho khi một phần cơ thể chảy máu, chúng ta có thể "garo" nó lại kịp thời để hệ thống vẫn tiếp tục sống sót và phục vụ người dùng.
Sau khi đã có Circuit Breaker để bảo vệ nội bộ, chúng ta cần một người "bảo vệ" ở vòng ngoài cùng, đứng chặn giữa hàng triệu người dùng và mớ hỗn độn Microservices bên trong.
Ở bài tiếp theo (Bài 7), mình sẽ mổ xẻ về API Gateway – Người gác cổng vĩ đại, nơi sẽ gánh vác các trọng trách như Authentication, Rate Limiting và Routing.
Anh em đã từng bị một tính năng "nhỏ xíu xiu" kéo sập cả hệ thống to bự chưa? Lúc đó anh em xử lý thế nào? Cùng chia sẻ ở phần comment nhé!
All rights reserved