الموثوقية والإتاحة والمرونة

تحديد معدل الطلبات

18 دقيقة الدرس 4 من 10

تحديد معدل الطلبات

كل خدمة لها طاقة استيعابية محدودة. وبدون ضوابط، يمكن لعميل واحد يتصرف بشكل خاطئ — خطأ في تطبيق جوال، أو سكريبت منفلت، أو هجوم حرمان من الخدمة مقصود — أن يستنزف تلك الطاقة كاملاً ويُعطّل الخدمة لجميع المستخدمين الآخرين. تحديد معدل الطلبات (Rate Limiting) هو ممارسة وضع سقف لعدد الطلبات التي يمكن لعميل ما (أو فئة من العملاء) إرسالها خلال نافذة زمنية معينة، وبذلك يتحول الطلب غير المحدود إلى شيء يمكن للنظام استيعابه بأمان.

لماذا يُعدّ تحديد المعدل أمراً بالغ الأهمية

يقع تحديد المعدل عند تقاطع الموثوقية والعدالة. إذ يحمي ثلاثة أشياء في آنٍ واحد:

  • التبعيات في اتجاه المصب. واجهة برمجية تطبيقاتك (API) تستدعي قاعدة بيانات، أو معالج دفع خارجي، أو خدمة استدلال بالذكاء الاصطناعي — لكل منها طاقتها الخاصة. إغراق إحداها يؤدي إلى فشل متتالي.
  • التكلفة. موارد الحوسبة السحابية مُدارة بالاستخدام. ارتفاع مفاجئ في حجم الطلبات يمكن أن يُولّد فاتورة ضخمة قبل أن يستيقظ أحد.
  • العدالة في الوصول. في المنصات متعددة المستأجرين، لا ينبغي لمستأجر واحد أن يُجوّع الآخرين. يُرسّخ تحديد المعدل هذا العقد الضمني.

أرقام حقيقية توضح ذلك: قد تسمح واجهة برمجية عامة شهيرة بـ 60 طلباً/دقيقة في الخطة المجانية، و1000 طلب/دقيقة في الخطة المدفوعة. أما بوابة الدفع فقد تحدّ أي تاجر بـ 100 معاملة متزامنة. وقد تحمي خدمة مصغّرة داخلية نفسها بـ 5000 طلب/ثانية لكل مُستدعٍ.

خوارزمية دلو الرموز (Token Bucket)

دلو الرموز هو الخوارزمية الأكثر انتشاراً — وتعتمد عليها AWS API Gateway وStripe ومعظم محددات المعدل السحابية. تخيّل الصورة التالية: دلو يحتوي على رموز (Tokens):

  • تُضاف الرموز بمعدل ثابت لإعادة التعبئة (مثلاً: 100 رمز/ثانية)، بحد أقصى سعة محددة (مثلاً: 500 رمز).
  • يستهلك كل طلب وارد رمزاً واحداً (أو أكثر للعمليات المكلفة).
  • إذا كان الدلو يحتوي على عدد كافٍ من الرموز، يُسمح بالطلب وتُخصَم الرموز. وإلا يُرفض الطلب (429 Too Many Requests) أو يُوضع في طابور الانتظار.

الخاصية الجوهرية لهذه الخوارزمية هي قابلية التدفق المتقطع (Burstability). إذا كان العميل خاملاً، يمتلئ دلوه تدريجياً حتى يبلغ سعته الكاملة، ثم يستطيع إرسال 500 طلب دفعة واحدة قبل أن يتدخل المحدد. وهذا مرغوب فيه في الغالب — فالمستخدم الذي لم يُرسل أي طلب منذ خمس دقائق ينبغي أن يتمكن من تحميل لوحة تحكم تُطلق 20 طلباً متوازياً.

