Chương 54. Failure modes

Ở các chương trước, ta đã nói về cách quan sát hệ thống:

  • Logs để biết chuyện gì đã xảy ra.
  • Metrics để biết hệ thống đang khỏe hay yếu.
  • Tracing để biết một request/job đi qua đâu và tốn thời gian ở đâu.
  • Alerting và SLO để biết khi nào cần gọi người xử lý.

Nhưng muốn vận hành hệ thống tốt, ta còn cần hiểu một thứ rất quan trọng:

Hệ thống thường hỏng theo những kiểu nào?

Đó là failure modes.

Failure mode là một kiểu hỏng quen thuộc.

Không phải mỗi hệ thống hỏng một kiểu hoàn toàn mới.

Thực tế, rất nhiều sự cố production lặp lại theo vài mẫu:

Retry quá nhiều.
Nhiều request cùng đổ vào một lúc.
Một service hỏng kéo service khác hỏng theo.
Queue phình lên.
Database nghẹt.
Nhà cung cấp bên ngoài chậm hoặc chết.
Một phần hệ thống chết nhưng phần khác vẫn sống.
Hệ thống không biết hạ cấp mềm.

Nếu chưa biết các mẫu này, ta rất dễ sửa sai.

Ví dụ:

AI chấm bài bị timeout.
Ta tăng retry.
Retry nhiều hơn.
Queue nhiều hơn.
Worker bận hơn.
Database ghi log nhiều hơn.
API chậm hơn.
User bấm nộp lại.
Hệ thống càng nghẹt hơn.

Ban đầu chỉ là một lỗi nhỏ.

Nhưng vì phản ứng sai, nó biến thành sự cố lớn.

Thông điệp chính của chương:

> Hệ thống tốt không phải là hệ thống không bao giờ lỗi. Hệ thống tốt là hệ thống hiểu lỗi thường lan như thế nào, giới hạn được mức lan, và biết hạ cấp để phần quan trọng nhất vẫn tiếp tục chạy.

---

54.1. Một câu chuyện: đêm trước hạn nộp bài

Hãy quay lại hệ thống AI Judge.

Tối Chủ nhật.

23:30.

Hạn nộp bài là 23:59.

Bình thường mỗi phút có vài chục bài.

Nhưng lúc này:

Hàng nghìn học sinh cùng nộp.
Nhiều bài dài.
AI provider phản hồi chậm hơn bình thường.
Một số request timeout.
Worker retry lại.
Queue bắt đầu tăng.
Database bắt đầu chậm.
API nộp bài cũng chậm theo.
User không thấy kết quả nên bấm nộp lại.

Nếu hệ thống được thiết kế kém, mọi thứ có thể sụp rất nhanh.

Nếu hệ thống được thiết kế tốt, nó sẽ làm mấy việc:

Vẫn nhận bài nộp.
Nói rõ bài đang chờ chấm.
Giới hạn retry.
Giới hạn số request ra AI provider.
Không để database bị ghi quá tải.
Tạm tắt phần không quan trọng.
Ưu tiên đường sống chính của nghiệp vụ.

Đây chính là tư duy failure mode.

Không chỉ hỏi:

Làm sao cho hệ thống chạy?

Mà phải hỏi thêm:

Khi nó bắt đầu hỏng, nó sẽ hỏng theo kiểu nào?
Khi một phần hỏng, phần nào cần được bảo vệ?
Khi không thể làm hết, việc nào vẫn phải làm được?

---

54.2. Failure mode khác bug thông thường thế nào?

Bug thông thường là:

Code sai logic.
Validate thiếu.
Query sai.
Tính điểm nhầm.
API trả nhầm field.

Failure mode thường là:

Hệ thống đúng logic, nhưng sụp vì tải, độ trễ, phụ thuộc bên ngoài, retry, hoặc cách các thành phần tương tác với nhau.

Ví dụ:

Code retry không sai cú pháp.
Worker chạy đúng.
Queue hoạt động đúng.
Database hoạt động đúng.
AI provider hoạt động đúng một phần.

Nhưng khi ghép lại, hệ thống vẫn có thể sập.

Vì retry nhiều quá.

Vì queue tăng nhanh hơn tốc độ xử lý.

Vì database phải ghi trạng thái quá nhiều.

Vì user bấm lại.

Vì alert đến quá muộn.

Failure mode là lỗi của hệ thống như một tổng thể.

Đây là lý do senior engineer không chỉ nhìn từng dòng code.

Họ nhìn:

Luồng dữ liệu.
Giới hạn tài nguyên.
Tốc độ sinh việc.
Tốc độ xử lý việc.
Độ trễ.
Timeout.
Retry.
Backpressure.
Điểm nghẽn.
Cách lỗi lan.

---

54.3. Retry storm là gì?

Retry là làm lại một thao tác khi nó thất bại tạm thời.

Ví dụ:

Gọi Gemini API bị timeout.
Worker thử lại sau vài giây.

Retry là cần thiết.

Vì không retry thì một lỗi mạng nhỏ cũng làm job fail ngay.

Nhưng retry có mặt tối.

Retry storm xảy ra khi quá nhiều request thất bại cùng lúc, rồi cùng retry, làm hệ thống càng quá tải.

Hãy tưởng tượng:

1000 job gọi AI.
AI provider chậm.
300 job timeout.
300 job retry sau 5 giây.
Trong lúc đó 1000 job mới vẫn đến.
AI provider lại chậm hơn.
Nhiều job lại timeout hơn.
Retry lại nhiều hơn.

