Thiết kế chịu lỗi: timeout, retry, circuit breaker và bulkhead
Xem thửTình huống: một phụ thuộc chậm, toàn bộ hệ thống chết
gdev-shop gọi nhà cung cấp thanh toán ở bước checkout. Bình thường lời gọi này mất khoảng 200ms.
Lúc 14:30, nhà cung cấp gặp sự cố: họ không lỗi, chỉ chậm — mỗi lời gọi mất 30 giây rồi mới trả về.
14:30 Nhà cung cấp thanh toán chậm: 200ms -> 30s
14:31 Tỉ lệ lỗi của gdev-shop: 0,1% -> 3%
14:32 Tỉ lệ lỗi: 3% -> 61%
14:33 Trang sản phẩm cũng không tải được
14:33 Trang tìm kiếm cũng không tải được
14:34 Toàn bộ website không phản hồi
Cấu hình liên quan:
Timeout khi gọi nhà cung cấp thanh toán : KHÔNG CÓ
Thread pool của ứng dụng : 100
Retry khi thất bại : 3 lần, không có backoff
Tỉ lệ traffic đi qua checkout : khoảng 4%
Chú ý dòng cuối: chỉ 4% traffic đi qua checkout, nhưng 100% website chết.
Dừng lại. Trả lời hai câu:
- Vì sao một phụ thuộc chỉ ảnh hưởng 4% traffic lại làm chết 100% website?
- Trong bốn dòng cấu hình, dòng nào là nguyên nhân gốc, và dòng nào khuếch đại sự cố?
Cách đọc hình kiến trúc. Nhà cung cấp thanh toán không lỗi mà chỉ chậm từ 200ms lên 30 giây. Vì không có timeout, mỗi request checkout giữ một luồng suốt 30 giây, và với pool 100 luồng dùng chung thì chỉ khoảng một phút là cả 100 luồng đều đang chờ. Request xem sản phẩm hay tìm kiếm, vốn chỉ cần 40ms, không còn luồng nào để chạy nên bị kẹt, dù checkout chỉ chiếm 4% traffic. Hai nguyên nhân gốc là không có timeout và pool dùng chung, còn retry 3 lần không backoff chỉ khuếch đại sự cố bằng cách nhân tải lên nhà cung cấp đang hấp hối.
Đang tải Animation…
Đáp án và reasoning
Câu 1 — vì tài nguyên dùng chung bị chiếm hết.
Ứng dụng có 100 luồng xử lý. Mỗi request checkout giữ một luồng suốt 30 giây vì không có timeout. Với vài request checkout mỗi giây, chỉ cần khoảng một phút là cả 100 luồng đều đang chờ nhà cung cấp thanh toán.
Lúc đó, request xem sản phẩm — vốn chỉ cần 40ms và không liên quan gì tới thanh toán — không còn luồng nào để chạy. Nó xếp hàng, rồi timeout ở phía client.
Đây chính là cơ chế đã gặp ở Capstone Chương 1 với connection pool, lặp lại ở tầng luồng xử lý: tài nguyên dùng chung biến một sự cố cục bộ thành sự cố toàn phần.
Câu 2 — phân loại bốn dòng cấu hình:
| Dòng | Vai trò |
|---|---|
| Không có timeout | Nguyên nhân gốc. Không có timeout nghĩa là một lời gọi có thể chiếm tài nguyên vô hạn |
| Thread pool dùng chung 100 luồng | Nguyên nhân gốc thứ hai. Nó cho phép một phụ thuộc bệnh tiêu hết tài nguyên của mọi tính năng |
| Retry 3 lần không backoff | Khuếch đại. Nó nhân tải lên nhà cung cấp đang hấp hối lên 3 lần, và giữ luồng lâu hơn 3 lần |
| Checkout chỉ chiếm 4% traffic | Không phải nguyên nhân — nhưng là bằng chứng cho thấy mức độ ảnh hưởng hoàn toàn không tương xứng với quy mô nguồn gốc |
Nguyên tắc số một của thiết kế chịu lỗi: mọi lời gọi ra ngoài phải có giới hạn — giới hạn thời gian, giới hạn số lần thử, và giới hạn tài nguyên được phép tiêu.
Timeout: giới hạn đầu tiên và quan trọng nhất
Một lời gọi mạng không có timeout là một lời gọi có thể không bao giờ kết thúc. Đọc lại sơ đồ đầu bài: toàn bộ chuỗi sự cố bắt đầu từ đó.
Đặt timeout bao nhiêu? Câu trả lời sai phổ biến là "30 giây cho an toàn". Cách đúng:
Timeout nên đặt quanh p99 của lời gọi đó trong điều kiện bình thường, cộng một biên nhỏ.
Ví dụ: nhà cung cấp thanh toán có p99 = 800ms
-> timeout khoảng 1,5 - 2 giây, KHÔNG phải 30 giây
Lý do: nếu lời gọi đã chậm gấp nhiều lần p99, gần như chắc chắn có sự cố — chờ thêm 28 giây nữa không làm tăng xác suất thành công, nó chỉ làm bạn giữ tài nguyên lâu hơn.
Ba loại timeout cần phân biệt, và thiếu loại nào cũng để lại lỗ hổng:
| Loại | Giới hạn điều gì | Lỗ hổng nếu thiếu |
|---|---|---|
| Connect timeout | Thời gian thiết lập kết nối | Kết nối tới một máy đã chết có thể treo rất lâu |
| Read/socket timeout | Thời gian chờ dữ liệu về | Kết nối mở nhưng phía kia im lặng vĩnh viễn |
| Timeout tổng cho cả thao tác | Toàn bộ thời gian, gồm cả các lần retry | 3 lần retry × 2 giây = 6 giây, dù mỗi lần đều "đúng timeout" |
Loại thứ ba hay bị bỏ sót nhất. Nếu bạn đặt timeout 2 giây cho mỗi lần gọi và retry 3 lần, tổng thời gian tệ nhất là 6 giây cộng thời gian chờ giữa các lần — trong khi người dùng có thể đã bỏ đi từ giây thứ ba.
Và một nguyên tắc quan trọng khi có nhiều tầng gọi nhau: timeout của tầng gọi phải ngắn hơn timeout của tầng gọi nó. Nếu gateway đặt timeout 5 giây mà service đặt 10 giây, gateway sẽ bỏ cuộc trước, còn service vẫn tiếp tục giữ tài nguyên để hoàn thành một công việc mà không còn ai chờ kết quả.
Cách đọc hình kiến trúc. Hình trên: connect timeout giới hạn thời gian thiết lập kết nối, read timeout giới hạn việc chờ dữ liệu từ một phía im lặng, và timeout tổng cho cả thao tác, gồm cả các lần retry, vì 3 lần retry mỗi lần 2 giây có thể thành 6 giây. Giá trị nên quanh p99 cộng một biên nhỏ (p99 800ms thì khoảng 1,5 đến 2 giây), không phải 30 giây. Hình dưới: khi các tầng gọi nhau, timeout của tầng bên trong phải ngắn hơn timeout của tầng bên ngoài đang chờ nó, nếu không gateway bỏ cuộc trước trong khi service vẫn giữ tài nguyên để làm việc không còn ai chờ.
Đang tải Animation…
Retry: cứu bạn khỏi lỗi thoáng qua, hoặc giết bạn nhanh hơn
Retry hữu ích với lỗi thoáng qua: một gói tin rớt, một node vừa restart, một timeout ngẫu nhiên. Nó vô dụng và có hại với lỗi kéo dài: dịch vụ quá tải, dữ liệu sai định dạng, lỗi xác thực.
Ba quy tắc, thiếu quy tắc nào cũng biến retry thành vũ khí tự sát:
1. Exponential backoff kèm jitter. Chờ 1s, 2s, 4s — nhưng phải cộng thêm một lượng ngẫu nhiên. Không có jitter, mọi client thất bại cùng lúc sẽ retry cùng lúc, tạo ra các đợt sóng đồng bộ đập vào dịch vụ đang cố hồi phục. Đây chính xác là bài học từ bão kết nối lại ở Chương 9.
2. Retry budget, không chỉ giới hạn số lần. "Retry tối đa 3 lần mỗi request" vẫn cho phép nhân ba toàn bộ tải khi mọi request đều thất bại. Giới hạn đúng là ở mức hệ thống: "tổng số lần retry không vượt quá 10% tổng số request". Khi tỉ lệ lỗi tăng cao, ngân sách cạn và hệ thống tự động ngừng retry thay vì dội thêm.
3. Chỉ retry những gì an toàn khi lặp lại. Với thao tác không idempotent (tạo đơn hàng, trừ tiền), retry sau timeout có thể tạo bản ghi trùng — và bạn không biết lần đầu đã thành công hay chưa, đúng bài học partial failure ở Chương 1. Retry chỉ an toàn khi thao tác idempotent hoặc có khóa idempotency.
Retry không có giới hạn tổng thể là cách biến một sự cố suy giảm thành một cuộc tấn công từ chối dịch vụ do chính bạn thực hiện.
Cách đọc hình kiến trúc. Retry chỉ hữu ích với lỗi thoáng qua. Quy tắc 1: exponential backoff kèm jitter để các client thất bại cùng lúc không retry cùng lúc tạo sóng đồng bộ. Quy tắc 2: retry budget ở mức hệ thống (tổng retry không quá 10% tổng request) để hệ thống tự ngừng retry khi lỗi tăng, vì giới hạn 3 lần mỗi request vẫn nhân ba tải khi mọi request đều lỗi. Quy tắc 3: chỉ retry thao tác idempotent hoặc có khóa idempotency, vì sau timeout không biết lần đầu đã thành công chưa. Thiếu các quy tắc này, retry gây tấn công từ chối dịch vụ do chính bạn thực hiện.
Đang tải Animation…
Đang tải sơ đồ…
Circuit breaker: ngừng gọi thứ đang chết
Timeout giới hạn một lời gọi. Circuit breaker giới hạn việc tiếp tục gọi khi đã rõ phía kia đang có vấn đề.
Đọc sơ đồ vòng đời: ba trạng thái và lý do tồn tại của từng trạng thái.
- Closed — bình thường, mọi lời gọi đi qua, breaker đếm tỉ lệ thất bại.
- Open — tỉ lệ lỗi vượt ngưỡng, breaker fail fast ngay lập tức mà không gọi ra ngoài. Hai lợi ích cùng lúc: người dùng nhận lỗi trong vài mili-giây thay vì chờ timeout, và dịch vụ đang ốm được để yên để hồi phục thay vì tiếp tục bị dội.
- Half-open — sau thời gian nguội, cho một lời gọi thử đi qua. Thành công thì đóng lại, thất bại thì mở tiếp.
Lợi ích thứ hai của trạng thái Open là thứ hay bị bỏ qua: một dịch vụ quá tải cần giảm tải để hồi phục. Nếu mọi client tiếp tục gọi và retry, nó không bao giờ có cơ hội đứng dậy.
Ba tham số cần cân nhắc, và cả ba đều là đánh đổi:
| Tham số | Đặt quá thấp | Đặt quá cao |
|---|---|---|
| Ngưỡng tỉ lệ lỗi | Mở breaker vì vài lỗi ngẫu nhiên, chặn nhầm dịch vụ khỏe | Mở quá muộn, tài nguyên đã bị tiêu hết |
| Thời gian nguội | Thử lại quá sớm, dịch vụ chưa kịp hồi phục | Dịch vụ đã khỏe từ lâu mà vẫn bị chặn |
| Số lượng mẫu tối thiểu | Mở breaker dựa trên 2-3 request, thống kê vô nghĩa | Cần quá nhiều request lỗi mới phản ứng |
Và câu hỏi quan trọng nhất: khi breaker mở, bạn trả về gì cho người dùng? Đây là quyết định nghiệp vụ, không phải kỹ thuật:
- Gợi ý sản phẩm không khả dụng: trả về danh sách rỗng, trang vẫn hiển thị bình thường (suy giảm có kiểm soát, đúng bài học ở Chương 1).
- Thanh toán không khả dụng: phải báo lỗi rõ ràng, không được âm thầm bỏ qua.
Cách đọc hình kiến trúc. Closed là trạng thái bình thường, mọi lời gọi đi qua và breaker đếm tỉ lệ thất bại. Khi tỉ lệ vượt ngưỡng, breaker chuyển sang Open: fail fast ngay, người dùng nhận lỗi trong vài mili-giây thay vì chờ timeout, và dịch vụ đang ốm được để yên để hồi phục. Sau thời gian nguội, breaker sang Half-open và cho một lời gọi thử đi qua: thành công thì đóng lại, thất bại thì mở tiếp. Khi breaker mở, quyết định trả về gì là quyết định nghiệp vụ: gợi ý sản phẩm trả danh sách rỗng, còn thanh toán phải báo lỗi rõ ràng.
Đang tải Animation…
Cách đọc hình kiến trúc. Hình trên: không có bulkhead, mọi lời gọi ra ngoài dùng chung một pool 100 luồng, gợi ý sản phẩm treo ăn hết 100 luồng và checkout, tính năng quan trọng nhất, chết theo dù bản thân nó khỏe. Hình dưới: có bulkhead, checkout có pool riêng 80 luồng còn gợi ý sản phẩm chỉ được 20 luồng nên treo thì chỉ mất 20 luồng đó. Ngân sách phân bổ theo mức độ quan trọng của tính năng, không theo lượng traffic, và bulkhead còn áp dụng ở pool kết nối database, instance riêng và queue riêng cho từng loại công việc.
Đang tải Animation…
Failure Lab: bulkhead — vì sao 4% traffic làm chết 100% hệ thống
Đọc hình phía trên: tên gọi này mượn từ ngành đóng tàu — thân tàu được chia thành các khoang kín, nên một khoang thủng không làm chìm cả con tàu.
Không có bulkhead: mọi lời gọi ra ngoài dùng chung một pool 100 luồng. Gợi ý sản phẩm treo, nó ăn hết 100 luồng, và checkout — tính năng quan trọng nhất — chết theo dù bản thân nó hoàn toàn khỏe mạnh.
Có bulkhead: mỗi phụ thuộc có ngân sách tài nguyên riêng. Gợi ý sản phẩm chỉ được dùng tối đa 20 luồng; nó treo thì mất đúng 20 luồng đó, còn checkout vẫn có 80 luồng của mình.
Điểm quan trọng nhất về mặt thiết kế: phân bổ ngân sách theo mức độ quan trọng của tính năng, không theo lượng traffic. Gợi ý sản phẩm có thể chiếm nhiều traffic hơn checkout, nhưng nó phải được cấp ít tài nguyên hơn — vì mất gợi ý là phiền toái, mất checkout là mất doanh thu.
Bulkhead có thể được áp dụng ở nhiều tầng, và càng nhiều tầng càng an toàn:
| Tầng | Cách thực hiện |
|---|---|
| Thread pool / connection pool | Pool riêng cho mỗi phụ thuộc hoặc mỗi nhóm tính năng |
| Kết nối database | Pool riêng cho luồng quan trọng, như đã làm ở Capstone Chương 1 |
| Tiến trình hoặc instance | Tách hẳn tính năng quan trọng sang cụm riêng |
| Hàng đợi | Queue riêng cho mỗi loại công việc, để một loại bị backlog không chặn loại khác |
Bốn mẫu hình này hoạt động cùng nhau, không thay thế nhau:
Timeout -> giới hạn MỘT lời gọi
Retry + budget -> xử lý lỗi thoáng qua mà không khuếch đại sự cố
Circuit breaker -> ngừng gọi khi đã rõ phía kia đang hỏng
Bulkhead -> giới hạn thiệt hại khi ba lớp trên đều không đủ
Áp dụng cả bốn vào sự cố ở đầu bài: timeout 2 giây thay vì vô hạn; retry có budget thay vì mù quáng 3 lần; breaker mở sau khi tỉ lệ lỗi vượt ngưỡng nên ngừng dội vào nhà cung cấp; và bulkhead đảm bảo checkout dù có chết thì trang sản phẩm và tìm kiếm vẫn hoạt động bình thường.
Hiểu lầm cần phá bỏ
Hiểu lầm 1 — "Đặt timeout 30 giây cho an toàn"
Nếu lời gọi đã chậm gấp nhiều lần p99 thì gần như chắc chắn có sự cố; chờ thêm không tăng xác suất thành công mà chỉ giữ tài nguyên lâu hơn. Timeout nên quanh p99 cộng một biên nhỏ.
Hiểu lầm 2 — "Giới hạn 3 lần retry là đã an toàn"
Nó vẫn cho phép nhân ba toàn bộ tải khi mọi request đều lỗi. Cần retry budget ở mức hệ thống để tự động ngừng retry khi tỉ lệ lỗi tăng cao.
Hiểu lầm 3 — "Retry giúp hệ thống đáng tin cậy hơn"
Chỉ với lỗi thoáng qua. Với dịch vụ đang quá tải, retry là thứ ngăn nó hồi phục. Và với thao tác không idempotent, retry sau timeout có thể tạo bản ghi trùng.
Hiểu lầm 4 — "Circuit breaker chỉ để bảo vệ phía gọi"
Lợi ích quan trọng không kém là để dịch vụ đang ốm được yên mà hồi phục. Nếu mọi client tiếp tục gọi, nó không bao giờ có cơ hội đứng dậy.
Hiểu lầm 5 — "Phân bổ tài nguyên theo lượng traffic là công bằng"
Bulkhead phải phân bổ theo mức độ quan trọng của tính năng. Gợi ý sản phẩm có thể nhiều traffic hơn checkout nhưng phải được cấp ít tài nguyên hơn.
Hiểu lầm 6 — "Có circuit breaker rồi thì không cần bulkhead"
Breaker chỉ mở sau khi đã có đủ lỗi để vượt ngưỡng. Trong khoảng thời gian trước đó, tài nguyên dùng chung vẫn có thể bị tiêu hết. Bulkhead là lớp bảo vệ khi các lớp trên chưa kịp phản ứng.
Góc nhìn phỏng vấn System Design
Đây là nhóm câu hỏi gần như chắc chắn xuất hiện ngay khi bạn vẽ một mũi tên từ service này sang service khác.
Interviewer có thể hỏi
- "Nhà cung cấp thanh toán chậm 30 giây. Chuyện gì xảy ra với hệ thống của bạn?"
- "Timeout bạn đặt bao nhiêu và dựa vào đâu?"
- "Bạn retry 3 lần. Khi 100% request đều lỗi thì tải lên dịch vụ đó là bao nhiêu?"
- "Khi circuit breaker mở, người dùng thấy gì?" — câu này kiểm tra bạn có nghĩ tới suy giảm có kiểm soát không.
- "Vì sao một tính năng chiếm 4% traffic lại làm chết cả hệ thống?"
Câu trả lời yếu và câu trả lời tốt hơn
Yếu: "Em thêm retry và circuit breaker để hệ thống chịu lỗi tốt hơn."
Tốt hơn: "Đầu tiên là timeout, vì không có timeout thì một lời gọi có thể chiếm tài nguyên vô hạn — em đặt quanh p99 của lời gọi đó cộng một biên nhỏ, và timeout của tầng gọi phải ngắn hơn tầng gọi nó để không làm việc mà không còn ai chờ. Retry em dùng exponential backoff có jitter, và quan trọng hơn là retry budget ở mức hệ thống thay vì chỉ giới hạn số lần, vì 3 lần retry vẫn nhân ba tải khi mọi request đều lỗi. Em chỉ retry thao tác idempotent. Circuit breaker để fail fast và quan trọng không kém là để dịch vụ đang ốm được yên mà hồi phục; khi breaker mở em quyết định theo tính năng — gợi ý sản phẩm trả về rỗng và trang vẫn hiển thị, còn thanh toán thì phải báo lỗi rõ ràng. Cuối cùng là bulkhead: pool tài nguyên riêng cho từng phụ thuộc, phân bổ theo mức độ quan trọng chứ không theo traffic, để một phụ thuộc chiếm 4% traffic không thể làm chết 100% hệ thống."
Tự kiểm tra — không nhìn lại bài
- Vì sao một phụ thuộc chỉ ảnh hưởng 4% traffic lại làm chết 100% website? Cơ chế cụ thể là gì?
- Timeout nên đặt dựa trên con số nào? Vì sao "30 giây cho an toàn" là sai?
- Kể ba loại timeout và lỗ hổng nếu thiếu từng loại.
- Vì sao timeout của tầng gọi phải ngắn hơn tầng gọi nó?
- "Retry tối đa 3 lần" vẫn nguy hiểm ở điểm nào? Giới hạn đúng là gì?
- Vì sao jitter là bắt buộc khi retry?
- Nêu hai lợi ích của trạng thái Open trong circuit breaker.
- Bulkhead phân bổ tài nguyên theo tiêu chí nào? Vì sao không theo lượng traffic?
- Vì sao có circuit breaker rồi vẫn cần bulkhead?
Chuyển sang bài toán khác
gdev-feed gọi một dịch vụ xếp hạng bên ngoài để sắp xếp bài viết trong feed. Dịch vụ này nằm trên đường đi của 95% traffic.
- Nếu dịch vụ xếp hạng chậm, bạn đặt timeout bao nhiêu — và khi timeout thì hiển thị gì cho người dùng?
- So sánh với
gdev-shop: ở đó phụ thuộc thanh toán chỉ ảnh hưởng 4% traffic nhưng không thể suy giảm. Ở đây phụ thuộc ảnh hưởng 95% traffic nhưng có thể suy giảm. Điều đó thay đổi thiết kế chịu lỗi như thế nào? - Nếu circuit breaker mở cho dịch vụ xếp hạng, feed nên hiển thị theo thứ tự gì?
Đáp án chi tiết cho các câu hỏi trong bài
Đáp án cho phần Tự kiểm tra
- Cơ chế là tài nguyên dùng chung bị chiếm hết: không có timeout nên mỗi request checkout giữ một luồng suốt 30 giây; với pool dùng chung 100 luồng, chỉ khoảng một phút là cả 100 luồng đều chờ nhà cung cấp thanh toán. Request xem sản phẩm (40ms, không liên quan thanh toán) không còn luồng nào, xếp hàng rồi timeout ở phía client. Sự cố cục bộ thành sự cố toàn phần.
- Timeout nên đặt quanh p99 của lời gọi đó trong điều kiện bình thường cộng một biên nhỏ (p99 800ms thì khoảng 1,5 đến 2 giây). "30 giây cho an toàn" là sai vì nếu lời gọi đã chậm gấp nhiều lần p99 thì gần như chắc chắn có sự cố; chờ thêm không tăng xác suất thành công mà chỉ giữ tài nguyên lâu hơn.
- Connect timeout (giới hạn thiết lập kết nối; thiếu thì kết nối tới máy đã chết treo rất lâu), read/socket timeout (giới hạn chờ dữ liệu; thiếu thì kết nối mở nhưng phía kia im lặng vĩnh viễn), timeout tổng cho cả thao tác (gồm cả retry; thiếu thì 3 lần retry × 2 giây là 6 giây dù mỗi lần đều đúng timeout).
- Nếu timeout của tầng bên trong dài hơn tầng bên ngoài đang chờ nó (ví dụ gateway 5 giây, service 10 giây), gateway bỏ cuộc trước trong khi service vẫn giữ tài nguyên để hoàn thành một việc không còn ai chờ kết quả. Vì vậy mỗi lời gọi lồng bên trong phải có timeout ngắn hơn timeout của tầng bên ngoài đang chờ nó.
- Giới hạn 3 lần mỗi request vẫn cho phép nhân ba toàn bộ tải khi mọi request đều thất bại. Giới hạn đúng là ở mức hệ thống, retry budget: tổng số lần retry không vượt quá khoảng 10% tổng số request; khi tỉ lệ lỗi tăng cao ngân sách cạn và hệ thống tự ngừng retry.
- Không có jitter, mọi client thất bại cùng lúc sẽ retry cùng lúc, tạo sóng đồng bộ đập vào dịch vụ đang cố hồi phục (như bão kết nối lại ở chương 9). Jitter trải các lần retry ra theo thời gian.
- Hai lợi ích của Open: (1) fail fast: người dùng nhận lỗi trong vài mili-giây thay vì chờ timeout, và tài nguyên của phía gọi được giải phóng; (2) dịch vụ đang ốm được để yên để hồi phục thay vì tiếp tục bị dội.
- Theo mức độ quan trọng của tính năng, không theo lượng traffic. Gợi ý sản phẩm có thể nhiều traffic hơn checkout nhưng phải được cấp ít tài nguyên hơn, vì mất gợi ý là phiền toái còn mất checkout là mất doanh thu.
- Breaker chỉ mở sau khi đã có đủ lỗi vượt ngưỡng; trước đó tài nguyên dùng chung vẫn có thể bị tiêu hết. Bulkhead là lớp bảo vệ khi các lớp trên chưa kịp phản ứng hoặc không phát hiện được kiểu lỗi đó.
Đáp án cho bài toán chuyển sang gdev-feed
- Timeout: đặt quanh p99 của dịch vụ xếp hạng cộng biên nhỏ, khá chặt vì nó nằm trên đường đi nóng của 95% traffic và người dùng đang chờ (ví dụ p99 là 150ms thì timeout khoảng 250 đến 300ms). Khi timeout, hiển thị feed dự phòng (thứ tự thời gian mới nhất trước, hoặc thứ hạng đã tính gần nhất) chứ không báo lỗi.
- So sánh với gdev-shop: thanh toán chỉ ảnh hưởng 4% traffic nhưng không thể suy giảm (phải báo lỗi rõ ràng), còn xếp hạng ảnh hưởng 95% traffic nhưng có thể suy giảm âm thầm. Với xếp hạng, mục tiêu là làm cho sự cố vô hình với người dùng: timeout chặt, breaker kèm giá trị dự phòng, và vì lưu lượng lớn nên pool cạn rất nhanh, nên bulkhead và fail fast càng quan trọng. Với thanh toán, bulkhead bảo vệ các tính năng khác còn bản thân nó hiển thị lỗi rõ ràng.
- Breaker mở: feed hiển thị theo thứ tự thời gian giảm dần (mới nhất trước) từ các tài khoản được theo dõi, có thể kèm thứ hạng đã cache gần nhất của từng người, và phương án dự phòng này không được phụ thuộc vào dịch vụ xếp hạng.
Đáp án gợi ý cho các câu hỏi phỏng vấn
- "Nhà cung cấp thanh toán chậm 30 giây thì sao?" Không có timeout thì mỗi lời gọi giữ một luồng 30 giây, pool dùng chung cạn trong khoảng một phút và cả hệ thống chết. Cần timeout, retry có budget, circuit breaker và bulkhead.
- "Timeout đặt bao nhiêu?" Quanh p99 của lời gọi cộng biên nhỏ; timeout bên trong ngắn hơn timeout bên ngoài đang chờ để không làm việc mà không còn ai chờ.
- "Retry 3 lần, khi 100% request lỗi thì tải lên dịch vụ đó?" Gấp khoảng 3 đến 4 lần tải thường tùy cách đếm lần thử đầu; vì vậy cần retry budget ở mức hệ thống thay vì chỉ giới hạn số lần.
- "Khi breaker mở người dùng thấy gì?" Tùy tính năng: gợi ý sản phẩm trả danh sách rỗng và trang vẫn hiển thị, còn thanh toán phải báo lỗi rõ ràng, không được âm thầm bỏ qua.
- "Vì sao tính năng 4% traffic làm chết cả hệ thống?" Vì pool luồng dùng chung bị chiếm hết bởi lời gọi treo, các tính năng không liên quan không còn luồng; bulkhead tách ngân sách theo mức độ quan trọng sẽ chặn điều này.
Nếu chỉ nhớ 3 điều
- Mọi lời gọi ra ngoài phải có giới hạn: giới hạn thời gian, giới hạn số lần thử ở mức hệ thống, và giới hạn tài nguyên được phép tiêu.
- Retry không có budget và jitter là cách biến một sự cố suy giảm thành cuộc tấn công từ chối dịch vụ do chính bạn thực hiện.
- Bulkhead phân bổ tài nguyên theo mức độ quan trọng của tính năng — đó là thứ quyết định một phụ thuộc bệnh có kéo sập cả hệ thống hay không.