المرونة والمراسلة والرصد

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

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

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

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

لماذا ينتمي تحديد المعدل إلى أدوات الصمود؟

في الأنظمة الموزعة، تكون الخدمات المصغرة مكشوفة — مباشرةً أو عبر بوابة API — لمتصلين كثيرين في آنٍ واحد. لكل خدمة موارد محدودة: معالج، ذاكرة، اتصالات قاعدة بيانات، وطاقة شبكية صادرة. يفرض تحديد المعدل عقدًا واضحًا: لا يجوز لأي عميل واحد — أو فئة من العملاء — استهلاك أكثر من حصة محددة من هذه الموارد في وحدة الزمن. والفوائد تتجاوز التوفر الخام:

  • العدالة: عميل بطيء أو جشع لا يستطيع تجويع الآخرين.
  • السيطرة على التكلفة: موارد السحابة تتوسع مع حركة المرور؛ الحمل غير المحدود يعني تكلفة غير محدودة.
  • الأمان: هجمات القوة الغاشمة وحشو بيانات الاعتماد والتقشير (scraping) كلها تعتمد على حجم طلبات مرتفع — يجعلها الحد الصارم للمعدل غير عملية.
  • اتفاقيات مستوى الخدمة المتوقعة: حين تعرف أقصى معدل وصول، تستطيع تحجيم بنيتك التحتية بثقة.

خوارزميات تحديد المعدل الشائعة

قبل كتابة الكود يفيد فهم الخوارزميات، لأن سلوكها يختلف اختلافًا ملموسًا في مواجهة حركة المرور المتقطعة:

  • النافذة الثابتة: تعد الطلبات في نافذة تُوافق التقويم (مثلًا 100 طلب في الدقيقة، تُعاد عند :00). بسيطة، لكنها تتيح ضعف الطلبات عند حدود النوافذ — 100 عند 0:59 و100 أخرى عند 1:00.
  • النافذة المنزلقة (سجل أو عداد): تأخذ بعين الاعتبار الـ N ثانية الأخيرة. أكثر سلاسةً وتلغي ارتفاعات الحدود، لكنها تستهلك ذاكرة أكبر.
  • دلو الرموز (Token Bucket): دلو سعته N يمتلئ بمعدل ثابت؛ كل طلب يستهلك رمزًا واحدًا. يتيح ارتفاعات قصيرة طبيعية حتى سعة الدلو ثم يفرض المعدل المتوسط. الخوارزمية الأكثر استخدامًا عمليًا.
  • الدلو المتسرب (Leaky Bucket): تدخل الطلبات طابورًا وتتصرف بمعدل ثابت. تُسوّي الارتفاعات كليًا — مفيدة لحماية الخدمات المنبعية التي لا تستطيع استيعاب التذبذب.
دلو الرموز هو الافتراضي في Resilience4j ومعظم بوابات API. يسمح بارتفاعات قصيرة (تجربة مستخدم جيدة عند إعادة المحاولة) مع فرض المتوسط على المدى البعيد. استخدم الدلو المتسرب فقط حين تحتاج إلى استدعاءات صادرة مقيّسة تمامًا لواجهة برمجية خارجية بحدود صارمة لكل ثانية.

تحديد المعدل مع Resilience4j

يأتي Resilience4j مع وحدة RateLimiter تتكامل بسلاسة مع بقية أوليات الصمود. أضف مُهيِّئ Spring Boot إلى pom.xml:

<dependency> <groupId>io.github.resilience4j</groupId> <artifactId>resilience4j-spring-boot3</artifactId> <version>2.2.0</version> </dependency>

اضبط محدد المعدل في application.yml:

resilience4j: ratelimiter: instances: orderService: limit-for-period: 50 # الرموز المُجدَّدة في كل دورة تحديث limit-refresh-period: 1s # مدة دورة إعادة ملء الدلو timeout-duration: 0ms # مدة انتظار الخيط للحصول على رمز (0 = فشل فوري)

طبّقه على طريقة الخدمة باستخدام تعليق @RateLimiter:

