Mở đầu loạt bài về hệ thống chịu tải, ta bắt đầu từ tuyến phòng thủ đầu tiên: rate limiting (giới hạn tốc độ). Khi traffic dồn dập — một client lỗi gọi vòng lặp, một đợt tấn công, hay đơn giản là quá đông người dùng — hệ của bạn cần một cách từ chối bớt để không sập. Rate limiting quyết định: request nào được qua, request nào bị chặn (thường trả 429 Too Many Requests). Bài này (phần 1 loạt Hệ thống chịu tải) chạy thật để thấy hai thuật toán phổ biến hoạt động và đo chính xác chúng chặn bao nhiêu.
Cơ chế: token bucket và leaky bucket
- Token bucket: một cái thùng chứa token, nạp đều
Rtoken/giây, tối đa bằng sức chứa (burst). Mỗi request lấy một token; hết token thì bị chặn. Điểm hay: nếu thùng đang đầy (lâu không có request), nó cho phép một cụm (burst) vượt tốc độ tức thời — hợp với traffic thực có lúc dồn lúc thưa. - Leaky bucket: request vào một hàng đợi, "rò ra" đều với tốc độ cố định. Nó làm mượt traffic — đầu ra luôn đều, không cho burst.
Token bucket tự cài rất gọn — mấu chốt là nạp token theo thời gian trôi thay vì dùng timer:
func (t *TokenBucket) Allow() bool {
now := time.Now()
t.tokens += now.Sub(t.last).Seconds() * t.refill // nạp theo thời gian
if t.tokens > t.max { t.tokens = t.max } // không quá sức chứa
t.last = now
if t.tokens >= 1 { t.tokens--; return true } // còn token -> cho qua
return false // hết -> chặn
}
Thư viện chuẩn Go có sẵn golang.org/x/time/rate:
lim := rate.NewLimiter(100, 100) // 100 token/s, burst 100
if lim.Allow() { /* xử lý */ } else { /* trả 429 */ }

Hình 1: Token bucket (nạp token đều, cho phép burst tới sức chứa) vs leaky bucket (rò đều, làm mượt); token bucket tự cài nạp token theo thời gian trôi; hoặc dùng x/time/rate.
Đo thật: burst, bền vững, và rate ổn định
Mình đặt giới hạn 100/s và đo ba kịch bản:

Hình 2: Chạy thật (giới hạn 100/s) — burst: 1000 request tức thì chỉ 100 qua (= sức chứa), 900 chặn; sustained 1s: 9,7 triệu lần thử, 199 qua (burst 100 + ~100 nạp); ổn định 3s: 300 qua = đúng 100 req/s.
Đọc kết quả đo được:
- Burst cho cụm đầu qua ngay: bắn 1000 request tức thì, đúng 100 qua (bằng sức chứa thùng đang đầy), 900 bị chặn. Đây là đặc tính quan trọng của token bucket: nó không trải đều một cách cứng nhắc mà cho phép một cụm ngắn vượt tốc độ — phù hợp traffic thực (người dùng bấm nhiều nút liền một lúc rồi nghỉ).
- Bền vững khớp giới hạn: bắn liên tục suốt 1 giây (9,7 triệu lần thử!), chỉ 199 qua — đúng bằng burst 100 cộng ~100 token nạp trong 1 giây. Dù áp lực khổng lồ, số cho qua vẫn bám sát cấu hình 100/s.
- Rate ổn định chính xác: bỏ hiệu ứng burst đầu (đặt burst=1), chạy 3 giây cho 300 request qua = 100,0 req/s — đúng y giới hạn. Token bucket giữ tốc độ trung bình chính xác về lâu dài.
Đánh đổi cần cân nhắc
Token bucket cho burst, leaky bucket cho mượt — chọn theo nhu cầu. Nếu bạn muốn cho phép các cụm ngắn (API công khai, người dùng thao tác theo đợt), token bucket đúng: burst giúp trải nghiệm mượt mà mà vẫn giới hạn trung bình. Nếu bạn cần bảo vệ một tài nguyên có tốc độ xử lý cố định (ghi đĩa, gọi API bên thứ ba có quota cứng), leaky bucket tốt hơn vì đầu ra luôn đều, không dồn cục. Nhiều hệ dùng token bucket vì nó dung hòa tốt.
Đặt ngưỡng theo capacity thật, không theo cảm tính. Rate limit chỉ có ý nghĩa nếu ngưỡng phản ánh khả năng thật của hệ. Đặt quá cao thì không bảo vệ được (hệ vẫn sập trước khi chạm ngưỡng); quá thấp thì chặn nhầm người dùng hợp lệ. Cách đúng: đo throughput bền vững mà hệ chịu được (như các bài đo trong loạt Mạng), đặt ngưỡng dưới mức đó một chút, và trả 429 rõ ràng (kèm header Retry-After) để client biết đường lùi.
Rate limit trong một tiến trình khác với phân tán. Ví dụ ở đây giới hạn trong một tiến trình. Khi bạn có nhiều instance sau load balancer, mỗi instance đếm token riêng — tổng giới hạn thực tế thành N × ngưỡng, không phải ngưỡng. Rate limit phân tán cần một store chung (thường là Redis với thuật toán token bucket/sliding window) để mọi instance chia sẻ cùng bộ đếm. Đây là bước phức tạp hơn nhiều và có chi phí độ trễ riêng — chỉ làm khi thật cần giới hạn toàn cục.
Ba ý mang về
- Token bucket cho phép burst rồi giữ tốc độ trung bình đúng ngưỡng: đo thật 1000 request tức thì chỉ 100 qua (burst = sức chứa), nhưng 3 giây liên tục cho qua chính xác 100 req/s — dung hòa giữa mượt trải nghiệm và bảo vệ hệ.
- Token bucket vs leaky bucket là lựa chọn theo nhu cầu: token bucket cho burst (hợp API, thao tác theo đợt), leaky bucket làm mượt đầu ra đều (bảo vệ tài nguyên tốc độ cố định); dùng
x/time/ratecho token bucket sẵn. - Đặt ngưỡng theo capacity thật, và phân tán cần store chung: đo throughput hệ chịu được rồi đặt dưới mức đó, trả 429 + Retry-After; nhiều instance thì mỗi cái đếm riêng nên cần Redis để giới hạn toàn cục.
Nguồn
- Go docs — golang.org/x/time/rate: https://pkg.go.dev/golang.org/x/time/rate
- Wikipedia — Token bucket / Leaky bucket: https://en.wikipedia.org/wiki/Token_bucket
- Stripe — Scaling your API with rate limiters: https://stripe.com/blog/rate-limiters
Phần sau ta gặp circuit breaker — khi một service phụ thuộc bắt đầu lỗi liên tục, cầu dao "ngắt mạch" để ngừng gọi nó, trả lỗi nhanh thay vì chờ timeout mãi; đo thật trạng thái đóng/mở/nửa-mở và cách nó bảo vệ hệ khỏi sụp dây chuyền.