في المشهد الحديث للخدمات المصغرة عالية الإنتاجية، فإن حماية واجهات برمجة التطبيقات (APIs) الخاصة بك من الإساءة، وهجمات حجب الخدمة الموزعة (DDoS)، والإحمال العرضي ليس خياراً بل ضرورة حاسمة. بينما يعمل تحديد المعدل الأساسي بشكل جيد في التطبيقات ذات العقدة الواحدة، تتطلب الأنظمة الموزعة حالة مشتركة لضمان التنفيذ المتسق عبر عدة مثيلات. هنا يبرز دور Redis، ولكن لضمان الصحة الحقيقية، يجب عليك تجاوز عمليات المفاتيح والقيم البسيطة والاستفادة من قوة سكريبتات Lua.
تحدي الذرية في الأنظمة الموزعة
تخيل أن لديك ثلاثة خوادم لواجهات برمجة التطبيقات خلف موزع حمل. يرسل مستخدم خبيث 100 طلب في الثانية، ويضرب الخادم A وB وC بطريقة الدوران (Round-Robin). إذا قمت بتخزين عدادات تحديد المعدل محلياً على كل خادم، فإن المستخدم يحصل فعلياً على 300 طلب في الثانية مسموح بها، متجاوزاً حد 100 طلب/ثانية الخاص بك. أنت بحاجة إلى عداد مركزي.
يبدو أن مجرد زيادة مفتاح في Redis (على سبيل المثال، INCR) هو حل، لكنه يفشل تحت المنافسة العالية دون منطق نافذة زمنية مناسب. قد تتضمن النهج البدائية تعيين مفتاح مع انتهاء صلاحية، لكن هذا يؤدي إلى "مشكلة الحدود". يمكن للمستخدم تقديم 10 طلبات في الثانية 0، و10 طلبات في الثانية 1، مما يضاعف فعلياً معدل نقل البيانات الخاص به فقط عن طريق توقيت طلباته على حواف النوافذ الزمنية. لحل هذا بدقة، نحتاج إلى فحص العدد وتعيين انتهاء الصلاحية بشكل ذري.
لماذا سكريبتات Lua؟
ينفذ Redis سكريبتات Lua بشكل ذري. هذا يعني أنه بمجرد بدء تشغيل السكربت، لن يتم مقاطعته بواسطة أي أمر آخر. هذه الخاصية ضرورية لتحديد المعدل لأننا نحتاج إلى:
- فحص ما إذا كان مفتاح تحديد المعدل موجوداً.
- استرداد عدده الحالي.
- زيادة العداد.
- تعيين انتهاء الصلاحية إذا كان هذا هو الطلب الأول في النافذة الزمنية.
- إرجاع الحالة إلى المتصل.
إن القيام بذلك باستخدام أوامر GET و SET منفصلة عرضة لظروف السباق (Race Conditions). من خلال تضمين هذا المنطق في سكريبت Lua، نضمن أن كتلة المنطق بأكملها تنفذ كوحدة واحدة غير قابلة للتجزئة.
تنفيذ عداد النافذة المنزلقة
يُقدم أدناه تنفيذ قوي لسكريبت Lua لمحدد معدل النافذة الثابتة. يفحص هذا السكربت عدد الطلبات ضمن نافذة زمنية محددة (معرفة بالميلي ثانية).
-- المفتاح لتخزين عداد تحديد المعدل
local key = KEYS[1]
-- الوقت الحالي بالميلي ثانية
local now = tonumber(ARGV[1])
-- حجم النافذة بالميلي ثانية
local window = tonumber(ARGV[2])
-- الحد الأقصى المسموح به من الطلبات
local limit = tonumber(ARGV[3])
-- حساب بداية النافذة الحالية
local window_start = now - window
-- إزالة الإدخالات المنتهية الصلاحية (تنظيف بسيط، على الرغم من أن المجموعة المرتبة أفضل للنوافذ المنزلقة الصارمة)
-- للنافذة الثابتة، نتحقق فقط من وجود المفتاح
local count = redis.call('GET', key)
if not count then
-- الطلب الأول في هذه النافذة
redis.call('SET', key, 1)
redis.call('EXPIRE', key, math.ceil(window / 1000))
return {1, 1}
else
count = tonumber(count)
if count < limit then
-- الطلب مسموح به
redis.call('INCR', key)
return {0, count + 1}
else
-- تم تجاوز حد المعدل
return {1, count}
end
end
في هذا السكربت، يمثل KEYS[1] المعرف الفريد للعميل (على سبيل المثال، عنوان IP الخاص بهم أو مفتاح API). يمرر ARGV الطابع الزمني الحالي، ومدة النافذة، والحد الأقصى المسموح به. يعيد السكربت مصفوفة حيث يشير العنصر الأول إلى ما إذا كان الطلب قد تم حظره (1) أو السماح به (0)، ويوفر العنصر الثاني عدد الطلبات الحالي.
التكامل العملي مع Python
إن تكامل هذا مع تطبيق Python Flask باستخدام مكتبة redis-py أمر مباشر. تقوم بتحميل السكربت في عميل Redis مرة واحدة ثم استدعاؤه بالإشارة.
import time
import redis
# تهيئة عميل Redis
r = redis.Redis(host='localhost', port=6379, decode_responses=True)
# تحميل سكريبت Lua
lua_script = """
-- (أدخل سكريبت Lua من الأعلى هنا)
"""
rate_limit_fn = r.register_script(lua_script)
def check_rate_limit(user_id, limit=10, window=60000):
now = int(time.time() * 1000)
key = f"rate_limit:{user_id}"
# تنفيذ سكريبت Lua
result = rate_limit_fn(keys=[key], args=[now, window, limit])
status, count = result
if status == 1:
# إرجاع 429 Too Many Requests
return False, count
# إرجاع 200 OK
return True, count
الخاتمة
يوفر تنفيذ تحديد المعدل الموزع باستخدام Redis وسكريبتات Lua أساساً قوياً وآمن وعالي الأداء لبنية واجهات برمجة التطبيقات الخاصة بك. من خلال ضمان الذرية، تمنع ظروف السباق وتفرض سياسات الأمان الخاصة بك بدقة. لا يحمي هذا النهج فقط الخلفية الخاصة بك من الإحمال، بل يحافظ أيضاً على تجربة عادلة ومتسقة للمستخدمين الشرعيين، بغض النظر عن أي مثيل للخادم يتعامل مع طلبهم.