import io.github.resilience4j.ratelimiter.annotation.RateLimiter; import org.springframework.stereotype.Service; @Service public class OrderService { @RateLimiter(name = "orderService", fallbackMethod = "rateLimitFallback") public OrderResponse placeOrder(OrderRequest request) { // المنطق الفعلي للأعمال — كتابة في قاعدة البيانات، استدعاء خارجي، إلخ return orderRepository.save(request.toEntity()).toResponse(); } // تُستدعى تلقائيًا عند تجاوز حد المعدل private OrderResponse rateLimitFallback(OrderRequest request, io.github.resilience4j.ratelimiter.RequestNotPermitted ex) { throw new ResponseStatusException( HttpStatus.TOO_MANY_REQUESTS, "Order rate limit exceeded. Please retry in a moment." ); } }
اضبط دائمًا timeout-duration: 0ms في خدمات REST المتزامنة. تحديد مهلة غير صفرية يجعل الخيط المُستدعي يحجب انتظارًا لرمز. تحت الحمل المستمر يُحجب كل خيط، يمتلئ مجمع الخيوط، وتتوقف الخدمة فعليًا — وهو بالضبط ما كنت تسعى لمنعه. أخفق بسرعة، أعد 429 Too Many Requests، ودع العميل يُراجع توقيت إعادة محاولته.

عرض استجابة 429 صحيحة

تحجز مواصفات HTTP الكود 429 Too Many Requests لاستجابات تحديد المعدل. العملاء الجيدون وبوابات API يفهمون هذا الكود. أضف ترويسة Retry-After حتى يعرف العملاء متى يعيدون المحاولة:

import io.github.resilience4j.ratelimiter.RequestNotPermitted; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; @RestControllerAdvice public class RateLimitExceptionHandler { @ExceptionHandler(RequestNotPermitted.class) public ResponseEntity<ErrorBody> handleRateLimit(RequestNotPermitted ex) { HttpHeaders headers = new HttpHeaders(); headers.set(HttpHeaders.RETRY_AFTER, "1"); // ثوان حتى إعادة ملء الدلو return ResponseEntity .status(HttpStatus.TOO_MANY_REQUESTS) .headers(headers) .body(new ErrorBody("rate_limit_exceeded", "You have exceeded the allowed request rate.")); } }

تحديد المعدل على مستوى المستخدم مقابل المستوى العام

يحمي محدد المعدل العام الخدمة ككل، لكنه لا يمنع مستخدمًا واحدًا من احتكار الميزانية المشتركة. تجمع الأنظمة الإنتاجية عادةً الطبقتين معًا:

  • المحدد العام — يحرس إجمالي إنتاجية الخدمة (مثلًا 10 000 طلب/ث عبر جميع المتصلين). يُوضع عند بوابة API أو موازن الحمل.
  • المحدد لكل مستخدم / لكل مفتاح — يفرض العدالة (مثلًا 100 طلب/ث لكل مفتاح API). يُنفَّذ في الخدمة أو عند البوابة مع مخزن موزع.

إن RateLimiter داخل العملية في Resilience4j لا يُقسَّم حسب المتصل بشكل افتراضي. للتحديد لكل مستخدم تحتاج إلى مخزن عداد مشترك — عادةً Redis — حتى تشترك جميع نسخ الخدمة المُوسَّعة أفقيًا في نفس العداد. Spring Cloud Gateway لديه تحديد معدل مدمج بدعم Redis عبر مرشح RequestRateLimiter:

# application.yml لبوابة Spring Cloud Gateway spring: cloud: gateway: routes: - id: order-service uri: lb://order-service predicates: - Path=/api/orders/** filters: - name: RequestRateLimiter args: redis-rate-limiter.replenishRate: 10 # رموز تُضاف في الثانية redis-rate-limiter.burstCapacity: 20 # أقصى سعة للارتفاع redis-rate-limiter.requestedTokens: 1 key-resolver: "#{@userKeyResolver}" # حبة Spring تستخرج المفتاح
import org.springframework.cloud.gateway.filter.ratelimit.KeyResolver; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import reactor.core.publisher.Mono; @Configuration public class RateLimitConfig { // تقسيم حسب ترويسة "X-User-Id"؛ العودة إلى عنوان IP عند الغياب @Bean public KeyResolver userKeyResolver() { return exchange -> { String userId = exchange.getRequest().getHeaders() .getFirst("X-User-Id"); if (userId != null) return Mono.just(userId); return Mono.just( exchange.getRequest().getRemoteAddress() .getAddress().getHostAddress() ); }; } }
لا تعتمد حصريًا على عنوان IP العميل لتحديد المعدل في الإنتاج. عناوين IP مشتركة (NAT، بروكسيات الشركات) وقابلة للانتحال. استخدم معرفًا مُوثَّقًا — مفتاح API، موضوع JWT، معرف المستخدم — كمفتاح تقسيم أساسي. التحديد المبني على IP طبقة ثانوية مفيدة للنقاط النهائية غير المُوثَّقة (تسجيل الدخول، التسجيل) حيث لا هوية مُوثَّقة بعد.

تحديد المعدل كضابط أمني

تحديد المعدل ليس آلية توفر فحسب — بل هو ضابط أمني من الخط الأول. تأمل هذه الأنماط الهجومية وكيف يُخفف منها تحديد المعدل:

  • حشو بيانات الاعتماد: يُعيد المهاجمون تشغيل ملايين أزواج اسم المستخدم/كلمة المرور المسروقة. حد 5 محاولات فاشلة لكل IP في الدقيقة يجعل هذا غير عملي دون شبكة روبوت ضخمة.
  • إساءة استخدام SMS أو OTP البريد الإلكتروني: إرسال رموز التحقق له تكلفة. حدّد نقاط إرسال الرمز بصرامة (مثلًا 3 في الساعة لكل رقم هاتف).
  • تقشير البيانات: يضرب المنافسون أو الروبوتات نقاط البحث أو الكتالوج بسرعة آلية. تحديد المعدل لكل مستخدم مقرونًا بـ captcha عند تجاوز العتبة يُعيق التقشير الآلي فعليًا.
  • النقاط النهائية كثيفة الموارد: النقاط التي تُطلق حسابات ثقيلة (توليد التقارير، التصدير الجماعي) ينبغي أن تحمل حدًا أكثر إحكامًا من القراءات البسيطة.

قابلية المراقبة: رصد محدد المعدل

محدد معدل لا يمكنك رصده لا يمكنك ضبطه. يكشف Resilience4j تلقائيًا عن مقاييس عبر Micrometer. المقاييس الرئيسية التي ينبغي متابعتها:

  • resilience4j.ratelimiter.available.permissions — عدد الرموز الحالي؛ يهبط إلى الصفر تحت الحمل.
  • resilience4j.ratelimiter.waiting.threads — الخيوط المحجوبة بانتظار رمز (يجب أن تكون صفرًا مع timeout-duration: 0ms).

اضبط تنبيهات حين يبقى available.permissions عند الصفر باستمرار — هذا هو الإشارة إلى أن حدك ضيق جدًا لحركة المرور المشروعة، وتحتاج إما إلى رفع الحد أو تحسين المسار الساخن.

الخلاصة

تحديد المعدل هو العقد بين خدمتك ومتصليها: سقف محدد لمعدل الطلبات يحمي الموارد، ويكفل العدالة، ويُصلّب الخدمة ضد الإساءة. في خدمات Spring Boot، يوفر @RateLimiter من Resilience4j مع timeout-duration: 0ms تحديدًا سريعًا قابلًا للرصد داخل العملية. أما التحديد الموزع لكل مستخدم عبر أسطول مُوسَّع أفقيًا، فـ RequestRateLimiter المدعوم بـ Redis في Spring Cloud Gateway هو الأداة القياسية. أعد دائمًا 429 Too Many Requests مع ترويسة Retry-After، وراقب توفر الرموز في لوحات المتابعة. في الدرس القادم ننتقل من حماية حركة المرور الواردة إلى التصميم للتزامن الزائف مع أنظمة الرسائل.