تحديد معدل الطلبات هو كيف تحمي بنية SaaS التحتية من مستأجر واحد يُثقل النظام على الجميع. هذا كيف ننفذ تحديد المعدل لكل مستأجر في تطبيقات Go SaaS باستخدام Redis token buckets.
تحديد معدل الطلبات هو كيف تحمي بنية SaaS التحتية من مستأجر واحد يُثقل النظام على الجميع. الخطأ في التطبيق يعني إما تقييد المستخدمين الشرعيين دون سبب، أو السماح لعميل API مكتوب بشكل سيئ بإسقاط قاعدة بياناتك. هذا كيف ننفذ تحديد معدل الطلبات لكل مستأجر في تطبيقات Go SaaS.
لماذا تحديد معدل الطلبات لكل مستأجر يهم تحديداً؟
في تطبيقات SaaS متعددة المستأجرين، تحديد معدل الطلبات يحمي المستأجرين من بعضهم. مستأجر لديه تكامل مُعدّ بشكل سيئ قد يصدر مئات طلبات API في الثانية في حلقة مضيقة. بدون تحديد المعدل لكل مستأجر، تلك الطلبات تستهلك اتصالات قاعدة البيانات وتُتعب مجموعات العمال وتدهور أوقات استجابة API لكل مستأجر آخر.
بالنسبة لمنصات SaaS التي تخدم عملاء لبنانيين وخليجيين، حيث تكاملات API غالباً ما تُبنى بفرق IT داخلية تحت ضغط الوقت، عملاء API المكتوبون بشكل سيئ هم القاعدة لا الاستثناء.
اختيار الخوارزمية الصحيحة
ثلاث خوارزميات تغطي معظم حالات الاستخدام:
النافذة الثابتة: تحسب الطلبات في نافذة زمنية (مثل 1000 طلب في الدقيقة). بسيطة لكن لديها مشكلة الحدود.
النافذة المنزلقة: أدق من النافذة الثابتة، أغلى قليلاً في التطبيق في النظام الموزع.
دلو الرموز (Token Bucket): يحتفظ بدلو من الرموز تُعبأ بمعدل ثابت. كل طلب يستهلك رمزاً. يسمح بانفجارات قصيرة فوق المتوسط مع تطبيق حد المعدل الطويل.
dلو الرموز هو الخيار الصحيح لمعظم APIs SaaS لأنه يسمح بانفجارات قصيرة مع تطبيق الحد الطويل الأمد.
تطبيق token bucket في Go مع Redis
يجب تطبيق الدلو بطريقة صحيحة عبر نسخ خادم API متعددة. دلو في الذاكرة لكل عملية غير صحيح. Redis مع سكريبت Lua يوفر دلو رموز ذري متسق عبر النسخ:
local key = KEYS[1]
local max_tokens = tonumber(ARGV[1])
local refill_rate = tonumber(ARGV[2])
local now = tonumber(ARGV[4])
local data = redis.call('HMGET', key, 'tokens', 'last_refill')
local tokens = tonumber(data[1]) or max_tokens
local last_refill = tonumber(data[2]) or now
local elapsed = now - last_refill
local new_tokens = math.min(max_tokens, tokens + elapsed * refill_rate)
if new_tokens < 1 then
redis.call('HSET', key, 'tokens', new_tokens, 'last_refill', now)
return {0, math.ceil((1 - new_tokens) / refill_rate * 1000)}
end
new_tokens = new_tokens - 1
redis.call('HSET', key, 'tokens', new_tokens, 'last_refill', now)
return {1, 0}
الـ Go wrapper:
func (rl *RateLimiter) Allow(ctx context.Context, tenantID string, maxTokens, refillRate float64) (bool, time.Duration, error) {
key := fmt.Sprintf("rate_limit:tenant:%s", tenantID)
now := float64(time.Now().UnixNano()) / 1e9
result, err := rl.script.Run(ctx, rl.redis,
[]string{key},
maxTokens, refillRate, 1, now,
).Int64Slice()
if err != nil { return false, 0, err }
return result[0] == 1, time.Duration(result[1]) * time.Millisecond, nil
}
دمج الوسيط
func RateLimitMiddleware(limiter *RateLimiter, limits TenantLimitResolver) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
tenantID := TenantFromContext(r.Context())
maxTokens, refillRate := limits.Resolve(tenantID)
allowed, retryAfter, err := limiter.Allow(r.Context(), tenantID, maxTokens, refillRate)
if err != nil {
next.ServeHTTP(w, r) // فشل مفتوح عند تعطل Redis
return
}
if !allowed {
w.Header().Set("Retry-After", fmt.Sprintf("%.0f", retryAfter.Seconds()))
http.Error(w, "rate limit exceeded", http.StatusTooManyRequests)
return
}
next.ServeHTTP(w, r)
})
}
}
لاحظ سلوك الفشل المفتوح عند عدم توافر Redis. تحديد المعدل مهم لكن لا يجب أن يكون نقطة فشل واحدة.
حدود لكل خطة واستيعاب الانفجارات
خطط اشتراك مختلفة تستحق حدوداً مختلفة:
| الخطة | الانفجار (رموز) | المعدل (طلب/ثانية) | |-------------|-----------------|--------------------| | مجاني | 20 | 2 | | ناشئة | 100 | 10 | | نمو | 500 | 50 | | مؤسسة | 2000 | 200 |
ليست كل حركة مرور الانفجار مسيئة. نظام POS للمطعم الذي يذهب دون اتصال أثناء انقطاع الكهرباء ويعيد الاتصال مع 30 دقيقة من المعاملات المُدرجة في الطابور سيولد حركة انفجار مشروعة ومتوقعة.
بالنسبة لمنصات SaaS في الشرق الأوسط، الانقطاعات الإنترنتية القصيرة التي تعقبها انفجارات عند إعادة الاتصال شائعة بما يكفي لجعل طاقة الانفجار سخية. عميل POS للمطعم الذي لا يستطيع مزامنة معاملاته أثناء انقطاع الكهرباء هو مكالمة دعم وطلب استرداد وخطر إلغاء.
إظهار معلومات تحديد المعدل لمستهلكي API
رؤوس معيارية لتضمينها في كل استجابة:
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 73
X-RateLimit-Reset: 1716000060
Retry-After: 12 (فقط في استجابات 429)
عملاء API ذوو السلوك الجيد يقرؤون هذه الرؤوس ويتراجعون تلقائياً قبل الوصول للحد. عملاء API الذين يتجاهلون هذه الرؤوس ويستمرون حتى يتلقون أخطاء 429 هم بالضبط من يحميك منهم تحديد المعدل.
الدروس من الإنتاج
الفشل المفتوح عند تعطل Redis. تحديد المعدل لا يجب أن يكون نقطة فشل واحدة لـ API الخاص بك.
طاقة الانفجار ميزة، ليست خطراً. عملاء SaaS يحتاجون شرعياً للانفجار فوق معدلهم المتوسط. خوارزمية token bucket تتعامل مع هذا بشكل صحيح.
سجل كل طلب مقيّد مع tenant_id وسياق تحديد المعدل. التقييد إشارة وليس مجرد آلية حماية. المستأجرون الذين يصطدمون بحدودهم باستمرار هم مرشحون لترقية الخطة.
هل تبني API لمنصة SaaS متعددة المستأجرين؟
Voxire يبني تطبيقات SaaS متعددة المستأجرين لفرق تقنية في لبنان والشرق الأوسط، تشمل البنية التحتية لتحديد المعدل وتصميم API والأدوات التشغيلية. تواصل معنا.
https://voxire.com/get-a-quote/



