API Engineering

تخفيف حدود معدل API دون إثارة استدلالات مكافحة إساءة الاستخدام

4 min read Published Updated 808 words

معظم تطبيقات تحديد معدل الطلبات (Rate Limiting) لواجهات API يمكن تجاوزها بسهولة عبر تدوير وكلاء (Proxy Rotation) ساذج — لكن نفس هذا التدوير يؤدي إلى تفعيل خوارزميات مكافحة الإساءة خلال دقائق. التحدي الحقيقي ليس فقط البقاء تحت الحد المسموح، بل القيام بذلك دون أن تبدو كروبوت. يتطلب ذلك فهم نطاقي تحديد المعدل المتميزين (لكل عنوان IP ولكل مفتاح)، وتطبيق تأخير متذبذب (Jittered Backoff)، وتدوير عشوائي لمجموعة الوكلاء، وتشكيل توقيت الطلبات لمحاكاة الزيارات البشرية الطبيعية. نمط الدلو الرمزي (Token Bucket) المطبق فوق مجموعة وكلاء يوفر أساسًا متينًا.

لكل عنوان IP مقابل لكل مفتاح: المحوران المزدوجان لتحديد المعدل

تعمل حدود المعدل على بُعدين مستقلين على الأقل: عنوان IP المصدر ومفتاح API (أو الرمز المميز). قد يسمح مفتاح واحد بـ 5,000 طلب في الساعة (الحد المسموح به للمستخدمين الموثّقين في GitHub)، لكن نفس المفتاح من عنوان IP واحد قد يتم تقييده بسقف اندفاعي أقل. الطلبات غير الموثّقة تكون أكثر تقييدًا — عادةً 60 طلبًا في الساعة لكل عنوان IP. تجاهل بُعد عنوان IP هو أسرع طريقة للحصول على استجابة 429 Too Many Requests (RFC 6585). يجب على مجموعة الوكلاء توزيع الطلبات عبر عدة عناوين IP لتجنب إشباع أي مصدر واحد، لكن كل عنوان IP لا يزال يشارك نفس المفتاح. إذا كان الحد العالمي للمفتاح هو 5,000/ساعة ولديك 50 وكيلًا، يمكن لكل وكيل إجراء 100 طلب فقط في الساعة قبل أن يتم تفعيل عداد مستوى المفتاح. قم بتحديد كلا الحدين قبل كتابة أي سطر من الكود.

التأخير مع التذبذب: الفرق بين المهذب والمتوقع

التأخير الأسي (Exponential Backoff) بدون تذبذب هو بصمة رقمية. ترى الخوادم طلبات تصل على فترات مضاعفة تمامًا وتصنفها كآلية. الحل هو التذبذب الكامل: sleep(random.uniform(0, min(cap, base * 2 ** attempt))). هذا يوزع إعادة المحاولات عبر النافذة الزمنية، مما يجعل النمط غير قابل للتمييز عن اندفاع المستخدمين الحقيقيين. توصي وثائق AWS الخاصة بالتأخير الأسي باستخدام التذبذب لهذا السبب بالضبط. المقتطف البرمجي التالي بلغة Python ينفذ دلوًا رمزيًا يعاد ملؤه بمعدل ثابت، وينام مع تذبذب عندما يكون فارغًا، ويدور الوكيل عند كل استحواذ ناجح:

import time
import random
from collections import deque

class TokenBucketProxyPool:
    def __init__(self, proxies, rate, burst):
        self.proxies = deque(proxies)
        self.rate = rate          # tokens per second
        self.burst = burst
        self.tokens = burst
        self.last_refill = time.monotonic()

    def refill(self):
        now = time.monotonic()
        elapsed = now - self.last_refill
        self.tokens = min(self.burst, self.tokens + elapsed * self.rate)
        self.last_refill = now

    def acquire(self):
        while True:
            self.refill()
            if self.tokens >= 1:
                self.tokens -= 1
                self.proxies.rotate(1)   # simple rotation, but see section below
                return self.proxies[0]
            else:
                sleep_time = (1 - self.tokens) / self.rate
                jitter = random.uniform(0, sleep_time * 0.5)
                time.sleep(sleep_time + jitter)

