การจำกัดอัตราคำขอ
rate limiter คืออะไร?
หัวข้อที่มีชื่อว่า “rate limiter คืออะไร?”rate limiter คือกลไกที่จำกัดจำนวนคำขอที่ไคลเอนต์สามารถส่งได้ภายในช่วงเวลาที่กำหนด Redis ทำให้สิ่งนี้ทำได้ง่ายด้วยคำสั่ง atomic สองตัว ได้แก่ INCR สำหรับนับคำขอ และ EXPIRE สำหรับกำหนดขอบเขตของช่วงเวลา
INCR + EXPIRE ทำงานอย่างไร
หัวข้อที่มีชื่อว่า “INCR + EXPIRE ทำงานอย่างไร”สำหรับคำขอที่เข้ามาแต่ละครั้ง ให้เพิ่มค่า counter ที่มี key ระบุตามผู้ใช้และช่วงเวลา เมื่อมีการเรียกครั้งแรก ซึ่งก็คือเมื่อ counter มีค่าเท่ากับ 1 ให้ตั้งค่า expiry บน key นั้น หาก counter เกินขีดจำกัด ให้ปฏิเสธคำขอนั้น
flowchart TD
A["Incoming request"] --> B["count = INCR rate:${userId}:window"]
B --> C{"count == 1?"}
C -->|"Yes (first hit)"| D["EXPIRE rate:${userId}:window 60"]
C -->|"No"| E{"count > 100?"}
D --> E
E -->|"Yes"| F["Reject (HTTP 429)"]
E -->|"No"| G["Allow request"] key ที่เข้ารหัสช่วงเวลาไว้ ทำให้แต่ละช่วงเวลาใหม่ได้รับ counter ใหม่โดยอัตโนมัติ
ตัวอย่างใน Redis CLI
หัวข้อที่มีชื่อว่า “ตัวอย่างใน Redis CLI”127.0.0.1:6379> INCR rate:user:42:window1(integer) 1127.0.0.1:6379> EXPIRE rate:user:42:window1 60(integer) 1127.0.0.1:6379> INCR rate:user:42:window1(integer) 2127.0.0.1:6379> TTL rate:user:42:window1(integer) 58INCR ครั้งแรกคืนค่า 1 จึงตั้งค่า expiry ทันที การเพิ่มค่าครั้งถัดไปจะนับในช่วงเวลาเดิม และ TTL ยืนยันว่า key จะหมดอายุหลังจากช่วงเวลาสิ้นสุด
INCR rate:user:42:window1
EXPIRE rate:user:42:window1 60
INCR rate:user:42:window1
TTL rate:user:42:window1Fixed window กับ sliding window
หัวข้อที่มีชื่อว่า “Fixed window กับ sliding window”INCR + EXPIRE ให้ fixed window: คำขอทั้งหมดใน bucket 60 วินาทีเดียวกันจะใช้ counter ร่วมกัน การส่งคำขอจำนวนมากที่ปลายของช่วงเวลาหนึ่งและต้นของช่วงเวลาถัดไปอาจทำให้อัตราที่อนุญาตเพิ่มขึ้นเป็นสองเท่าที่ขอบเขตได้
สำหรับ sliding window ที่แท้จริง คุณจะต้องใช้ sorted set: เก็บ timestamp ของแต่ละคำขอด้วย ZADD ตัดรายการเก่าออกด้วย ZREMRANGEBYSCORE แล้วนับรายการที่ยังมีผลด้วย ZCARD วิธีนี้แม่นยำกว่าแต่มีต้นทุนต่อคำขอสูงกว่าเล็กน้อย
ข้อแลกเปลี่ยน
หัวข้อที่มีชื่อว่า “ข้อแลกเปลี่ยน”| ตัวเลือก | Benefit | Cost |
|---|---|---|
Fixed window (INCR + EXPIRE) | ง่าย เร็ว ใช้เพียง 2 คำสั่งต่อ request | อัตราที่อนุญาตอาจเพิ่มเป็นสองเท่าได้ที่รอยต่อของ window |
Sliding window (ZADD + ZREMRANGEBYSCORE) | แม่นยำกว่า ไม่มี edge burst | ใช้ memory และ CPU มากกว่าต่อ request หนึ่งครั้ง |
| Token bucket | รองรับ burst traffic แบบควบคุมได้ | ต้องคำนวณ refill rate เพิ่มความซับซ้อนของ logic |
ข้อผิดพลาดที่พบบ่อย
หัวข้อที่มีชื่อว่า “ข้อผิดพลาดที่พบบ่อย”- ลืมตั้ง
EXPIREตอน count == 1 — ถ้าไม่ตั้ง TTL ให้ counter key จะไม่มีวันรีเซ็ต ผู้ใช้จะโดน rate limit ถาวรหลังแตะขีดจำกัดครั้งแรก - ใช้ fixed window แล้วคิดว่าป้องกัน burst ได้ 100% — คำขอที่กระจุกอยู่ปลาย window หนึ่งกับต้น window ถัดไปสามารถรวมกันเกินขีดจำกัดจริงได้ถึงสองเท่า
- ไม่คืน HTTP header ที่บอกสถานะ rate limit ให้ client — ถ้าไม่ส่ง header อย่าง
Retry-Afterclient จะ retry แบบสุ่มแทนที่จะรอเวลาที่เหมาะสม
💡 ตัวอย่างจากของจริง
GitHub API — ใช้ counter แบบ
INCR/EXPIREต่อ token เพื่อจำกัดจำนวน request ต่อชั่วโมง พร้อมส่ง header บอกโควตาที่เหลือCloudflare — ใช้กลไก rate limiting ที่อิงหลักการเดียวกับ sliding window เพื่อป้องกัน DDoS และ abuse ที่ edge ก่อนถึง origin server