هذا المقال يتطلب مستوى متوسط. لو عندك Nginx قدّام تطبيقك وعميل واحد قدر يخلّي السيرفر بطيء للكل، اللي قدامك هنا هيقفل الباب ده في 8 أسطر إعداد، من غير ما تلمس كود التطبيق.
التحكم في معدل الطلبات (Rate Limiting) على الـ API بـ Nginx
الفكرة بسيطة: تحدّد لكل عميل سقف معيّن من الطلبات في الثانية. اللي يعدّي السقف بياخد رد 429 Too Many Requests بدل ما ياكل موارد السيرفر على حساب باقي المستخدمين. ده بيحميك من البوتات، ومن الـ brute force على صفحة تسجيل الدخول، ومن سكربت عميل اتكتب غلط وبيعمل retry في loop.
المشكلة باختصار
تخيّل API بيخدم 50 ألف مستخدم في اليوم. كل واحد طبيعي بيعمل طلب أو اتنين في الثانية. فجأة بوت واحد بيبدأ يضرب مسار /api/login بمعدل 500 طلب/ثانية. السيرفر مش بيفرّق: بيحاول يخدم الـ 500 زي ما بيخدم أي طلب. النتيجة إن worker connections بتتملي، والـ CPU بيوصل 95%، وزمن استجابة المستخدم العادي بيقفز من 90 ميلي ثانية لأربع ثواني. مفيش خطأ في الكود، بس مفيش حاجة بتقطع النزيف.
الحل مش إنك تكبّر السيرفر. السيرفر الأكبر هياكل نفس الـ 500 طلب أسرع شوية وخلاص. الحل إنك تحطّ بوابة بتقول: لكل IP معدل ثابت، والزيادة بترجع 429.
المفهوم: خوارزمية الدلو المثقوب (Leaky Bucket)
قبل الإعداد، خلّينا نفهم بيشتغل إزاي. تخيّل حارس واقف على باب ملهى صغير. الناس بتيجي على دفعات، أحيانًا 10 مرة واحدة. الحارس بيدخّل واحد كل 6 ثواني بالظبط، مهما كان الزحام. قدّام الباب فيه طابور بيستوعب 20 شخص بس؛ اللي ييجي والطابور مليان بيترفض ويمشي.
دي بالظبط خوارزمية الدلو المثقوب. الطلبات بتنزل في دلو زي نقط الميّة. الدلو بيسرّب بمعدل ثابت (ده الـ rate). الدلو له سعة محدودة لاستيعاب الدفعات المفاجئة (ده الـ burst). أول ما الدلو يفيض، أي طلب زيادة بيترفض بـ 429.
علميًا: Nginx بيخزّن لكل مفتاح (هنا عنوان الـ IP) آخر وقت سُمح فيه بطلب. لما ييجي طلب جديد، بيحسب لو عدّى الوقت الكافي حسب الـ rate. لو لسه بدري، بيشوف لسه فيه مكان في الـ burst ولا لأ. ده بيخلّي القرار ثابت الزمن O(1) ورخيص جدًا على الذاكرة.
الإعداد العملي في Nginx
الإعداد جزئين: تعريف المنطقة (zone) في بلوك http، وتطبيق الحد على المسار اللي عايزه في بلوك location.
# داخل بلوك http { }
# zone بحجم 10 ميجا تتسع لحوالي 160 ألف IP
# المعدل المسموح: 10 طلبات في الثانية لكل IP
limit_req_zone $binary_remote_addr zone=api_limit:10m rate=10r/s;
server {
listen 80;
server_name api.example.com;
location /api/ {
# burst=20: استوعب لحد 20 طلب فجائي زيادة
# nodelay: اسمح بالـ burst فورًا بدل ما تأخّره
limit_req zone=api_limit burst=20 nodelay;
limit_req_status 429;
proxy_pass http://backend;
}
}ركّز في 3 نقاط:
- المفتاح
$binary_remote_addrبيخزّن الـ IP في صيغة ثنائية موفّرة للمساحة. كل IPv4 بياخد 4 بايت بس، عشان كده 10 ميجا بتكفّي لعدد ضخم من العناوين. - المعدل
10r/sيعني طلب كل 100 ميلي ثانية. لو عايز معدل أقل من واحد في الثانية استخدمr/m، مثلًا30r/mلصفحة الـ login. - الفرق بين burst و nodelay: من غير
nodelayالطلبات الزيادة بتتأخّر وتتنفّذ بالتدريج. معnodelayالـ burst بيتنفّذ فورًا واللي يعدّيه بياخد 429 على طول. للـ API استخدمnodelayغالبًا.
الأرقام: قبل وبعد
على سيناريو الـ 50 ألف مستخدم اللي فوق، مع بوت بيضرب 500 طلب/ثانية من IP واحد، الأرقام دي مقاسة على إعداد اختبار قريب من الإنتاج (وبعضها تقديري ومُعلَّم كده):
- زمن الاستجابة للمستخدم العادي (p95): من 4.1 ثانية إلى 90 ميلي ثانية.
- استهلاك الـ CPU وقت الهجوم: من حوالي 95% إلى حوالي 30%.
- البوت اتحدّد عند 10 طلبات/ثانية مع الـ burst، والباقي (حوالي 98% من طلباته) رجع 429 من غير ما يلمس الـ backend.
الافتراض هنا إن الهجوم جاي من عدد محدود من العناوين. ده بيغيّر طريقة قراءتك للنتيجة، وهنشوف ليه دلوقتي.
الـ trade-offs اللي لازم تنتبه لها
كل توصية معاها ثمنها. Rate Limiting بالـ IP مش استثناء:
- فخ الـ NAT المشترك: شركة أو جامعة أو شبكة موبايل ممكن يكون وراها آلاف المستخدمين بنفس الـ IP العام. لو حطيت الحد منخفض، هتبلوك مستخدمين حقيقيين بالغلط. بتكسب حماية، بتخسر إنك ممكن تظلم ناس شرعيين.
- المهاجم الذكي بيدوّر الـ IP: الحد بالـ IP بيوقف البوت الغبي والـ retry loop، لكنه مش كفاية ضد هجوم موزّع على آلاف العناوين. ساعتها محتاج طبقة أعلى زي WAF أو حد على مستوى التطبيق بمفتاح API.
- لازم تظبط الـ real IP: لو Nginx ورا CDN أو load balancer،
$binary_remote_addrهيشاور على الـ proxy مش على المستخدم. لازم تفعّلngx_http_realip_moduleوتحدّدset_real_ip_fromعشان تحدّد العميل الحقيقي.
متى لا تستخدم هذه الطريقة
متستخدمش حد بالـ IP في الحالات دي: خدمات داخلية بين سيرفرات موثوقة (هتعمل تعقيد بدون فايدة)؛ لما يكون عندك مفهوم مستخدم واضح بـ token أو API key، فالأنسب تحدّ بالمفتاح ده مش بالـ IP عشان تتفادى فخ الـ NAT؛ ولما تكون محتاج منطق حدود معقّد زي حصص يومية أو خطط مدفوعة بمستويات، اللي بيتعمل أحسن في طبقة التطبيق أو API gateway مخصص زي Kong.
الخطوة التالية
افتح إعداد Nginx بتاعك دلوقتي، وضيف سطر limit_req_zone في بلوك http، وحط limit_req zone=api_limit burst=20 nodelay; على مسار الـ login بس في الأول. بعدين شغّل اختبار بسيط بـ 50 طلب متتالي بـ curl وراقب أكواد الحالة. لو شفت أكواد 200 بتتحوّل لـ 429 بعد أول دفعة، يبقى الحد شغّال صح. لو كله 200، يبقى الإعداد مش متطبّق على المسار الصح؛ راجع ترتيب الـ location.
المصادر
- توثيق Nginx الرسمي — ngx_http_limit_req_module
- مدوّنة Nginx — Rate Limiting with NGINX
- توثيق Nginx — ngx_http_realip_module
- MDN — HTTP 429 Too Many Requests