Từ bên ngoài nhìn vào, có vẻ hệ thống đang "cố gắng hơn".

Nhưng thực chất nó đang tự đánh mình mệt hơn.

Retry storm nguy hiểm vì nó biến lỗi tạm thời thành lũ request nhân lên.

Một request ban đầu có thể thành:

1 lần gọi chính
+ 3 lần retry
= 4 lần gọi provider

Nếu có 10.000 job, 3 retry nghĩa là có thể thành 40.000 lần gọi.

Nếu retry không có giới hạn, mọi thứ còn tệ hơn.

---

54.4. Vì sao retry storm hay xảy ra?

Retry storm thường xảy ra vì ta nghĩ retry là một giải pháp đơn giản:

Hỏng thì thử lại.

Nhưng ta quên hỏi:

Thử lại lúc nào?
Thử lại bao nhiêu lần?
Có thêm delay không?
Delay có giống nhau giữa các job không?
Nếu provider đang quá tải thì retry có giúp gì không?
Nếu tất cả worker cùng retry thì sao?

Một lỗi phổ biến là retry ngay lập tức.

Ví dụ:

Gọi AI timeout.
Retry ngay.
Lại timeout.
Retry ngay.
Lại timeout.

Cách này làm provider và chính worker của ta bị đập liên tục.

Một lỗi khác là tất cả retry cùng một thời điểm.

Ví dụ:

1000 job timeout lúc 10:00:00.
Tất cả retry sau đúng 10 giây.
10:00:10, provider nhận thêm một đợt request lớn.

Đây là lý do retry thường cần jitter.

Jitter nghĩa là thêm độ lệch ngẫu nhiên nhỏ.

Thay vì tất cả retry sau đúng 10 giây, ta để:

Job A retry sau 8 giây.
Job B retry sau 13 giây.
Job C retry sau 11 giây.

Tải được trải ra.

Hệ thống thở được.

---

54.5. Retry đúng nên như thế nào?

Retry đúng thường có vài nguyên tắc.

Thứ nhất: chỉ retry lỗi có khả năng tạm thời.

Ví dụ nên retry:

Timeout.
Network error.
HTTP 429.
HTTP 500, 502, 503, 504.

Ví dụ không nên retry:

Prompt không hợp lệ.
API key sai.
Submission không tồn tại.
User không có quyền.
Input vượt giới hạn không thể xử lý.

Nếu lỗi là lỗi vĩnh viễn, retry chỉ làm tốn tài nguyên.

Thứ hai: retry phải có giới hạn.

Ví dụ:

Tối đa 3 lần.
Sau đó chuyển job sang trạng thái failed rõ ràng.

Thứ ba: retry nên có exponential backoff.

Nghĩa là càng fail nhiều, càng chờ lâu:

Lần 1: chờ 5 giây.
Lần 2: chờ 30 giây.
Lần 3: chờ 2 phút.

Thứ tư: retry nên có jitter.

Thứ năm: retry phải gắn với idempotency.

Idempotency nghĩa là làm lại không tạo ra hậu quả lặp.

Ví dụ nguy hiểm:

Job chấm bài ghi điểm vào database.
Request timeout sau khi database đã ghi.
Worker retry.
Retry ghi thêm một bản điểm nữa.

Thiết kế tốt hơn:

Mỗi submission có một grading_result chính.
Retry cập nhật cùng result_id.
Không tạo nhiều kết quả trùng.

---

54.6. Thundering herd là gì?

Thundering herd là hiện tượng rất nhiều client, worker, hoặc service cùng làm một việc vào cùng một lúc, khiến hệ thống bị dồn tải đột ngột.

Ví dụ trong AI Judge:

23:59 hết hạn nộp bài.
Tất cả học sinh bấm submit gần cùng lúc.

Hoặc:

Cache chứa danh sách đề bài hết hạn lúc 00:00.
Ngay 00:00, tất cả request đều miss cache.
Tất cả cùng query database.
Database bị dồn tải.

Hoặc:

Provider lỗi 5 phút.
Hệ thống giữ lại nhiều job.
Provider vừa sống lại.
Tất cả worker cùng gửi request lại.
Provider lại bị quá tải.

Tên nghe có vẻ lạ, nhưng ý rất đơn giản:

Một đám đông cùng lao vào một cửa hẹp.

Trong hệ thống, cửa hẹp có thể là:

Database.
Cache.
AI provider.
Message queue.
File storage.
Một API nội bộ.

---

54.7. Cách giảm thundering herd

Muốn giảm thundering herd, ta cần làm tải bớt đồng loạt.

Một cách là dùng jitter.

Không để mọi job chạy lại cùng một thời điểm.

Ví dụ:

Không retry toàn bộ sau đúng 60 giây.
Retry rải trong khoảng 45-90 giây.

Một cách khác là giới hạn concurrency.

Ví dụ:

Chỉ cho 50 worker gọi AI provider cùng lúc.
Các job còn lại chờ trong queue.

Điều này nghe có vẻ làm chậm.

Nhưng thực tế nó bảo vệ hệ thống.

Nếu provider chỉ chịu nổi 50 request đồng thời, gửi 500 request đồng thời không làm xong nhanh hơn.

Nó chỉ làm timeout nhiều hơn.

Một cách nữa là cache có TTL lệch nhau.

Nếu mọi cache key cùng hết hạn sau đúng 1 giờ, database có thể bị dồn tải theo chu kỳ.