# Usage: pool = TokenBucketProxyPool(proxies, rate=2.0, burst=10)
#         proxy = pool.acquire()   # blocks until token available

تدوير مجموعة الوكلاء: عشوائية لا دورية

التدوير المتسلسل الدائري (Round-Robin) عبر قائمة الوكلاء يمكن اكتشافه. الخادم الذي يرى طلبات من 1.2.3.4، ثم 5.6.7.8، ثم 9.10.11.12 بنفس الترتيب سيربط النمط عبر عناوين IP. بدلاً من ذلك، اختر وكلاء عشوائيًا مع إعادة الاستبدال، وأضف فترة تبريد لكل وكيل. تقارير أدلة الوكلاء العامة عن معدلات فشل تتراوح بين 60–80% — الوكيل الذي يعيد استجابة 429 أو خطأ اتصال يجب إنزاله إلى قائمة ميتة لمدة 60 ثانية على الأقل. قم بتنفيذ اختيار عشوائي مرجح حيث تكون الوكلاء الناجحة حديثًا أكثر احتمالية للاختيار. يستخدم الدلو الرمزي أعلاه rotate(1) بسيطًا للتوضيح؛ في الإنتاج استبدله بـ random.choice() وتتبع عدد حالات الفشل لكل وكيل.

تشكيل الطلبات: محاكاة المتوسط دون تجاوز الخط

حتى مع وجود دلو رمزي ومجموعة وكلاء مثاليين، فإن معدل طلبات ثابت تمامًا قدره 2.0 في الثانية غير طبيعي. المستخدمون الحقيقيون يتوقفون، ويجمعون الطلبات، ويغيرون الفترات الزمنية بين الطلبات. أضف تأخيرًا عشوائيًا صغيرًا (مثل time.sleep(random.uniform(0.1, 0.5))) قبل كل استدعاء acquire()، وقم بتغيير معدل الدلو الرمزي بنسبة ±10% كل بضع دقائق. لكن لا تضف تأخيرات تتجاوز نافذة تحديد المعدل الموثقة لواجهة API — فهذا يبطل الغرض. الهدف هو البقاء ضمن الحد مع الظهور كمجموعة من العملاء الشرعيين. الإفراط في التشكيل (مثل إدخال تأخيرات تشبه سرعة الكتابة البشرية) هو جهد ضائع؛ واجهة API تهتم بتكرار الطلبات، وليس بتوقيت الضغط على المفاتيح.

التخفيف الأخلاقي: اعرف أين يقع الخط

كل تقنية موصوفة هنا هي تحسين مشروع — حتى تنتهك شروط الخدمة لواجهة API. معظم شروط الخدمة تحظر صراحةً "تجاوز حدود المعدل" أو "استخدام وسائل آلية للوصول إلى الخدمة". قراءة شروط الخدمة ليست اختيارية. إذا كانت واجهة API تسمح بـ 100 طلب في الدقيقة لكل مفتاح، فإن استخدام 50 وكيلًا كل منهم يرسل طلبين في الدقيقة هو امتثال. استخدام 100 وكيل كل منهم يرسل 100 طلب في الدقيقة ليس امتثالًا — بل هو إساءة استخدام، بغض النظر عن مدى ذكاء التذبذب. الفرق هو القصد والحجم. الدلو الرمزي المطبق فوق مجموعة وكلاء هو أداة؛ نفس الأداة التي تسحب بيانات دليل عام بشكل أخلاقي يمكن استخدامها أيضًا لشن هجوم حجب خدمة موزع (DDoS) على واجهة API صغيرة. وثّق أهداف تحديد المعدل لديك، وراجع سجلاتك بحثًا عن استجابات 429، ولا تتجاوز أبدًا السقف الموثق لكل مفتاح. إذا كنت بحاجة إلى إنتاجية أعلى، اطلب حدًا أعلى أو ادفع مقابل خطة مخصصة.