چگونه یک محدودکننده نرخ (Rate Limiter) با پنجره لغزان در Redis ساختم
API ویدیوی ما قبلاً هر شب ساعت ۸ شب به وقت UTC کرش میکرد.
ترافیک واقعی نبود. چند اسکرپر (scraper) نقطه انتهایی (endpoint) ترند ما را پیدا کرده و به آن حملات سنگینی میکردند. پرسوجوهای (queries) پایگاه داده ما انباشته میشدند. کاربران واقعی به جای ویدیو، فقط چرخهای بارگذاری (loading spinners) را میدیدند.
ما به محدودکننده نرخ (rate limiting) نیاز داشتیم. اکثر روشهای ساده، مشکل را بدتر میکردند.
مشکل شمارندههای پنجره ثابت (Fixed Window):
اکثر مردم با پنجرههای ثابت شروع میکنند. شما درخواستها را در یک بلوک زمانی مشخص میشمارید. اگر محدودیت شما ۱۰۰ درخواست در دقیقه باشد، یک کاربر میتواند ۱۰۰ درخواست در ساعت ۱۱:۵۹:۵۹ و ۱۰۰ درخواست دیگر را در ساعت ۱۲:۰۰:۰۰ ارسال کند. این یعنی ۲۰۰ درخواست در یک ثانیه. اسکرپرها از این مرزها برای دور زدن محدودیتهای شما سوءاستفاده میکنند.
راه حل: پنجره لغزان (Sliding Window).
یک پنجره لغزان، درخواستها را در $N$ ثانیه اخیر نسبت به زمان حال میشمارد. هیچ مرزی برای سوءاستفاده وجود ندارد.
ما بر اساس نیازهایمان از دو روش مختلف استفاده میکنیم:
۱. لاگ پنجره لغزان (برای دقت بالا)
ما برای کاربران احراز هویت شده از مجموعههای مرتبشده Redis (ZSET) استفاده میکنیم. ما برای هر درخواست یک برچسب زمانی (timestamp) ذخیره میکنیم.
- از
ZREMRANGEBYSCOREبرای حذف ورودیهای قدیمی استفاده میکنیم. - از
ZCARDبرای شمارش درخواستهای فعلی استفاده میکنیم. - از
ZADDبرای ثبت درخواستهای جدید استفاده میکنیم.
برای جلوگیری از شرایط رقابتی (race conditions)، این منطق را در یک اسکریپت واحد Lua اجرا میکنیم. این کار عملیات را اتمیک (atomic) میکند. یک اسکریپت همه چیز را در یک رفت و برگشت به Redis مدیریت میکند. این کار مانع از آن میشود که دو درخواست همزمان تصور کنند که هنوز زیر حد مجاز هستند.
۲. شمارنده پنجره لغزان (برای مقیاسپذیری)
برای ترافیک ناشناس، ما میلیونها کلید داریم. نمیتوانیم هر برچسب زمانی را ذخیره کنیم. در عوض، از شمارندهای استفاده میکنیم که به پنجره فعلی و قبلی وزن میدهد. این روش حافظه بسیار کمتری مصرف میکند و بسیار کارآمد است.
درسهای کلیدی از محیط عملیاتی (production):
- از اسکریپتهای Lua استفاده کنید. این کار تضمین میکند که بررسی و نوشتن شما به عنوان یک اقدام واحد انجام شود.
- هدرهای استاندارد ارسال کنید. همیشه
X-RateLimit-RemainingوRetry-Afterرا ارسال کنید. این کار به کلاینتهای خوشرفتار کمک میکند تا به طور خودکار از ارسال درخواستهای بیش از حد خودداری کنند. - حالت Fail open را در نظر بگیرید. اگر Redis از کار افتاد، اجازه درخواست را بدهید. یک API تحت فشار بهتر از یک API کاملاً از کار افتاده است.
- کلاینتها را به درستی شناسایی کنید. از کلیدهای API برای کاربران و از IPهای هششده برای مهمانها استفاده کنید.
- محدودیتها را با نقاط انتهایی (endpoints) مطابقت دهید. یک نقطه انتهایی سنگین برای جستجو، نسبت به یک نقطه انتهایی ساده برای لیست کردن، به محدودیتهای سختگیرانهتری نیاز دارد.
نتیجه فوری بود. اوجهای ترافیکی در عصر ناپدید شدند. عملکرد پایگاه داده ما به حالت عادی بازگشت.