Thiết kế tốt hơn:

Key A hết hạn sau 55 phút.
Key B hết hạn sau 63 phút.
Key C hết hạn sau 58 phút.

Một cách nữa là request coalescing.

Nghĩa là nếu nhiều request cùng cần một dữ liệu giống nhau, chỉ một request thật sự đi lấy, các request còn lại chờ kết quả đó.

Ví dụ:

100 request cùng cần rubric của assignment 123.
Thay vì 100 query database, chỉ 1 query.
99 request dùng lại kết quả.

---

54.8. Cascading failure là gì?

Cascading failure là lỗi dây chuyền.

Một thành phần hỏng hoặc chậm, rồi kéo các thành phần khác hỏng theo.

Ví dụ:

AI provider chậm.
Worker giữ connection lâu hơn.
Worker xử lý ít job hơn.
Queue tăng.
API phải đọc trạng thái queue nhiều hơn.
Database bị query nhiều hơn.
Database chậm.
API nộp bài chậm.
User bấm submit lại.
Queue tăng thêm.

Ban đầu lỗi nằm ở AI provider.

Cuối cùng user lại thấy:

Không nộp bài được.
Không xem điểm được.
Trang web chậm.

Cascading failure đáng sợ vì nó làm ranh giới lỗi biến mất.

Một service hỏng không nằm yên trong service đó.

Nó lan.

Muốn chống lỗi dây chuyền, ta phải thiết kế ranh giới.

Ranh giới đó có thể là:

Timeout.
Circuit breaker.
Rate limit.
Queue.
Bulkhead.
Fallback.
Graceful degradation.

---

54.9. Timeout không phải chi tiết nhỏ

Timeout là thời gian tối đa ta sẵn sàng chờ một thao tác.

Nhiều người xem timeout là cấu hình phụ.

Thực ra timeout là một công cụ bảo vệ hệ thống.

Nếu không có timeout, một request chậm có thể giữ tài nguyên rất lâu.

Ví dụ:

Worker gọi AI provider.
Provider không trả lời.
Worker cứ chờ 5 phút.
Trong 5 phút đó, worker không xử lý job khác.

Nếu nhiều worker cùng bị treo như vậy, queue sẽ tăng nhanh.

Nhưng timeout cũng không được đặt bừa.

Timeout quá ngắn:

Request đáng lẽ trả về sau 12 giây.
Ta timeout ở 5 giây.
Nhiều job fail giả.
Retry tăng.

Timeout quá dài:

Provider hỏng thật.
Worker chờ quá lâu.
Queue nghẹt.

Timeout tốt phải hiểu hành vi thực tế.

Ví dụ:

Nếu p95 AI response là 20 giây,
timeout 3 giây là vô lý.

Nếu provider hỏng thường treo hơn 2 phút,
timeout 5 phút có thể quá dài.

Timeout nên được chọn cùng với retry, queue, concurrency, và SLO.

Không nên xem riêng lẻ.

---

54.10. Circuit breaker là gì?

Circuit breaker giống cầu dao điện.

Khi thấy một dependency đang lỗi quá nhiều, hệ thống tạm ngừng gọi nó trong một thời gian.

Ví dụ:

Trong 1 phút, 70% request đến AI provider bị timeout.
Circuit breaker mở.
Worker tạm không gửi request mới đến provider đó.
Job được giữ lại hoặc chuyển sang provider khác.
Sau 30 giây, thử lại một ít request.
Nếu ổn, mở lại dần.

Nếu không có circuit breaker, hệ thống cứ tiếp tục gọi dependency đang chết.

Kết quả:

Tốn worker.
Tốn connection.
Tăng timeout.
Tăng retry.
Tăng queue.

Circuit breaker không làm provider sống lại.

Nó bảo vệ hệ thống của ta khỏi việc đốt tài nguyên vào một chỗ đang không phản hồi.

Trong AI Judge, circuit breaker đặc biệt hữu ích khi có nhiều provider hoặc nhiều model.

Ví dụ:

Gemini chậm bất thường.
Tạm giảm traffic sang Gemini.
Chuyển một phần bài sang model dự phòng.
Hoặc giữ job lại và thông báo đang chờ chấm.

---

54.11. Bulkhead là gì?

Bulkhead là cách chia hệ thống thành các khoang để lỗi ở một phần không làm chìm toàn bộ.

Từ này đến từ thiết kế tàu.

Nhưng trong hệ thống, ý rất thực tế:

Không để một loại traffic ăn hết tài nguyên của loại traffic khác.

Ví dụ trong AI Judge:

Chấm bài tự luận dài rất tốn AI token.
Chấm bài trắc nghiệm nhanh hơn nhiều.
Export báo cáo cũng tốn database.

Nếu tất cả dùng chung một pool worker, một đợt bài tự luận dài có thể làm:

Trắc nghiệm cũng chậm.
Export cũng chậm.
Regrade cũng chậm.

Bulkhead tốt hơn:

Worker pool riêng cho grading.
Worker pool riêng cho report/export.
Concurrency limit riêng cho AI calls.
Connection pool riêng nếu cần.
Queue riêng theo loại công việc.

Bulkhead không làm hệ thống đơn giản hơn.

Nhưng nó giúp lỗi có biên giới.

Khi phần chấm tự luận bị nghẹt, phần nộp bài vẫn nên sống.

Khi phần export báo cáo chậm, phần chấm điểm không nên chết theo.

