← Bài trước | Series hub | Tiếp theo →
Chương 3: Bảo Mật APIs Bằng Distributed Rate Limiting
Nếu caching là tấm khiên che chở cơ sở dữ liệu, thì Rate Limiting (giới hạn lưu lượng) lại là bộ giáp che chắn cho các máy chủ API của bạn khỏi các cuộc tấn công DDoS và tình trạng cạn kiệt tài nguyên gây ra bởi những clients lạm dụng.
Tại Sao Local Rate Limiting Thất Bại Trong Microservices
Answer-first: Các hệ thống limiters lưu trên RAM cục bộ (local RAM) thất bại bởi vì Load Balancers có nhiệm vụ phân tán traffic ra nhiều nodes. Một người dùng được cấp hạn mức 100 req/sec hoàn toàn có thể lợi dụng một cluster 5 nodes để gửi thốc 500 req/sec, dễ dàng chọc thủng hàng rào giới hạn. Việc gom trạng thái tập trung (Centralized state) qua Redis là bắt buộc.
Một sai lầm rất kinh điển là tận dụng các biến đếm token trên bộ nhớ trong (Local Cache) để làm rate limiters. Giả sử luật đặt ra là: “100 Requests/giây cho mỗi User”. Hệ thống của bạn chạy 5 backend servers. Ngay lúc User A bắn liên thanh 500 requests cùng một lúc, Load Balancer rẽ nhánh 100 requests về từng server. Chính vì mỗi server lại đếm theo một vùng nhớ cô lập của riêng mình, nó phán rằng: “User A vừa gửi 100 requests, con số này hợp lệ” và cho phép đi qua toàn bộ! Hệ quả: User A đã lách luật (bypass) giới hạn thành công trót lọt.
Muốn gỡ rối vụ này, chúng ta cần một Trạng Thái Tập Trung (Centralized State) được quản lý trực tiếp bởi Redis.
Token Bucket Đấu Với Leaky Bucket
Tồn tại hai thuật toán vang bóng một thời trong miền Rate Limiting:
Token Bucket (Xô Chứa Token):
- Cơ Chế: Các tokens được tự động sinh ra theo một nhịp cố định rồi thả vào một cái xô. Cứ mỗi request trôi qua sẽ ăn mất 1 token. Nếu cái xô cạn đáy, request đó bị hất văng (báo lỗi
HTTP 429). - Ưu Điểm: Chịu đựng tốt traffic tăng đột biến (burst). Khi cái xô đầy tràn (100 tokens), một người dùng có dư sức bắn 100 requests nội trong một mili-giây. Đây là chân ái của đám Public APIs (như Stripe, Twitter).
- Cơ Chế: Các tokens được tự động sinh ra theo một nhịp cố định rồi thả vào một cái xô. Cứ mỗi request trôi qua sẽ ăn mất 1 token. Nếu cái xô cạn đáy, request đó bị hất văng (báo lỗi
Leaky Bucket (Xô Lủng Đáy):
- Cơ Chế: Tất thảy mọi requests bị dội tuốt vào một cái xô. Cái xô này sẽ rỉ (leak) requests qua một lỗ thủng đáy theo một nhịp độ không thể xê dịch. Khách mà dội nước quá đà, cái xô sẽ tràn trề (từ chối).
- Ưu Điểm: Vị vua của trò Traffic Shaping (Định hình lưu lượng). Bất chấp đầu vào nhốn nháo cỡ nào, lưu lượng xả đầu ra luôn được chốt cứng. Cực kỳ hiệu nghiệm khi mang ra che chở cho các hệ thống legacy mỏng manh ở phía downstream.
Sức Mạnh Vô Song Của Thuật Toán GCRA
Answer-first: Thuật toán GCRA (Generic Cell Rate Algorithm) dò dẫm theo Thời Gian thay vì là bộ đếm số Token. Nó nhào nặn tinh hoa của Leaky Bucket gói gọn lại thành một công thức toán duy nhất, ép chỉ tốn đúng 1 Redis Key cho mỗi user, tối ưu bộ nhớ kinh hoàng.
Việc diễn trò mô phỏng xô chậu trong Redis thường đòi hỏi phải cắn răng tính toán số dư token lẫn mảng lưu vết thời gian (timestamps), dẫn đến nạn Race Conditions cắn trộm và phình (bloat) bộ nhớ.
Vị thư viện Rate Limiting bằng Go được xưng tụng nhiều nhất, github.com/go-redis/redis_rate, chính là kẻ đang sử dụng giải thuật GCRA. Căn bản thì GCRA không đi đếm số lượng Token; nó đếm nhịp Thời Gian. Nó tính toán ra con số “Theoretical Arrival Time” (TAT - Thời gian đến theo lý thuyết) — thời điểm sớm nhất để cái request tiếp theo có quyền qua cổng. Trò này tốn vỏn vẹn đúng một Key trên Redis, cắt rớt đáng kể gánh nặng lưu trữ (storage overhead).
Redis Lua Script: Chìa Khóa Của Tính Nguyên Tử (Atomicity)
Answer-first: Khâu tra xét rồi trừ bớt hạn mức trong Redis phải đạt được tính nguyên tử (atomic). Bằng cách phong ấn logic GCRA vào bên trong một đoạn mã Redis Lua Script, chúng ta triệt tiêu được các lỗi race conditions vì Redis sẽ chỉ thi hành lần lượt (sequentially) các tập mã Lua trên cái luồng đơn (single thread) độc nhất của nó.
Dưới áp suất high-concurrency, hai động tác Read (tra xét) và Write (trừ kho) bắt buộc phải đạt tính Atomic (Nguyên tử) tuyệt đối. Nếu cái đoạn code Go của bạn vọc hàm GET limit kế đó bồi thêm lệnh SET limit = limit - 1, một kẽ hở Race Condition nguy hiểm sẽ tự động lòi ra.
Redis lấp bít kẽ hở đó thông qua Lua Scripting. Hễ bạn cấy lệnh EVAL nạp mã Lua Script, Redis sẽ đè sập (lock) toàn cục mảng engine, cứ thế chạy kịch bản lần lượt tính từ đầu chí cuối. Cấm tiệt có câu lệnh nào chen ngang đứt gánh. Đó cũng là đáp án tại sao thư viện redis_rate và hàng loạt limiters trứ danh production-grade đều giấu trọn phần lõi của họ nằm ngấm trong mấy dòng Lua scripts.
Hợp sức cùng lúc bộ ngàm Golang Middleware, Redis Lua Scripts, và giải thuật GCRA, bạn đã tự thân lập được một chốt kiểm dịch bất khả xâm phạm trùm bảo kê toàn diện cho hệ thống Microservices của mình.