วิธีที่ผมสร้าง 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 วินาทีล่าสุดโดยอ้างอิงจากเวลาปัจจุบัน ทำให้ไม่มีรอยต่อให้ใครมาใช้ประโยชน์ได้

เราใช้วิธีที่แตกต่างกันสองแบบตามความต้องการของเรา:

  1. 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 สองรายการประมวลผลพร้อมกันแล้วเข้าใจผิดว่ายังไม่เกินขีดจำกัดที่ตั้งไว้ในเวลาเดียวกัน

  1. 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