---

54.12. Queue overload là gì?

Queue overload xảy ra khi tốc độ tạo job lớn hơn tốc độ xử lý job trong một thời gian đủ lâu.

Công thức rất đơn giản:

Job vào mỗi phút > Job xử lý mỗi phút
= Queue tăng

Ví dụ:

Mỗi phút có 2000 bài nộp.
Worker chỉ chấm được 500 bài mỗi phút.
Queue tăng thêm 1500 job mỗi phút.

Sau 10 phút:

15.000 job đang chờ.

Sau 30 phút:

45.000 job đang chờ.

Lúc này tăng thêm vài worker có thể không đủ.

Vì backlog đã quá lớn.

Queue overload không chỉ làm chậm.

Nó làm nhiều thứ khác xấu đi:

Job cũ quá hạn.
Retry chen vào job mới.
Worker xử lý job đã không còn cần nữa.
Database lưu nhiều trạng thái hơn.
Monitoring nhiễu hơn.
User mất niềm tin.

Một queue dài không tự nó là lỗi.

Nhưng queue dài mà oldest job age vượt SLO là vấn đề nghiêm trọng.

---

54.13. Queue overload nên nhìn bằng chỉ số nào?

Nhiều người chỉ nhìn queue length.

Ví dụ:

Queue có 10.000 job.

Nhưng con số này chưa đủ.

10.000 job có thể ổn nếu:

Worker xử lý 20.000 job/phút.

10.000 job rất nguy hiểm nếu:

Worker xử lý 200 job/phút.

Chỉ số quan trọng hơn là:

Oldest job age.
Time to complete.
Enqueue rate.
Processing rate.
Retry rate.
Failure rate.

Với AI Judge, câu hỏi thật là:

Bài nộp cũ nhất đã chờ bao lâu?
95% bài được chấm trong bao lâu?
Job fail vì provider hay vì input?
Retry đang chiếm bao nhiêu phần trăm traffic?

Nếu oldest job age là 2 phút, queue length cao chưa chắc đáng sợ.

Nếu oldest job age là 45 phút, đó là dấu hiệu người dùng thật đang bị ảnh hưởng.

---

54.14. Queue overload xử lý thế nào?

Không có một nút thần kỳ.

Nhưng có vài hướng thực tế.

Thứ nhất: tăng capacity xử lý, nếu bottleneck thật sự là worker.

Ví dụ:

Tăng số worker.
Tăng concurrency hợp lý.
Tách queue theo loại job.

Nhưng nếu bottleneck là AI provider, tăng worker có thể làm tệ hơn.

Worker nhiều hơn chỉ gửi nhiều request hơn đến provider đang chậm.

Thứ hai: giảm tốc độ tạo job.

Ví dụ:

Rate limit regrade.
Không cho user spam submit cùng một bài.
Gộp các request trùng.
Tạm dừng job không quan trọng.

Thứ ba: ưu tiên công việc quan trọng.

Ví dụ:

Bài vừa nộp trước deadline ưu tiên hơn job thống kê.
Chấm lần đầu ưu tiên hơn regrade hàng loạt.

Thứ tư: loại bỏ việc vô ích.

Ví dụ:

Nếu cùng một submission đã có job mới hơn, job cũ có thể bỏ.
Nếu assignment đã đóng và giáo viên hủy chấm, job còn lại không cần chạy.

Thứ năm: nói thật với user.

Bài đã được ghi nhận.
Hệ thống đang chấm, dự kiến có điểm trong X phút.

Im lặng là cách rất nhanh để user bấm lại nhiều lần.

---

54.15. Database overload là gì?

Database overload xảy ra khi database không xử lý kịp lượng đọc/ghi/query/transaction.

Trong hệ thống web, database thường là điểm chung của rất nhiều luồng:

API đọc user.
API ghi submission.
Worker ghi kết quả chấm.
Dashboard giáo viên đọc thống kê.
Admin query dữ liệu.
Report export chạy query nặng.

Khi database chậm, mọi thứ dùng database chậm theo.

Đây là lý do database overload rất dễ tạo cascading failure.

Ví dụ:

Worker chấm xong, ghi result chậm.
Worker giữ transaction lâu.
API đọc submission bị chậm.
User refresh nhiều hơn.
Database nhận thêm query.

Database overload không chỉ do "database yếu".

Nó có thể đến từ:

Query thiếu index.
N+1 query.
Transaction quá dài.
Connection pool cạn.
Lock contention.
Report query nặng chạy giờ cao điểm.
Ghi log/audit quá nhiều.
Retry ghi database quá nhiều.

---

54.16. Connection pool cạn là lỗi rất thực tế

Một lỗi nhiều người gặp là connection pool cạn.

Ví dụ:

Database cho phép 100 connections.
API dùng 60.
Worker dùng 60.
Admin/report dùng 20.
Tổng nhu cầu là 140.

Kết quả:

Một số request chờ connection.
Request chậm.
Timeout tăng.
Retry tăng.

Tăng connection pool không phải lúc nào cũng đúng.

Nếu database chỉ xử lý tốt 100 connection, mở 500 connection có thể làm nó mệt hơn.

Giống như một quầy thu ngân chỉ có một người, cho thêm 500 người xếp hàng trước quầy không làm thanh toán nhanh hơn.

Hướng đúng thường là:

Giới hạn connection theo service.
Tối ưu query.
Tách job nặng.
Read replica nếu phù hợp.
Cache dữ liệu đọc nhiều.
Không chạy report nặng giờ cao điểm.