Token Bucket Algorithm Refill Timer 100 tokens / sec add tokens Token Bucket capacity = 500 Incoming Request tokens > 0? consume Allow 200 OK yes Reject 429 Too Many no Tokens refill at a steady rate; bursts are allowed up to bucket capacity.
دلو الرموز: تُعبَّأ الرموز بمعدل ثابت، ولا يُسمح بالطلب إلا حين يتوفر رمز.
اختصار في التنفيذ: لا تحتاج عملياً إلى خيط تنفيذ خلفي (background thread) لإعادة تعبئة الرموز. خزّن قيمتين في Redis لكل مفتاح: tokens وlast_refill_ts. عند كل طلب، احسب عدد الرموز التي كان ينبغي إضافتها منذ آخر تعبئة، أضفها (بحد أقصى السعة)، ثم حاول استهلاك رمز واحد — وكل ذلك في سكريبت Lua واحد لضمان الذرية (Atomicity).

خوارزمية الدلو المتسرب (Leaky Bucket)

يعكس نموذج الدلو المتسرب المنظور. بدلاً من تتبع الرموز، نُنمذج دلواً يتسرب بمعدل ثابت. الطلبات تملأ الدلو؛ وعندما يمتلئ، تُسقط الطلبات الجديدة.

  • للدلو سعة ثابتة (عمق الطابور).
  • تُعالَج الطلبات (تتسرب للخارج) بمعدل ثابت (مثلاً: 100 طلب/ثانية).
  • إذا وصل تدفق مفاجئ ولم يمتلئ الدلو بعد، تُضاف الطلبات إلى الطابور. وإن امتلأ، تُسقط الطلبات الزائدة.

الفارق الجوهري عن دلو الرموز: ينتج الدلو المتسرب معدل خرج ثابتاً وسلساً. بصرف النظر عن مدى انتظام المدخلات، تتلقى الخدمة في اتجاه المصب تدفقاً منتظماً تماماً. وهذا مثالي لحماية نظام خلفي بطيء (مثل قاعدة بيانات قديمة لا تتحمل سوى 50 عملية كتابة/ثانية) أو لتشكيل حركة مرور الشبكة.

المقايضة: التدفقات المتقطعة المشروعة تُؤخَّر أو تُسقط. إذا أرسل عميلك 50 طلباً في 50 ميلي ثانية، وكان معدل التسرب 100/ثانية، فستنتشر تلك الطلبات عبر 500 ميلي ثانية من الخرج — مما يزيد زمن الاستجابة على العميل.

Token Bucket vs Leaky Bucket Comparison Token Bucket Bursty Input Bucket capacity: 500 Stores tokens not requests refill: 100/s Bursty Output (allows bursts) Leaky Bucket Bursty Input Bucket capacity: queue Queues requests leak: 100/s Drop (full) Smooth Output (constant rate)
دلو الرموز مقابل الدلو المتسرب: الأول يسمح بالتدفقات المتقطعة، والثاني يُخرج معدلاً ثابتاً وسلساً.

خوارزميات أخرى باختصار

ثمة خوارزميتان أبسط تستحقان المعرفة:

  • عداد النافذة الثابتة (Fixed Window Counter). تُقسَّم الزمن إلى نوافذ ثابتة (مثلاً: كل دقيقة). يُحتسب عدد الطلبات لكل نافذة ولكل مفتاح. بسيط التنفيذ، لكن العميل يستطيع مضاعفة التدفق عند حدود النافذة — مئة طلب في الثانية الأخيرة من الدقيقة الأولى، ثم مئة في الثانية الأولى من الدقيقة الثانية.
  • سجل النافذة المنزلقة (Sliding Window Log). يُخزّن طابعاً زمنياً لكل طلب خلال آخر N ثانية. دقيق تماماً لكنه مكلف من حيث الذاكرة على نطاق واسع. نادراً ما يُستخدم في أنظمة ذات حركة مرور عالية.
  • عداد النافذة المنزلقة (Sliding Window Counter). يمزج بين الاثنين: يستخدم عداد النافذة الحالية مضافاً إليه جزء موزون من النافذة السابقة. يُقرّب سجل النافذة المنزلقة باستخدام ذاكرة O(1). يُستخدم من قِبَل Cloudflare وتنفيذات قائمة على Redis.
دلو الرموز = الخيار الأفضل افتراضياً. يتعامل مع التدفقات المتقطعة بشكل طبيعي، وتنفيذه في Redis رخيص، ويتطابق مع ما يتوقعه المستخدمون: حصة تتراكم مع الوقت ويمكن إنفاقها دفعةً واحدة.

