Hệ thống chịu tải: mẫu thiết kế cho độ tin cậy và khả năng mở rộng
Loạt bài chuyên sâu về các mẫu thiết kế giúp hệ thống chịu tải và bền bỉ: rate limiting, circuit breaker, retry/backoff, timeout, backpressure, load shedding, worker pool, caching, idempotency, bulkhead. Mỗi bài cài và ĐO THẬT bằng Go trong container để thấy mẫu hoạt động và đánh đổi.
12/12 phần đã đăng
Lập trình
1
Rate limiting: cho phép burst nhưng giữ tốc độ bền vững đúng ngưỡng
Traffic dồn dập làm quá tải hệ hoặc bị lạm dụng — rate limiting chặn phần vượt ngưỡng. Bài này chạy thật trong Go: token bucket giới hạn 100/s cho cụm 100 request đầu qua ngay (burst) rồi giữ đúng 100 req/s bền vững; đo được 1000 request tức thì chỉ 100 qua, và 3 giây liên tục cho qua chính xác 100 req/s. Giải thích token bucket vs leaky bucket và khi nào dùng cái nào.
22/09/2026
· 6 phút đọc
2
Circuit breaker: khi service phụ thuộc chết, đừng gọi mãi — hãy ngắt mạch
Một service phụ thuộc lỗi liên tục mà ta cứ gọi = chờ timeout mãi, tốn tài nguyên, kéo sập cả caller (cascading failure). Circuit breaker ngắt mạch: sau vài lỗi, nó chặn ngay mọi call, trả lỗi nhanh thay vì chờ. Bài này chạy thật trong Go: 20 call tới service chết chỉ 5 chạm thật, 15 bị chặn ~0ms, tổng 1,02s thay vì 4000ms; rồi tự hồi phục qua half-open khi service sống lại.
22/09/2026
· 6 phút đọc
3
Retry đúng cách: exponential backoff và jitter để không tự gây bão
Thử lại khi lỗi thoáng qua là đúng — nhưng retry ngay và đều của nhiều client cùng lúc tạo 'bão retry' đánh sập service đang gượng dậy. Bài này chạy thật trong Go: retry với exponential backoff (100/200/400ms), và đo sức mạnh của jitter — 50 client cùng lỗi, không jitter thì cả 50 dội cùng một cửa sổ 20ms, có jitter thì đỉnh chỉ còn 5, cắt tải retry ~10 lần.
22/09/2026
· 5 phút đọc
4
Timeout và context: đặt hạn cho mọi thao tác, và cái bẫy rò rỉ goroutine
Không đặt hạn thì một thao tác treo giữ luồng mãi, tài nguyên cạn dần. Bài này chạy thật trong Go: context timeout 200ms khiến thao tác 2 giây trả về sau 209ms với DeadlineExceeded; và đo cái bẫy quan trọng — 100 goroutine không nghe ctx.Done() vẫn rò rỉ dù đã cancel, còn 100 goroutine có nghe thì thoát sạch. Timeout chỉ có tác dụng nếu hàm thực sự tôn trọng context.
22/09/2026
· 5 phút đọc
5
Backpressure: vì sao hàng đợi 'không giới hạn' là quả bom RAM chờ nổ
Khi bên sản xuất nhanh hơn bên tiêu thụ và hàng đợi không giới hạn, công việc dồn đống tới khi hết RAM. Bài này chạy thật trong Go: hàng đợi không giới hạn phình lên gần 4GB trong nửa giây, còn hàng đợi có giới hạn (bounded channel cap 100) chặn nguồn lại — producer tự chậm khớp consumer, RAM ổn định. Giải thích backpressure và khi nào chặn, khi nào từ chối.
22/09/2026
· 6 phút đọc
6
Load shedding: thà từ chối 38% để 62% được phục vụ nhanh, còn hơn tất cả cùng chậm
Khi tải vượt capacity, nhận hết vào hàng đợi khiến MỌI request chậm — p99 nổ. Load shedding từ chối bớt sớm (503) để phần được nhận vẫn nhanh. Bài này chạy thật trong Go: không shedding thì 2000 request có p99 = 4,8 giây (ai cũng khổ); có shedding thì 62% được phục vụ ở p99 = 21ms, 38% nhận 503 ngay. Giải thích khác biệt với backpressure và cách ưu tiên request quan trọng.
22/09/2026
· 5 phút đọc
7
Worker pool: vì sao 'một goroutine mỗi task' hỏng ở quy mô, và bao nhiêu worker là đủ
Tạo một goroutine cho mỗi task nghe tiện, nhưng với hàng triệu task nó bùng nổ thành hàng triệu goroutine tốn RAM và đè downstream. Bài này chạy thật trong Go: worker pool giữ đúng 11 goroutine cho 200.000 task (so với 200.001), và đo throughput theo số worker — với CPU-bound, throughput bão hòa quanh số core (10), thêm worker không nhanh hơn. Giải thích cách chọn N cho CPU-bound vs I/O-bound.
22/09/2026
· 5 phút đọc
8
Cache stampede: khi cache hết hạn, 1000 request cùng đấm xuống DB — và singleflight gộp về 1
Cache là lá chắn cho database, nhưng đúng khoảnh khắc một key nóng hết hạn, hàng nghìn request cùng miss và cùng lao xuống nguồn — đó là cache stampede, thứ có thể quật sập DB ngay cả khi mọi thứ 'đang chạy tốt'. Bài này đo thật trong Go: 1000 goroutine cùng miss 1 key gọi nguồn 1000 lần; thêm singleflight, con số về đúng 1; và một cache LRU với truy cập lệch đạt hit rate 32.2%.
22/09/2026
· 9 phút đọc
9
Idempotency: retry đã cứu bạn ở phần 3, giờ đừng để nó trừ tiền khách hai lần
Retry là lá chắn chống lỗi tạm thời, nhưng nó có một mặt tối: nếu request đầu tiên đã thành công mà phản hồi bị mất, lần retry sẽ chạy lại side-effect — khách bị trừ tiền hai lần. Bài này đo thật trong Go: một handler thanh toán không có idempotency bị retry 5 lần trừ tiền 5 lần (số dư 1000 → 500); thêm idempotency key, con số về đúng 1 lần; và 1000 goroutine gửi cùng key đồng thời vẫn chỉ trừ đúng 1 lần, kiểm bằng go run -race.
22/09/2026
· 7 phút đọc
10
Bulkhead: vì sao một API chậm kéo sập cả những endpoint chẳng liên quan gì tới nó
Con tàu Titanic chìm vì nước tràn từ khoang này sang khoang khác. Hệ thống của bạn cũng vậy: khi một phụ thuộc chậm (một API bên thứ ba treo), nó chiếm hết luồng dùng chung và làm chết đói cả những request nhanh chẳng liên quan. Bài này đo thật trong Go: pool dùng chung khiến request nhanh A chỉ 11/200 thành công (latency 46ms) khi request chậm B treo; tách pool riêng (bulkhead), A trở lại 200/200 với latency 2.6ms.
22/09/2026
· 7 phút đọc
11
Graceful degradation: khi phụ thuộc chết, trả dữ liệu cũ có ích còn hơn trang trắng 500
Amazon từng nói: thà hiển thị gợi ý sản phẩm cũ hơn là để trang trắng. Đó là graceful degradation — khi một phụ thuộc chết hẳn, trả một phản hồi suy giảm nhưng vẫn có ích thay vì lỗi 500. Bài này đo thật trong Go: một handler phụ thuộc cứng chỉ đạt 25% thành công khi downstream chết; thêm fallback bằng stale cache, tỉ lệ thành công lên 100% (150/200 request được phục vụ bằng dữ liệu cũ đánh dấu rõ).
22/09/2026
· 8 phút đọc
12
Chọn mẫu chịu tải nào? Cây quyết định và cách ghép 11 mẫu thành một pipeline sống sót
Loạt bài này đi qua 11 mẫu chịu tải, mỗi mẫu giải một vấn đề riêng. Bài tổng kết trả lời câu hỏi thực tế: gặp tình huống X thì dùng mẫu nào, và ghép chúng theo thứ tự nào? Kèm demo thật trong Go ghép bulkhead + circuit breaker + timeout + fallback: cùng một downstream ốm yếu, pipeline nâng tỉ lệ thành công người dùng từ 48% lên 100%, giảm tải xuống downstream từ 500 còn 73 lần gọi, và cắt latency từ 93.9ms xuống 13.3ms.
22/09/2026
· 8 phút đọc