Quan trọng nhất:

Đừng để một service ăn hết connection của toàn hệ thống.

Đó cũng là bulkhead.

---

54.17. External provider outage là gì?

External provider outage là khi dịch vụ bên ngoài mà ta phụ thuộc bị lỗi, chậm, giới hạn, hoặc thay đổi hành vi.

Trong AI Judge, provider có thể là:

Gemini API.
OpenAI API.
Email service.
Object storage.
Payment gateway.
Authentication provider.

Provider outage không nhất thiết là chết hoàn toàn.

Có nhiều mức:

Chậm hơn bình thường.
Timeout tăng.
429 tăng.
Một region lỗi.
Một model lỗi.
Một loại request lỗi.
Response format thay đổi nhẹ.
Quota gần hết.
Cost tăng bất thường.

Nếu hệ thống của ta giả định provider luôn ổn, sự cố sẽ rất đau.

Thiết kế tốt phải xem provider bên ngoài là thứ có thể hỏng.

Không phải vì họ kém.

Mà vì mọi hệ thống đều có giới hạn.

---

54.18. Ứng xử với provider bên ngoài

Khi dùng provider bên ngoài, ta cần có lớp bảo vệ.

Một: timeout rõ ràng.

Không để worker chờ mãi.

Hai: retry có giới hạn.

Ba: circuit breaker.

Nếu provider lỗi hàng loạt, tạm giảm hoặc ngừng gọi.

Bốn: rate limit phía mình.

Đừng gửi quá khả năng provider hoặc quota của mình.

Năm: fallback nếu nghiệp vụ cho phép.

Ví dụ:

Dùng model khác.
Dùng provider khác.
Chấm sơ bộ trước, chấm đầy đủ sau.
Giữ job lại và báo đang chờ.

Sáu: lưu trạng thái rõ ràng.

Không nên để user thấy một màn hình mơ hồ:

Đang xử lý...

mãi mãi.

Tốt hơn:

Bài đã được ghi nhận.
Hệ thống chấm điểm đang chậm do dịch vụ AI.
Bạn không cần nộp lại.

Bảy: đo riêng provider metrics.

Ví dụ:

provider_latency_seconds
provider_error_rate
provider_timeout_rate
provider_429_rate
provider_cost_per_hour
tokens_per_submission

Không đo riêng provider thì rất khó biết lỗi nằm ở provider, mạng, prompt, worker, hay database.

---

54.19. Partial outage là gì?

Partial outage là sự cố một phần.

Hệ thống không chết toàn bộ.

Nhưng một số chức năng, một số người dùng, hoặc một số khu vực bị ảnh hưởng.

Ví dụ:

User vẫn đăng nhập được nhưng không xem báo cáo.
Học sinh vẫn nộp bài được nhưng điểm ra chậm.
Giáo viên vẫn xem lớp A nhưng lớp B lỗi.
Chỉ bài tự luận lỗi, bài trắc nghiệm vẫn ổn.
Chỉ model X lỗi, model Y vẫn chạy.
Chỉ một queue bị backlog.

Partial outage khó hơn full outage ở một điểm:

Nếu chỉ nhìn tổng thể, ta có thể tưởng mọi thứ vẫn ổn.

Ví dụ:

API success rate toàn hệ thống = 99%.

Nghe rất tốt.

Nhưng nếu 1% lỗi đó toàn bộ nằm ở:

Tất cả bài thi cuối kỳ của một trường lớn.

Thì đó là sự cố nghiêm trọng.

Vì vậy metrics cần có đủ chiều để phát hiện lỗi cục bộ.

Nhưng cũng không được quá nhiều label làm hệ thống metrics nổ cardinality.

Đây là chỗ cần cân bằng.

---

54.20. Partial outage cần dashboard khác

Dashboard quá tổng quát thường che mất partial outage.

Ví dụ chỉ có:

Total requests.
Overall error rate.
Overall latency.

Thì chưa đủ.

Với AI Judge, nên có thêm góc nhìn theo:

Loại job: first_grade, regrade, report.
Provider/model.
Queue.
Assignment quan trọng.
Error class.
Latency bucket.

Không phải lúc nào cũng cần chia theo từng user.

Điều đó có thể quá chi tiết và tốn kém.

Nhưng cần đủ để trả lời:

Lỗi có nằm ở một loại bài không?
Lỗi có nằm ở một provider không?
Lỗi có nằm ở một queue không?
Lỗi có nằm ở một nhóm khách hàng lớn không?

Partial outage cũng cần alert thông minh.

Nếu overall error rate chưa cao nhưng một luồng quan trọng lỗi 50%, vẫn phải alert.

Ví dụ:

Regrade lỗi 5% có thể là warning.
Submit bài lỗi 5% có thể là critical.

Cùng một tỷ lệ lỗi, mức độ nghiêm trọng khác nhau vì nghiệp vụ khác nhau.

---

54.21. Graceful degradation là gì?

Graceful degradation là hạ cấp mềm.

Nghĩa là khi hệ thống không thể phục vụ đầy đủ, nó vẫn phục vụ phần quan trọng nhất một cách rõ ràng và có kiểm soát.

Không graceful:

AI provider chậm.
Trang nộp bài quay vòng mãi.
User không biết bài đã được nhận chưa.
User bấm lại.
Hệ thống càng tải.

Graceful:

