Zoom-in: Rate Limiter

Gửi quá nhiều request liên tiếp lên một API, và bạn nhận về mã lỗi quả quyết: “429 Too Many Requests”.
graph LR
C(["💻 Client"]) -->|"Request liên tục"| RL["🛡️ Rate Limiter"]
RL -->|"Vượt ngưỡng"| Reject["❌ 429 Too Many Requests"]
RL -->|"Hợp lệ"| API["🖥️ API Service"]
style C fill:#1e3a5f,stroke:#3b82f6,color:#93c5fd
style RL fill:#3b2a1a,stroke:#f59e0b,color:#fcd34d
style Reject fill:#1e293b,stroke:#e11d48,color:#fda4af
style API fill:#1a3a2a,stroke:#22c55e,color:#86efac
Phóng to dần vào đó.
Layer 1 — Token Bucket: Thuật toán cấp phát cơ bản
Để chặn bớt request thừa, hệ thống cần một cách đếm tần suất đơn giản và tốn ít bộ nhớ nhất. Thuật toán phổ biến nhất là Token Bucket.
graph TD
Refill["💧 Nạp mới: +2 token/giây"] --> Bucket{"🪣 Token Bucket\n(Max: 5 tokens)"}
Req["📥 Request mới"] --> Check{"Kiểm tra Bucket"}
Check -->|"Còn token (Tiêu tốn 1)"| Allow["✅ Cho qua"]
Check -->|"Hết token"| Reject["❌ Trả về lỗi 429"]
Bucket -.-> Check
style Refill fill:#1e293b,stroke:#475569,color:#cbd5e1
style Bucket fill:#3b2a1a,stroke:#f59e0b,color:#fcd34d
style Req fill:#1e3a5f,stroke:#3b82f6,color:#93c5fd
Mỗi client (phân biệt qua IP hoặc API Key) được gán một "chiếc xô" ảo chứa các token. Xô có dung lượng tối đa (ví dụ: 5 tokens) và được tự động nạp đầy lại theo một tỷ lệ cố định (ví dụ: 2 tokens mỗi giây).
Mỗi request gửi đến sẽ tiêu tốn 1 token trong xô. Nếu xô hết sạch token, request lập tức bị từ chối với mã lỗi 429. Thuật toán này rất nhẹ và cho phép hệ thống chịu được các đợt bùng nổ traffic ngắn hạn (burst traffic) khi xô đang đầy.
Layer 2 — Sliding Window Counter: Độ chính xác theo thời gian trượt
Để giải quyết nhược điểm của việc đếm theo chu kỳ cố định (ví dụ: tối đa 10 request từ phút 1:00 đến 2:00, rồi reset lại ở phút 2:00), Rate Limiter áp dụng thuật toán cửa sổ trượt (Sliding Window Counter).
graph LR
subgraph Chu kỳ trước: 10 requests
W1["Cửa sổ cũ (phút trước)"]
end
subgraph Chu kỳ hiện tại: 2 requests
W2["Cửa sổ mới (phút này)"]
end
W1 -.->|"Trượt 30% thời gian"| SW["Cửa sổ trượt thực tế"]
W2 -.-> SW
style W1 fill:#1e293b,stroke:#334155,color:#94a3b8
style W2 fill:#1e3a5f,stroke:#3b82f6,color:#93c5fd
style SW fill:#3b2a1a,stroke:#f59e0b,color:#fcd34d
Thay vì reset bộ đếm về 0 khi sang phút mới, thuật toán ước tính số lượng request dựa trên tỷ lệ trượt thời gian.
Ví dụ: Nếu ta đang ở giây thứ 18 của phút hiện tại (trượt 30% thời gian), Rate Limiter sẽ tính số request bằng công thức: Số request cửa sổ cũ * 70% + Số request cửa sổ mới. Cách này đảm bảo attacker không thể gửi dồn dập request vào thời điểm chuyển giao chu kỳ để vượt ngưỡng.
Layer 3 — Distributed Limiting: Đồng bộ nguyên tử với Redis và Lua Script
Khi ứng dụng chạy trên cluster gồm nhiều server, việc lưu trữ bộ đếm trên bộ nhớ RAM của từng server đơn lẻ sẽ làm mất tác dụng của Rate Limiter. Ta cần một bộ lưu trữ tập trung như Redis.
sequenceDiagram
participant S1 as App Server 1
participant R as Redis (Tập trung)
participant S2 as App Server 2
Note over S1,S2: Cùng lúc nhận được request của IP 1.2.3.4
S1->>R: Thực thi Lua Script (Kiểm tra & trừ token)
Note over R: Xử lý đơn luồng (Atomic)<br/>Trừ 1 token thành công
R-->>S1: Đồng ý (Cho qua)
S2->>R: Thực thi Lua Script (Kiểm tra & trừ token)
Note over R: Hết token!
R-->>S2: Từ chối (Trả về 429)
Nếu hai server cùng đọc giá trị đếm từ Redis lên, cộng thêm 1 rồi ghi ngược lại, một lỗi race condition sẽ xảy ra khiến bộ đếm bị ghi đè sai lệch.
Để giải quyết, các kỹ sư gửi toàn bộ logic kiểm tra và cập nhật xuống Redis dưới dạng một đoạn Lua Script. Redis chạy đơn luồng (single-threaded) sẽ thực thi đoạn mã Lua này một cách nguyên tử (atomic) — đảm bảo không có hai request nào có thể tranh chấp ghi đè dữ liệu của nhau.
Full picture
sequenceDiagram
participant C as Client
participant LB as Load Balancer
participant App as App Server
participant Redis as Redis Cache
C->>LB: GET /api/resource (IP: 1.2.3.4)
LB->>App: Chuyển tiếp Request
App->>Redis: EVALSHA rate_limit.lua (Key: limit:1.2.3.4)
Note over Redis: Thực thi Lua Script nguyên tử:<br/>1. Đọc số token hiện tại<br/>2. Nếu còn, trừ 1 và cập nhật TTL
alt Còn token trong xô
Redis-->>App: Trả về: [1, số token còn lại]
App->>App: Xử lý nghiệp vụ chính
App-->>C: 200 OK
else Hết token
Redis-->>App: Trả về: [0, 0]
App-->>C: 429 Too Many Requests (Retry-After: 30s)
end
Takeaway
Rate Limiter là lá chắn bảo vệ hệ thống trước các tác nhân gây quá tải ở tầng ứng dụng. Thiết kế một hệ thống rate limit phân tán đòi hỏi sự cân bằng giữa độ chính xác và hiệu năng: việc thực thi Lua Script trên Redis tập trung mang lại sự chính xác tuyệt đối nhưng cũng biến Redis thành một điểm nghẽn cổ chai (bottleneck) mới nếu lượng request gửi đến quá lớn.
Bài viết được hỗ trợ bởi Amy 🌸 - AI Assistant. Nội dung đã được kiểm duyệt bởi tác giả.
Related Posts
Zoom-in: WebSocket
Chat app cập nhật tin nhắn tức thời mà không cần reload trang. Cách WebSocket giải thoát ứng dụng khỏi giới hạn một chiều của HTTP.
Zoom-in: Virtual Memory
Chạy nhiều ứng dụng cùng lúc, mỗi ứng dụng đều nghĩ mình đang sở hữu toàn bộ RAM hệ thống. Phép thuật nào giúp cô lập bộ nhớ an toàn như vậy?
Zoom-in: Decoding Parameters
Kiểm soát tính ngẫu nhiên của mô hình. Phóng to vào cơ chế phân bổ xác suất và cách mô hình lựa chọn từ tiếp theo.