أين يُطبَّق تحديد المعدل

تحديد المعدل ليس بوابة واحدة — يجب أن يظهر على طبقات متعددة:

  1. بوابة API / الحافة. حد شامل خشن لكل مفتاح API أو عنوان IP. يوقف الإساءة قبل وصولها إلى خدماتك.
  2. بين الخدمات. كل خدمة مصغّرة تُطبّق حدوداً على مُستدعييها. يمنع خدمة واحدة من إغراق تبعية مشتركة.
  3. على مستوى المستخدم. مرتبط بهوية المستخدم المصادق عليه، لا بعنوان IP. أكثر دقة في المنصات متعددة المستأجرين حيث يشترك مستخدمون كثيرون في عنوان IP واحد (NAT المؤسسي).
  4. على مستوى المورد. حدود لكل نقطة نهاية على حدة، حين تكون بعض النقاط أكثر تكلفة بكثير (كنقطة البحث مقارنةً بنقطة الحالة).

إبلاغ العملاء بالحدود

ينبغي للعملاء الذين يتصرفون باحترافية أن يتمكنوا من تقليص معدل طلباتهم ذاتياً. أعِد ترويسات قياسية مع كل استجابة:

X-RateLimit-Limit: 1000 X-RateLimit-Remaining: 743 X-RateLimit-Reset: 1718000400 Retry-After: 30

حين يُرفض طلب عميل (429)، تُخبره ترويسة Retry-After بالضبط متى يُعيد المحاولة. العملاء الذين يحترمون هذه الترويسة لا يُضيفون أي حمل على نظامك خلال نافذة الانتظار.

تحديد المعدل في الأنظمة الموزعة

عند وجود نسخ متعددة من خادم API، فإن عداداً محلياً في ذاكرة كل نسخة يكون غير دقيق — إذ ترى كل نسخة جزءاً فقط من حركة المرور. الحل القياسي هو مخزن مركزي (Redis) حيث تُنقص جميع النسخ من العداد ذاته بشكل ذري (Atomic) باستخدام سكريبت Lua أو نمط INCR/EXPIRE. التكلفة هي رحلة واحدة إلى Redis لكل طلب. للخدمات ذات الإنتاجية العالية جداً، استخدم نهجاً ثنائي المستوى: استهلاك الرموز من Redis على دفعات بمقدار N، مع الاحتفاظ بالباقي في الذاكرة المحلية، والتجديد عند النفاد.

انزياح الساعة في الأنظمة الموزعة. يمكن أن تتصرف خوارزميات النوافذ المنزلقة التي تعتمد على طوابع زمنية دقيقة بشكل غير صحيح إذا انزاحت الساعات بين العقد. استخدم TIME في Redis (جانب الخادم) بدلاً من ساعة خادم التطبيق عند حساب النوافذ. انزياح 100 ميلي ثانية في الساعة يمكن أن يسمح بمرور تدفق متقطع يتجاوز الحد المقصود.

ملخص المقايضات

  • دلو الرموز — يسمح بالتدفقات المتقطعة المتحكّم بها، مرن (تكلفة متعددة الرموز لكل طلب)، مستخدم على نطاق واسع. تعقيد طفيف في الإعداد الموزع.
  • الدلو المتسرب — يضمن خرجاً سلساً، رائع لحماية النظم الخلفية البطيئة. يُضيف زمن استجابة؛ قد يُسقط التدفقات المتقطعة المشروعة.
  • النافذة الثابتة — بالغة البساطة. ثغرة التدفق المتقطع عند الحدود.
  • عداد النافذة المنزلقة — دقيق مع ذاكرة O(1). تقريبي بشكل طفيف.

تحديد المعدل هو أحد أرخص آليات الموثوقية المتاحة: بضعة مفاتيح Redis وسكريبت Lua قصير يكفيان لحماية واجهة API الكاملة من فئة واسعة من أنماط الفشل. ادمجه مع قواطع الدائرة (الدرس القادم) لتحقيق دفاع متعمق.