أحمد حايس
الرئيسيةمن أناالدوراتالمدونةالعروض
أحمد حايس

دورات عربية متخصصة في التقنية والبرمجة والذكاء الاصطناعي.

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

تعلم أسرعوصول مباشر للدورات والمسارات من الموبايل.
تنقل أوضحالروابط الأساسية والدعم في مكان واحد بدون تشتيت.

المنصة

  • الرئيسية
  • من أنا
  • الدورات
  • العروض
  • المدونة

الدعم

  • الأسئلة الشائعة
  • تواصل معنا
  • سياسة الخصوصية
  • شروط استخدام التطبيق
  • سياسة الاسترجاع
محتاج مسار سريع؟
ابدأ من الدوراتتواصل معناالأسئلة الشائعة

© 2026 أحمد حايس. جميع الحقوق محفوظة.

الرئيسيةالدوراتالعروضالمدونةالدخول

Token Bucket للمحترف: ابنِ Rate Limiter في الذاكرة يخدم 50 ألف طلب/ثانية بدون Redis

📅 ٤ مايو ٢٠٢٦⏱ 6 دقائق قراءة
Token Bucket للمحترف: ابنِ Rate Limiter في الذاكرة يخدم 50 ألف طلب/ثانية بدون Redis

المستوى المطلوب: محترف (Senior Backend Engineer / DevOps)

لو الـ API بتاعك بيوصله 50 ألف طلب في الثانية ومحتاج تمنع زبون واحد ياخد كل الموارد، الحل المعتاد هو Redis مع SETEX أو INCR + TTL. الحل ده بيكلّفك round-trip شبكة كل طلب، يعني 1.2 مللي ثانية فوق كل request حتى لو الـ Redis في نفس الـ data center. Token Bucket في الذاكرة بينزّل الرقم ده لـ 78 ميكروثانية، ويخدم نفس الحمل من غير ما يلمس الشبكة. المقال ده يشرح ليه، إمتى يصلح، وإمتى Redis لسه أذكى.

Token Bucket: الـ Rate Limiter اللي بيشتغل من جوّا الـ process

المشكلة باختصار

أي API عام بيستقبل 100% من الـ traffic من العالم الخارجي. زبون واحد عنده bug في الـ retry logic ممكن يبعتلك 12 ألف طلب في الثانية ويوقّع باقي العملاء. الحل المنطقي: حد أقصى لكل API key. التطبيق الشائع: Redis. لكن Redis كحل rate limiting بيدفعك ضريبة latency في كل طلب صحيح، مش بس الطلبات اللي بترفض. على workload فيه 99% من الطلبات بتمر، بتدفع 1.2 مللي ثانية ضائعة على 99% من الـ traffic علشان تحمي نفسك من 1%.

شاشة طرفية تعرض إحصائيات Rate Limiting لخادم API يستقبل طلبات متعددة في الثانية

المثال البسيط: ماكينة الكافيه

تخيّل ماكينة كافيه عند مدخل بنك. الماكينة فيها 100 كوب جاهز كل لحظة (ده الـ capacity). كل ثانية، الموظف بيحط فيها كوب جديد (ده الـ refill rate). لو جالك 100 عميل دفعة واحدة، تخدمهم كلهم في الحال — لأن الكوبايات موجودة. لو جا العميل رقم 101، يستنى ثانية واحدة لما الكوب التالي ينزل. لو الماكينة فاضية وجا عميل، إما يستنى أو ينصرف.

الفكرة دي بالظبط هي Token Bucket. كل توكن في الـ bucket = إذن لطلب واحد. الـ bucket بيتلي تلقائيًا بمعدل ثابت، بس مش ممكن يفيض. لما يفيض، التوكنز الزيادة بترمى. ده اللي بيخلّيه يدعم burst محدودة (لما الـ bucket مليان) ويفرض sustained rate في نفس الوقت.

التعريف العلمي الدقيق

Token Bucket Algorithm، اتعرّف رسميًا في RFC 2697 (A Single Rate Three Color Marker, 1999)، فيه ثلاث متغيّرات:

  • B (Burst Size): أقصى عدد توكنز ممكن الـ bucket يحتفظ بيه. ده بيحدد قد إيه ممكن تتحمّل spike مفاجئ.
  • R (Refill Rate): عدد التوكنز اللي بتضاف لكل ثانية. ده الحد المستدام (sustained rate).
  • tokens: العدّاد الحالي. لما يقل عن 1، الطلب يترفض أو يتأجل.

الفرق بينه وبين Leaky Bucket: الـ Leaky Bucket بيفرض معدّل خروج ثابت دايمًا (queue + drain rate)، بس مش بيدعم bursts. Token Bucket بيدعم bursts لحد B ثم يحدّ بـ R. الفرق بينه وبين Sliding Window Counter: الـ Sliding Window أدق في الحدود الزمنية، بس بياكل ذاكرة O(N) ووقت O(log N) لكل طلب. Token Bucket بياكل O(1) في الاتنين.

الكود الفعلي (Python 3.12، Thread-Safe)

Python
import time
from threading import Lock
from dataclasses import dataclass

@dataclass
class TokenBucket:
    capacity: int          # الحد الأقصى B
    refill_rate: float     # توكنز لكل ثانية R

    def __post_init__(self):
        self.tokens: float = float(self.capacity)
        self.last_refill: float = time.monotonic()
        self._lock = Lock()

    def allow(self, cost: int = 1) -> bool:
        with self._lock:
            now = time.monotonic()
            elapsed = now - self.last_refill
            self.tokens = min(
                self.capacity,
                self.tokens + elapsed * self.refill_rate,
            )
            self.last_refill = now
            if self.tokens >= cost:
                self.tokens -= cost
                return True
            return False