Bài nộp được lưu ngay.
User thấy: "Bài đã được ghi nhận, đang chờ chấm."
Phần điểm số cập nhật sau.
Nút nộp lại bị giới hạn hợp lý.

Graceful degradation không phải là che giấu lỗi.

Nó là nói thật và giữ phần chính sống.

Trong AI Judge, phần chính thường là:

Nhận bài nộp.
Không mất dữ liệu.
Không chấm trùng gây sai điểm.
Cho user biết trạng thái.
Cho admin/teacher biết backlog.

Phần có thể hạ cấp:

Chấm ngay lập tức.
Báo cáo realtime.
Thống kê đẹp.
Export file lớn.
Gợi ý nâng cao.
Regrade hàng loạt.

---

54.22. Hạ cấp mềm cần được thiết kế trước

Graceful degradation không tự nhiên xuất hiện khi sự cố xảy ra.

Nó phải được thiết kế trước.

Ví dụ muốn hạ cấp AI Judge, ta cần có sẵn:

Trạng thái submission rõ ràng: received, queued, grading, graded, failed, needs_review.
UI hiển thị trạng thái đó.
Worker có thể xử lý lại job an toàn.
Queue có priority.
Feature flag để tắt phần phụ.
Runbook cho người vận hành.
Alert khi backlog vượt SLO.

Nếu database schema chỉ có:

score

mà không có trạng thái chấm, rất khó nói với user bài đang ở đâu.

Nếu UI chỉ biết:

Có điểm hoặc không có điểm.

thì khi chấm chậm, user sẽ nghĩ hệ thống lỗi.

Nếu không có queue priority, job export báo cáo có thể chen ngang job chấm bài cần gấp.

Hạ cấp mềm là kết quả của thiết kế trạng thái, queue, UI, và vận hành.

Không chỉ là một dòng code.

---

54.23. Backpressure là gì?

Backpressure là cách hệ thống nói:

Tôi đang quá tải, đừng đẩy thêm việc vào nhanh như vậy.

Nếu không có backpressure, upstream cứ gửi việc xuống downstream.

Downstream không xử lý kịp.

Queue phình lên.

Timeout tăng.

Retry tăng.

Sự cố lan.

Ví dụ:

API nhận submit không giới hạn.
Mỗi submit tạo grading job.
Worker không xử lý kịp.
Queue tăng lên 1 triệu job.

Backpressure có thể là:

Rate limit.
Trả 429 với retry-after.
Giới hạn số job mỗi user/lớp/assignment.
Tạm dừng regrade hàng loạt.
Chỉ nhận bài, chưa tạo job phụ.
Đưa job vào priority thấp.

Backpressure nghe có vẻ "từ chối user".

Nhưng nếu làm đúng, nó bảo vệ user.

Từ chối có kiểm soát tốt hơn là nhận hết rồi chết toàn bộ.

---

54.24. Load shedding là gì?

Load shedding là chủ động bỏ bớt tải không quan trọng để giữ phần quan trọng.

Ví dụ trong AI Judge:

Khi hệ thống quá tải:
- Tạm tắt export báo cáo lớn.
- Tạm tắt thống kê realtime.
- Tạm hoãn regrade hàng loạt.
- Giảm tần suất refresh dashboard.
- Không tạo preview nâng cao.

Mục tiêu là bảo vệ:

Đăng nhập.
Nộp bài.
Lưu bài.
Chấm bài chính.
Xem trạng thái cơ bản.

Load shedding khác với hệ thống chết.

Hệ thống chết là mất kiểm soát.

Load shedding là chọn có ý thức:

Việc nào giữ.
Việc nào bỏ tạm.
Việc nào hoãn.

Để làm được, product và engineering phải thống nhất trước.

Trong sự cố, không nên mất 30 phút tranh luận xem export có quan trọng hơn submit không.

---

54.25. Rate limit không chỉ để chống spam

Nhiều người nghĩ rate limit chỉ dùng để chống spam hoặc abuse.

Đúng, nhưng chưa đủ.

Rate limit còn là công cụ bảo vệ tài nguyên.

Ví dụ:

Một giáo viên bấm regrade toàn bộ 50.000 bài.
Nếu hệ thống tạo 50.000 job ngay, queue có thể nghẹt.

Thiết kế tốt hơn:

Chấp nhận yêu cầu regrade.
Tạo job theo batch.
Giới hạn tốc độ tạo job.
Hiển thị tiến độ.
Cho phép hủy.

Rate limit cũng có thể áp dụng cho:

Số lần submit lại.
Số lần gọi export.
Số request xem dashboard.
Số AI call đồng thời.
Số job regrade mỗi assignment.

Rate limit tốt không chỉ nói "không".

Nó nên nói:

Bạn đang gửi quá nhanh.
Hãy thử lại sau X giây.
Yêu cầu của bạn đã được ghi nhận và đang xếp hàng.

Thông điệp rõ ràng làm user ít bấm lại hơn.

---

54.26. Một failure nhỏ có thể biến thành failure lớn như thế nào?

Hãy ghép các khái niệm lại thành một chuỗi.

Ban đầu:

AI provider latency tăng từ 15 giây lên 60 giây.

Nếu hệ thống yếu:

Worker timeout.
Worker retry ngay.
Retry không jitter.
Tất cả retry cùng lúc.
Provider nhận thêm traffic.
Timeout tăng.
Queue tăng.
Database ghi retry state nhiều hơn.
API đọc trạng thái chậm.
User refresh và submit lại.
Queue tăng thêm.
Alert bắn quá nhiều.
Team không biết alert nào quan trọng.

