عندما يصرخ API الخاص بك "SOS": التعرف على تحديد المعدل (Rate Limiting) والتحكم في التدفق (Throttling)
\n\nتخيل: لقد كتبت تطبيق Kotlin مثاليًا. إنه سريع، أنيق، ويستخدم الكوروتينات و Flow. قمت بنشره في الإنتاج، وفجأة! مستخدم "لطيف" واحد (أو الأسوأ من ذلك، مهاجم) يبدأ في قصف نقطة النهاية الخاصة بك بآلاف الطلبات في الثانية. الخادم ينهار، قاعدة البيانات تبكي، وبقية المستخدمين يتلقون 503 Service Unavailable.
ألم مألوف؟ لهذا السبب تحديدًا توجد آليات تحديد المعدل (rate limiting) و التحكم في التدفق (throttling). في هذه المقالة، لن نتحدث فقط عن النظرية، بل سنشرح من خلال أمثلة عملية كيفية تطبيق هذه الأنماط في Kotlin.
\n\nسنكتب كودًا يحمي حقًا الواجهة الخلفية الخاصة بك، سواء كان تطبيقًا على Ktor أو Spring Boot أو كوروتين خالص. هيا بنا!
\n\nتحديد المعدل (Rate Limiting) مقابل التحكم في التدفق (Throttling): ما الفرق (ولماذا هو مهم)؟
\n\nلنبدأ بالأساسيات لتجنب الخلط بين المصطلحات. على الرغم من استخدام هذه المفاهيم غالبًا كمرادفات، إلا أن هناك فارقًا دقيقًا بينهما.
\n\n- \n
- تحديد المعدل (Rate Limiting) — هو حد صارم. "يمكنك فقط إجراء 100 طلب في الدقيقة. بعد الطلب 101، ستحصل على
429 Too Many Requestsوحظر حتى الدقيقة التالية". هذا يتعلق بـ الحد. \n - التحكم في التدفق (Throttling) — هو تنظيم أكثر ليونة. "لا يمكنك إرسال الطلبات أسرع من مرة واحدة كل 200 مللي ثانية. إذا أرسلت أسرع، فسنضعها في قائمة انتظار أو نتجاهلها ببساطة". هذا يتعلق بـ السرعة. \n
في عالم Kotlin، غالبًا ما ننفذ تحديد المعدل على مستوى API (middleware) والتحكم في التدفق على مستوى العمليات المحددة (على سبيل المثال، لتجنب إغراق خادم خارجي بالطلبات).
\n\nالخوارزميات التي يجب أن يعرفها كل مطور Kotlin
\n\nقبل كتابة الكود، دعنا نختار الأداة. هناك ثلاث خوارزميات كلاسيكية تتناسب تمامًا مع بنية Kotlin.
\n\n1. Token Bucket (دلو الرموز) — الأكثر شيوعًا
\nتخيل دلوًا، يتم إسقاط N من الرموز الجديدة فيه كل ثانية. للدلو سعة قصوى (burst). لمعالجة طلب، تحتاج إلى أخذ رمز. إذا لم تكن هناك رموز، يتم رفض الطلب.
\nالمزايا: يسمح باندفاعات صغيرة من الحمل (bursts)، سهل التنفيذ.
\n\n2. Leaky Bucket (الدلو المثقوب) — تنعيم القمم
\nدلو به ثقب. تدخل الطلبات إلى الدلو، ويخرجها "الثقب" بسرعة ثابتة. إذا امتلأ الدلو، يتم تجاهل الطلبات.
\nالمزايا: يوازن الحمل بشكل مثالي، ولا يسمح بوجود قمم.
\n\n3. Sliding Window (النافذة المنزلقة) — الدقة بدون هزات
\nبدلاً من الحدود الصارمة (كما في "دقيقة واحدة")، ننظر إلى فترة زمنية منزلقة. أكثر تعقيدًا في التنفيذ، لكنه يعكس الحمل الحقيقي بدقة أكبر.
\n\nلمشاريع Kotlin، Token Bucket هو الوسط الذهبي بين البساطة والكفاءة. هذا ما سنقوم بتنفيذه.
\n\nكتابة الكود: محدد معدل (Rate Limiter) في Kotlin من الصفر (بدون مكتبات)
\n\nدعنا ننشئ محدد معدل بسيطًا ولكنه عملي، باستخدام الكوروتينات و Mutex. ستكون هذه نسخة آمنة للخيوط من Token Bucket.
import kotlinx.coroutines.sync.Mutex\nimport kotlinx.coroutines.sync.withLock\nimport kotlin.time.Duration\nimport kotlin.time.TimeSource\n\nclass TokenBucketRateLimiter(\n private val maxTokens: Int,\n private val refillRate: Long, // رمز في الثانية\n private val timeSource: TimeSource = TimeSource.Monotonic\n) {\n private var tokens: Long = maxTokens.toLong()\n private var lastRefillTimestamp: Long = timeSource.markNow().elapsedNow().inWholeMilliseconds\n private val mutex = Mutex()\n\n suspend fun tryAcquire(): Boolean {\n mutex.withLock {\n refillTokens()\n return if (tokens > 0) {\n tokens--\n true\n } else {\n