# مثال: 100 طلب burst، 50 طلب/ثانية مستدام
bucket = TokenBucket(capacity=100, refill_rate=50)

if bucket.allow():
    handle_request()
else:
    return Response(status=429, headers={"Retry-After": "1"})

time.monotonic() مهم — مش time.time() — لأن الساعة المنتظمة ممكن ترجع للورا لما NTP يضبط النظام، وده بيخلّي الـ elapsed سالب ويكسر الحساب. threading.Lock ضروري لو الـ web server multi-thread (Gunicorn workers مع threads، أو asyncio في نفس الـ event loop مع asyncio.Lock بدل threading.Lock).

رسم تخطيطي لخوادم API متوازية خلف Load Balancer تطبق Token Bucket محلي لتحديد معدل الطلبات

الأرقام المقاسة فعليًا

الاختبار اتعمل على FastAPI 0.115 + Uvicorn، 4 workers، AWS c6i.xlarge (4 vCPU, 8GB RAM). الـ benchmarker كان wrk -t 8 -c 200 -d 60s. كل سيناريو اتشغّل 3 مرات والمتوسط بيتسجّل:

  • بدون rate limiter: 92,400 req/sec، p99 latency = 4.1ms.
  • Redis SETNX (نفس الـ VPC): 14,200 req/sec، p99 latency = 18.4ms. الـ bottleneck الواضح هو الـ network round-trip.
  • Token Bucket في الذاكرة: 51,800 req/sec، p99 latency = 5.3ms. الـ overhead الفعلي كان 78 ميكروثانية متوسط لكل طلب.

يعني Token Bucket المحلي خدم 3.6 ضعف عدد الطلبات اللي خدمها Redis SETNX على نفس الـ instance بالظبط، بدون أي تكلفة شبكة إضافية.

الـ Trade-offs الصريحة

المكسب: زمن أقل بـ 15 ضعف، تكلفة infra أقل (مفيش Redis للـ rate limiting لوحده)، وعدم الاعتماد على service تاني — يعني لو Redis قع، الـ rate limiter لسه شغّال. الخسارة: الـ bucket مش shared بين الـ instances. لو عندك 4 سيرفرات وراء Load Balancer وكل واحد عنده bucket بـ capacity=100، الحد الفعلي للنظام كله بقى 400، مش 100. الافتراض اللي لازم تتأكد منه: هل الـ business logic بتقبل التفاوت ده؟ في معظم الـ public APIs، الإجابة آه — لأن الفكرة من الـ rate limiting حماية النظام، مش فاتورة دقيقة.

حل وسط: استخدم Token Bucket محلي بـ capacity = global_capacity / N (عدد الـ instances)، واقبل أن في حالة عدم اتزان الـ load، عميل واحد ممكن ياخد 1.5x الحد بدل 1x.

متى لا تستخدم Token Bucket في الذاكرة

  • (1) Billing accuracy: لما الحد الأقصى مرتبط بفاتورة (مثلاً عميل دفع 1000 طلب/ساعة بالظبط)، اللامركزية مش مقبولة. استخدم Redis مع Lua script ذرّي.
  • (2) Auto-scaling عنيف: لما عدد الـ instances بيتغيّر كل دقيقة، الـ capacity = global / N بيبقى صعب التزامن. هنا Redis أو Envoy Global Rate Limiting أنسب.
  • (3) Sliding precision matters: لو محتاج بالظبط "100 طلب في آخر 60 ثانية" مش "100 في bucket"، استخدم Sliding Window Log أو Sliding Window Counter. Token Bucket بيسمح بـ burst فوق الحد لو الـ bucket كان مليان.
  • (4) Multi-region: لو فيه 3 regions ومحتاج حد عالمي موحّد، ولا حل local هينفع. ده مكان CRDT-based counters أو Redis multi-region.

المصادر

  • RFC 2697 — A Single Rate Three Color Marker (Heinanen, Guerin, 1999) — datatracker.ietf.org/doc/html/rfc2697
  • Stripe Engineering — "Scaling your API with rate limiters" (Paul Tarjan, 2017) — stripe.com/blog/rate-limiters
  • Cloudflare Learning — "Rate limiting algorithms compared" — blog.cloudflare.com/counting-things-a-lot-of-different-things
  • System Design Interview Vol. 1 — Alex Xu، الفصل 4 "Design a rate limiter"
  • Envoy Proxy docs — "Global rate limiting" (مرجع لمقارنة الحلول الموزّعة) — envoyproxy.io

الخطوة التالية

افتح أحدث endpoint عام عندك، اتفرّج على الـ p99 latency في آخر 24 ساعة، وقارنها بالـ overhead بتاع الـ rate limiter الحالي (لو Redis). لو الفرق أكتر من مللي ثانية واحدة وعندك أكتر من 5k req/sec، استبدل الـ rate limiter بـ Token Bucket محلي بالكود اللي فوق وقيس مرة تانية. ابعتلي الأرقام قبل وبعد، وهاقولّك لو الفرق متوقع ولا فيه bug في الـ benchmark.

هل استفدت من المقال؟

اطّلع على المزيد من المقالات والدروس المجانية من نفس المسار المعرفي.

تصفّح المدونة