Đây là lỗi dây chuyền.

Nếu hệ thống tốt:

Timeout hợp lý.
Retry có backoff và jitter.
Concurrency gọi provider bị giới hạn.
Circuit breaker mở khi provider lỗi nhiều.
Queue có priority.
Regrade/report bị hạ ưu tiên.
User thấy bài đã được nhận và đang chờ.
Alert dựa trên oldest job age và SLO.
Runbook chỉ người trực cần làm gì.

Cùng một lỗi provider.

Hai hệ thống phản ứng khác nhau.

Kết quả khác nhau.

Đây là lý do kiến trúc quan trọng.

---

54.27. Đừng chỉ hỏi "lỗi ở đâu?"

Khi sự cố xảy ra, câu hỏi đầu tiên thường là:

Lỗi ở đâu?

Câu này cần, nhưng chưa đủ.

Ta nên hỏi thêm:

Lỗi bắt đầu ở đâu?
Lỗi lan sang đâu?
Tài nguyên nào bị cạn trước?
Retry có làm nặng thêm không?
Queue có đang tích tụ không?
Database có bị kéo theo không?
User có đang tạo thêm tải không?
Phần nào cần được bảo vệ trước?

Ví dụ nếu thấy API chậm, đừng vội tối ưu API.

Có thể API chậm vì database chậm.

Database chậm vì worker ghi quá nhiều retry state.

Worker retry nhiều vì provider timeout.

Provider timeout vì ta gửi quá nhiều request đồng thời.

Nếu sửa ở sai tầng, ta chỉ chữa triệu chứng.

Senior không chỉ tìm điểm cháy.

Họ tìm đường lan của lửa.

---

54.28. Dấu hiệu sớm của một failure mode

Failure mode thường có tín hiệu sớm.

Retry storm:

Retry rate tăng nhanh.
Timeout rate tăng.
Traffic ra dependency tăng dù traffic user không tăng tương ứng.

Thundering herd:

Spike traffic theo cùng một thời điểm.
Cache miss tăng đột ngột.
Database query tăng theo chu kỳ.

Cascading failure:

Một service chậm trước, sau đó nhiều service khác chậm theo.
Error lan qua nhiều endpoint.
Queue và database cùng xấu đi.

Queue overload:

Oldest job age tăng.
Enqueue rate lớn hơn processing rate.
Worker utilization cao nhưng throughput không tăng.

Database overload:

Query latency tăng.
Connection pool wait tăng.
Lock wait tăng.
Slow query tăng.

Provider outage:

Timeout/429/5xx từ provider tăng.
Latency provider tăng.
Cost hoặc token bất thường.

Partial outage:

Overall metric nhìn ổn nhưng một segment lỗi mạnh.
Một queue, một provider, một assignment, hoặc một job type xấu bất thường.

Graceful degradation thiếu:

User không biết trạng thái.
Submit lại nhiều.
Support ticket tăng.
Traffic refresh tăng.

---

54.29. Cách thiết kế để lỗi không lan quá xa

Có vài nguyên tắc rất thực tế.

Một: đặt timeout ở mọi chỗ gọi qua mạng.

Không có timeout nghĩa là có thể chờ vô hạn.

Hai: retry có giới hạn, backoff, jitter, và chỉ retry lỗi tạm thời.

Ba: dùng queue để tách việc lâu khỏi request trực tiếp.

Nhưng queue phải có SLO, priority, và backpressure.

Bốn: giới hạn concurrency với dependency yếu.

Đừng để worker nhiều hơn biến thành provider chết nhanh hơn.

Năm: tách tài nguyên theo luồng quan trọng.

Submit bài không nên chết vì export báo cáo.

Sáu: có circuit breaker cho provider hay lỗi.

Bảy: có trạng thái rõ ràng cho user.

Im lặng làm user tạo thêm tải.

Tám: đo đúng thứ.

Không chỉ đo CPU.

Phải đo queue age, provider latency, retry rate, database wait, SLO burn.

Chín: có runbook.

Khi sự cố xảy ra, người trực không nên phải nghĩ từ số không.

---

54.30. AI Judge nên thiết kế chống failure mode thế nào?

Một thiết kế thực dụng cho AI Judge có thể như sau.

Khi user nộp bài:

API chỉ làm việc tối thiểu:
- Validate.
- Lưu submission.
- Trả về trạng thái received/queued.
- Đẩy job chấm vào queue.

API không nên chờ AI chấm xong rồi mới trả response.

Vì AI call lâu và có thể chậm bất thường.

Worker chấm bài:

Lấy job từ queue.
Kiểm tra job còn hợp lệ không.
Gọi AI provider với timeout.
Retry lỗi tạm thời với backoff và jitter.
Ghi kết quả idempotent.
Nếu fail sau retry, chuyển needs_review hoặc failed rõ ràng.

Provider guard:

Giới hạn concurrency gọi AI.
Đo latency/error/429.
Mở circuit breaker nếu lỗi hàng loạt.
Có fallback hoặc chờ lại tùy nghiệp vụ.

Queue:

Ưu tiên first_grade hơn regrade.
Tách report/export khỏi grading.
Theo dõi oldest job age.
Không để job vô hạn tuổi.

UI:

Hiển thị bài đã được ghi nhận.
Hiển thị trạng thái đang chờ chấm.
Không khuyến khích submit lại vô ích.
Thông báo khi hệ thống chấm chậm.

