Bài trước phân biệt semaphore (giới hạn số đồng thời) với rate limiter (giới hạn tốc độ). Bài này đi sâu vào rate limiter chuẩn của Go: golang.org/x/time/rate, dựa trên thuật toán token bucket. Nó trả lời câu hỏi khác semaphore: không phải "bao nhiêu chạy cùng lúc" mà "bao nhiêu sự kiện mỗi giây" — và cho phép burst (đợt dồn ngắn). Bài này đo thật hành vi của nó: burst, tốc độ hội tụ, và ba cách lấy token.

Thuật toán token bucket

Hình dung một cái xô: chứa tối đa burst token, tự đổ đầy với tốc độ rate token mỗi giây. Mỗi sự kiện tiêu một token. Hết token thì chờ (hoặc từ chối):

lim := rate.NewLimiter(rate.Limit(5), 3)
//                          5 token/giây    burst=3
// → trung bình 5/giây, nhưng cho phép DỒN tối đa 3 lần liền

burst là chìa khóa: nó cho phép các đợt ngắn vượt tốc độ trung bình (miễn xô còn token), rồi kéo về đúng tốc độ khi xô cạn. Đây là cân bằng giữa "mượt hoàn toàn" (burst=1) và "cho phép bùng ngắn" (burst lớn) — quan trọng cho traffic thực tế vốn không đều.

Ảnh chụp đoạn mã Go nền tối minh hoạ rate limiting bằng token bucket giới hạn tốc độ không phải số đồng thời, golang.org x time rate semaphore giới hạn bao nhiêu cùng lúc rate limiter giới hạn bao nhiêu mỗi giây, một thuật toán token bucket một cái xô chứa tối đa BURST token tự đổ đầy RATE token giây mỗi sự kiện tiêu 1 token hết token chờ hoặc từ chối lim bằng rate.NewLimiter rate.Limit 5 3 5 token giây burst bằng 3 trung bình 5 giây nhưng cho phép dồn tối đa 3 lần liền burst cho phép các đợt ngắn vượt tốc độ trung bình miễn xô còn token rồi bị kéo về đúng tốc độ khi xô cạn, hai ba cách lấy token lim.Allow không chờ true nếu có token false thì bỏ drop lim.Wait ctx chờ tới khi có token tôn trọng ctx hủy timeout r bằng lim.Reserve đặt chỗ r.Delay cho biết phải chờ bao lâu r.Cancel trả token nếu không dùng Allow cho API quá tải thì trả 429 ngay Wait cho worker cứ chờ rồi làm Reserve cho logic tùy biến biết delay để quyết định, ba ví dụ burst rồi bị chặn lim bằng rate.NewLimiter rate.Limit 5 3 for i bằng 0 i nhỏ hơn 5 i print lim.Allow true true true false false 3 token đầu có sẵn hết false, bốn khác semaphore bài trước semaphore giới hạn số đồng thời tối đa N chạy cùng lúc rate limit giới hạn tốc độ tối đa N mỗi giây hai trục khác nhau 1 request chạy 10 phút vẫn 1 đồng thời nhưng 1000 request giây tức thời là vấn đề tốc độ bảo vệ backend thường cần cả hai rate limit chặn đợt dồn semaphore chặn quá tải đồng thời

Hình 1: Token bucket. Xô chứa burst token, nạp rate token/giây. Ba cách lấy (Allow/Wait/Reserve), ví dụ burst, và khác biệt với semaphore.

Ba cách lấy token

x/time/rate cho ba cách, mỗi cách hợp một tình huống:

lim.Allow()          // KHÔNG chờ: true nếu có token, false thì bỏ (drop)
lim.Wait(ctx)        // CHỜ tới khi có token (tôn trọng ctx hủy/timeout)
r := lim.Reserve()   // đặt chỗ: r.Delay() cho biết PHẢI CHỜ bao lâu; r.Cancel() trả token
  • Allow cho API "quá tải thì trả 429 ngay" — không muốn giữ request chờ.
  • Wait cho worker "cứ chờ rồi làm" — throttle công việc nền theo tốc độ.
  • Reserve cho logic tùy biến — biết trước delay để quyết định (chờ, bỏ, hay báo client).

Đo thật: burst và tốc độ

Burst: NewLimiter(5/giây, burst=3), gọi Allow() 5 lần liền:

→ true true true false false   // 3 token đầu có sẵn, hết → bị bỏ

Xô bắt đầu đầy 3 token nên 3 sự kiện đầu qua ngay; token cạn nên các sự kiện tiếp bị từ chối cho tới khi xô nạp lại.

Tốc độ: đo bằng cách gọi Wait() nhiều lần và tính thời gian:

Ảnh chụp bảng kết quả đo thật nền tối token bucket giữ đúng tốc độ và burst go run golang.org x time rate v0.6.0 Go 1.23 arm64 10 core, burst 3 token đầu có sẵn hết thì chặn NewLimiter 5 giây burst bằng 3 gọi Allow 5 lần true true true false false 3 lần đầu OK lần 4-5 bị bỏ xô bắt đầu đầy 3 token nên 3 sự kiện đầu qua ngay burst token cạn nên các sự kiện tiếp bị từ chối cho tới khi xô nạp lại, tốc độ giới hạn đúng như cấu hình cấu hình Wait 20 lần 5 giây burst 3 đo thật 3,40 s kỳ vọng 3,4 s 20 trừ 3 chia 5 Wait 200 lần 50 giây burst 1 đo thật 3,98 s tốc độ 50,2 giây kỳ vọng 4,0 s 50 giây Reserve khi hết token 2 giây đo thật chờ 0,50 s kỳ vọng 1 chia 2 bằng 0,5 s token bucket hội tụ chính xác về tốc độ cấu hình 50,2 giây so với mục tiêu 50 Reserve báo trước delay 0,50s thời gian nạp 1 token ở 2 giây mà không block, rate limiter vs semaphore rate limiter tối đa N sự kiện mỗi giây kiểm soát tốc độ cho burst semaphore tối đa N chạy cùng lúc kiểm soát đồng thời backend thường cần cả hai rate chặn đợt dồn sem chặn quá tải, cốt lõi token bucket xô BURST token nạp RATE token giây mỗi sự kiện tiêu 1 ba cách Allow bỏ Wait chờ Reserve biết delay đúng đắn tốc độ hội tụ về cấu hình đo 50,2 vs 50 giây burst cho phép đợt ngắn vượt trung bình đo 3 token đầu khác sem tốc độ mỗi giây vs đồng thời cùng lúc

Hình 2: Burst (true×3 rồi false). Tốc độ: Wait 20 lần ở 5/giây burst 3 mất 3,40s (kỳ vọng (20-3)/5); Wait 200 lần ở 50/giây → 50,2/giây (hội tụ); Reserve báo chờ 0,50s. So rate limiter vs semaphore.

  • Wait 20 lần ở 5/giây, burst 3: mất 3,40s ≈ (20-3)/5 (3 token burst dùng ngay, 17 còn lại ở 5/giây).
  • Wait 200 lần ở 50/giây, burst 1: 3,98s → 50,2/giây — hội tụ chính xác về tốc độ cấu hình.
  • Reserve khi hết token ở 2/giây: báo chờ 0,50s (= thời gian nạp 1 token) mà không block.

Token bucket giữ tốc độ chính xác như cấu hình (50,2 so với mục tiêu 50), và Reserve cho biết trước delay — công cụ chính xác để kiểm soát tốc độ.

Ứng dụng thực tế

Bảo vệ backend/API khỏi quá tải. Đặt rate limiter trước một endpoint hoặc một dependency (DB, API bên ngoài) để không bao giờ vượt tốc độ an toàn. Dùng Allow + trả 429 cho traffic vào, hoặc Wait để throttle worker gọi dependency.

Tôn trọng rate limit của API bên thứ ba. Nhiều API giới hạn "X request/giây". Đặt một rate.Limiter khớp giới hạn đó và Wait() trước mỗi lời gọi — bạn không bao giờ bị 429 từ họ, và burst cho phép tận dụng hạn ngạch khi cần.

Rate limit theo client (per-key). Giữ một map[clientID]*rate.Limiter để giới hạn từng client riêng — chống một client lạm dụng làm ảnh hưởng client khác. Nhớ dọn limiter cũ (hoặc dùng thư viện có TTL) để không rò bộ nhớ.

Đánh đổi cần cân nhắc

Rate limiter và semaphore giải hai bài toán khác nhau — thường cần cả hai. Rate limiter kiểm soát tốc độ (sự kiện/giây), semaphore kiểm soát đồng thời (số chạy cùng lúc). Một request chạy 10 phút vẫn là "1 đồng thời" nhưng 1000 request/giây tức thời là vấn đề tốc độ. Backend thực thường đặt cả hai: rate limit chặn đợt dồn, semaphore chặn quá tải đồng thời.

Chọn burst cẩn thận. Burst quá nhỏ (=1) làm traffic bị "nghẹt" cứng nhắc, không chịu được đợt tự nhiên. Burst quá lớn cho phép đợt dồn lớn có thể vẫn làm ngợp backend trong khoảnh khắc. Cân theo khả năng chịu đợt của hệ thống phía sau — thường burst = vài giây tốc độ trung bình là hợp lý.

Rate limiter này là in-process, không phân tán. x/time/rate giới hạn trong một tiến trình. Nếu bạn chạy nhiều instance sau load balancer, mỗi instance có limiter riêng — tổng tốc độ = N × giới hạn. Rate limit phân tán (chia sẻ qua Redis) là bài toán khác, phức tạp hơn nhiều; đừng nhầm limiter in-process với giới hạn toàn cục.

Ba ý mang về

  1. Token bucket giới hạn tốc độ với burst: một xô chứa tối đa burst token, nạp rate token/giây, mỗi sự kiện tiêu 1 — đo thật burst 3 cho 3 lần Allow() đầu qua ngay rồi bị bỏ, và tốc độ hội tụ chính xác về cấu hình (200 sự kiện ở 50/giây → 50,2/giây).
  2. Ba cách lấy token cho ba tình huống: Allow (không chờ, bỏ — cho API trả 429 ngay), Wait(ctx) (chờ tới khi có token — cho worker), Reserve (báo trước delay — đo thật 0,50s khi hết token ở 2/giây, cho logic tùy biến).
  3. Rate limiter khác semaphore — thường cần cả hai: rate limiter giới hạn tốc độ (mỗi giây, cho burst), semaphore giới hạn đồng thời (cùng lúc) — backend thực đặt cả hai; và nhớ x/time/rate là in-process, nhiều instance thì tổng tốc độ nhân lên.

Phần sau ta xem một kỹ thuật đồng thời giải quyết vấn đề "nhiều request cùng hỏi một thứ": Phần sau mổ xẻ singleflight — cách gộp nhiều lời gọi trùng thành một để chống thundering herd (đàn sấm) khi cache miss, và đo thật hiệu quả.