วิธีที่ผมสร้าง Sliding Window Rate Limiter ใน Redis
Video API ของเราเคยล่มทุกเย็นตอนเวลา 20:00 น. UTC
มันไม่ใช่ทราฟฟิกจริง แต่มีพวก Scraper ไม่กี่เจ้าที่เจอ trending endpoint ของเราแล้วก็รัวยิงเข้ามาไม่หยุด ทำให้คิวของ database queries พอกพูนขึ้นเรื่อยๆ ส่งผลให้ผู้ใช้งานจริงเห็นแต่หน้าจอโหลด (loading spinners) แทนที่จะได้ดูวิดีโอ
เราจำเป็นต้องทำ rate limiting แต่ระเบียบวิธีแบบง่ายๆ ส่วนใหญ่กลับทำให้ปัญหาแย่ลงกว่าเดิม
ปัญหาของ Fixed Window counters: คนส่วนใหญ่มักจะเริ่มด้วย fixed windows โดยการนับจำนวน request ในช่วงเวลาที่กำหนด หากคุณตั้งขีดจำกัดไว้ที่ 100 ครั้งต่อนาที ผู้ใช้สามารถส่ง 100 request ตอนเวลา 11:59:59 และส่งอีก 100 request ตอนเวลา 12:00:00 ซึ่งเท่ากับว่ามีการส่งถึง 200 request ภายในเวลาเพียงวินาทีเดียว พวก Scraper มักจะใช้ประโยชน์จากรอยต่อเหล่านี้เพื่อหลบเลี่ยงขีดจำกัดที่คุณตั้งไว้
ทางออกคือ: Sliding Window. Sliding window จะนับจำนวน request ในช่วง N วินาทีล่าสุดโดยอ้างอิงจากเวลาปัจจุบัน ทำให้ไม่มีรอยต่อให้ใครมาใช้ประโยชน์ได้
เราใช้วิธีที่แตกต่างกันสองแบบตามความต้องการของเรา:
- Sliding Window Log (เพื่อความแม่นยำ) เราใช้ Redis Sorted Sets (ZSET) สำหรับผู้ใช้ที่ผ่านการยืนยันตัวตน (authenticated users) โดยเราจะเก็บ timestamp ของทุกๆ request
- เราใช้ ZREMRANGEBYSCORE เพื่อลบข้อมูลเก่าออก
- เราใช้ ZCARD เพื่อนับจำนวน request ปัจจุบัน
- เราใช้ ZADD เพื่อบันทึก request ใหม่
เพื่อป้องกันปัญหา race conditions เราจึงรัน logic นี้ภายใน Lua script เพียงตัวเดียว ซึ่งจะทำให้การทำงานเป็นแบบ atomic โดย script เดียวจะจัดการทุกอย่างในการเชื่อมต่อกับ Redis เพียงครั้งเดียว วิธีนี้ช่วยป้องกันไม่ให้ request สองรายการประมวลผลพร้อมกันแล้วเข้าใจผิดว่ายังไม่เกินขีดจำกัดที่ตั้งไว้ในเวลาเดียวกัน
- Sliding Window Counter (เพื่อการรองรับสเกล) สำหรับทราฟฟิกที่ไม่ระบุตัวตน (anonymous traffic) เรามี key เป็นจำนวนหลายล้านตัว ซึ่งเราไม่สามารถเก็บทุก timestamp ได้ เราจึงใช้ counter ที่มีการถ่วงน้ำหนัก (weight) ระหว่าง window ปัจจุบันและ window ก่อนหน้าแทน วิธีนี้ใช้หน่วยความจำน้อยกว่ามากและมีประสิทธิภาพสูง
บทเรียนสำคัญจากการใช้งานจริง:
- ใช้ Lua scripts เพื่อให้มั่นใจว่าขั้นตอนการตรวจสอบ (check) และการเขียนข้อมูล (write) เกิดขึ้นเป็นขั้นตอนเดียวกัน
- ส่ง standard headers ออกไปเสมอ เช่น X-RateLimit-Remaining และ Retry-After เพื่อช่วยให้ client ที่ทำตามกฎสามารถหยุดส่ง request ชั่วคราวได้โดยอัตโนมัติ
- ใช้หลักการ Fail open หาก Redis ล่ม ให้ยอมรับ request นั้นไปก่อน เพราะ API ที่ทำงานหนักเกินไป (overloaded) ยังดีกว่า API ที่ตายสนิทจนใช้งานไม่ได้เลย
- ระบุตัวตน client ให้ถูกต้อง โดยใช้ API keys สำหรับผู้ใช้ และใช้ hashed IPs สำหรับ guest
- ตั้งขีดจำกัดให้เหมาะสมกับแต่ละ endpoint โดย endpoint ที่มีการค้นหา (search) ซึ่งใช้ทรัพยากรสูง ควรมีขีดจำกัดที่เข้มงวดกว่า endpoint ที่ใช้แสดงรายการ (list) แบบง่ายๆ
ผลลัพธ์ที่ได้นั้นเห็นผลทันที ยอดการใช้งานที่พุ่งสูงขึ้นในช่วงเย็นหายไป และประสิทธิภาพของ database ก็กลับเข้าสู่สภาวะปกติ
แหล่งที่มา: https://dev.to/ahmet_gedik778845/how-i-built-a-sliding-window-rate-limiter-for-our-video-api-in-redis-4k1c