Alert:

Alert khi oldest job age vượt SLO.
Alert khi provider error rate cao.
Alert khi retry rate tăng bất thường.
Alert khi database wait tăng.
Alert khi cost/token tăng bất thường.

Đây không phải thiết kế phức tạp cho đẹp.

Nó là thiết kế để khi một phần đau, toàn bộ cơ thể không gục.

---

54.31. Bảng nhìn nhanh các failure modes

| Failure mode | Hiểu đơn giản | Dấu hiệu | Cách giảm | |---|---|---|---| | Retry storm | Lỗi rồi retry quá nhiều làm lỗi nặng thêm | Retry rate tăng, timeout tăng | Backoff, jitter, retry limit, circuit breaker | | Thundering herd | Nhiều request cùng lao vào một điểm | Spike traffic, cache miss tăng | Jitter, cache TTL lệch, request coalescing | | Cascading failure | Một lỗi kéo nhiều phần khác lỗi theo | Nhiều service cùng xấu dần | Timeout, bulkhead, circuit breaker, fallback | | Queue overload | Job vào nhanh hơn job ra | Oldest job age tăng | Tăng capacity đúng chỗ, priority, backpressure | | Database overload | DB không xử lý kịp đọc/ghi | Query latency, pool wait, lock wait | Index, cache, tách job nặng, giới hạn connection | | Provider outage | Dịch vụ ngoài chậm/lỗi/quota | Provider latency/error/429 tăng | Timeout, rate limit, fallback, circuit breaker | | Partial outage | Chỉ một phần hệ thống lỗi | Overall ổn, một segment xấu | Dashboard theo luồng quan trọng, alert theo SLO | | Thiếu graceful degradation | Hỏng là hỏng cứng | User bấm lại, ticket tăng | Trạng thái rõ ràng, hạ cấp phần phụ, giữ phần chính |

---

54.32. Một checklist trước khi đưa feature mới lên production

Khi thêm một feature mới, nhất là feature dùng queue, AI, hoặc service ngoài, hãy hỏi:

  • Nếu dependency chậm, feature này chờ bao lâu?
  • Timeout ở đâu?
  • Lỗi nào được retry?
  • Retry tối đa bao nhiêu lần?
  • Có backoff và jitter không?
  • Làm lại có idempotent không?
  • Nếu job tăng đột biến, queue nào phình lên?
  • Oldest job age có được đo không?
  • Có priority không?
  • Feature này có ăn chung worker với luồng quan trọng hơn không?
  • Có giới hạn concurrency không?
  • Có thể tạm tắt feature này không?
  • Nếu feature này chết, user nhìn thấy trạng thái gì?
  • Có alert nào gắn với trải nghiệm user không?
  • Có runbook không?

Nếu trả lời không rõ, feature có thể vẫn chạy được ở môi trường test.

Nhưng production sẽ dạy bài học rất đắt.

---

54.33. Điều quan trọng: không phải failure nào cũng cần ngăn tuyệt đối

Một hiểu lầm phổ biến là:

Ta phải ngăn mọi lỗi xảy ra.

Không thực tế.

Thực tế hơn là:

Lỗi nhỏ được phép xảy ra.
Lỗi phải được phát hiện sớm.
Lỗi không được lan vô hạn.
Lỗi không được làm mất dữ liệu quan trọng.
User phải được thông báo đúng.
Hệ thống phải có đường hồi phục.

Ví dụ:

Một vài bài chấm chậm là chấp nhận được.
Mất bài nộp là không chấp nhận được.
Một vài regrade bị hoãn là chấp nhận được.
Không nộp bài được trước deadline là nghiêm trọng.
Report chậm là chấp nhận được.
Database sập vì report là không chấp nhận được.

Thiết kế hệ thống là nghệ thuật chọn thứ nào phải được bảo vệ trước.

Không phải mọi thứ đều quan trọng như nhau.

---

54.34. Kết luận của chương

Failure mode là các kiểu hỏng quen thuộc của hệ thống production.

Biết chúng giúp ta không hoảng khi sự cố xảy ra.

Quan trọng hơn, biết chúng giúp ta thiết kế trước.

Retry storm nhắc ta rằng retry sai có thể làm hệ thống chết nhanh hơn.

Thundering herd nhắc ta rằng tải đồng loạt nguy hiểm hơn tải đều.

Cascading failure nhắc ta rằng lỗi cần có biên giới.

Queue overload nhắc ta rằng job vào nhanh hơn job ra thì backlog sẽ tăng.

Database overload nhắc ta rằng database thường là điểm chung cần được bảo vệ.

External provider outage nhắc ta rằng dịch vụ bên ngoài không bao giờ nên được xem là luôn ổn.

Partial outage nhắc ta rằng tổng thể ổn không có nghĩa mọi nhóm user đều ổn.

Graceful degradation nhắc ta rằng khi không thể làm đủ, phải giữ phần quan trọng nhất sống.

Thông điệp cần nhớ:

> Production không sụp chỉ vì có lỗi. Production thường sụp vì lỗi lan, retry sai, queue phình, tài nguyên cạn, và hệ thống không biết hạ cấp. Thiết kế tốt là thiết kế giới hạn được thiệt hại.

Ở chương tiếp theo, ta sẽ nói về backup và restore: vì quan sát tốt, alert tốt, chống failure tốt vẫn chưa đủ nếu dữ liệu quan trọng bị mất mà không khôi phục được.