مستأجر واحد يرسل طلبات بشكل مفرط يمكن أن يضر بتجربة جميع المستأجرين الآخرين على النظام. هذه هي بنية تحديد معدل الطلبات لكل مستأجر التي نشغّلها في أنظمة SaaS الإنتاجية المبنية بـ Go.
واجهة برمجة التطبيقات بدون تحديد معدل الطلبات هي واجهة برمجة تطبيقات تنتظر انقطاع الخدمة. في نظام SaaS متعدد المستأجرين، مستأجر واحد يرسل طلبات بشكل مفرط يمكن أن يرفع معدل الطلبات إلى الحد الذي يُضعف تجربة كل المستأجرين الآخرين على النظام. هذه هي بنية تحديد معدل الطلبات لكل مستأجر التي نشغّلها في أنظمة SaaS الإنتاجية في Go.
لماذا التحديد لكل مستأجر أهم من التحديد العالمي؟
تحديد المعدل العالمي يرفض جميع الطلبات التي تتجاوز حدًا إجمالياً للنظام. يحمي هذا الخادم من الحمل الزائد لكنه يخلق مشكلة العدالة: مستأجر واحد يُرسل طلبات بشكل مكثف يمكن أن يستنزف الحصة العالمية ويُطلق أخطاء 429 لكل المستأجرين الآخرين على المنصة.
تحديد المعدل لكل مستأجر يمنح كل مستأجر دلو حصة مستقل خاص به. مستأجر يُرسل 500 طلباً في الدقيقة مقابل حد 100 طلب في الدقيقة يتعرض للتقييد عند حده الخاص. كل مستأجر آخر يستمر في العمل بشكل طبيعي دون أن يتأثر بسلوك جاره.
في SaaS منطقة الشرق الأوسط مع مزيج متنوع من العملاء، يستحيل ضبط حد عالمي واحد بشكل صحيح. المستأجرون من الشركات الكبرى قد يُرسلون مئات استدعاءات API في الدقيقة كجزء من العمليات العادية. مستأجرو الأعمال الصغيرة يُرسلون طلبات بأرقام منخفضة في الدقيقة. حد يناسب أحدهم يكسر الآخر.
خوارزمية Token Bucket في Go
خوارزمية token bucket هي الآلية القياسية لتحديد معدل الطلبات. كل مستأجر لديه دلو بسعة رموز قصوى ومعدل إعادة تعبئة. كل طلب يستهلك رمزاً واحداً. عندما يفرغ الدلو، يُرفض الطلب التالي بـ 429 Too Many Requests. تُعاد تعبئة الرموز بمعدل ثابت بمرور الوقت.
مكتبة Go الممتدة توفر golang.org/x/time/rate التي تُنفّذ token bucket للإنتاج:
import "golang.org/x/time/rate"
limiter := rate.NewLimiter(rate.Every(time.Minute/100), 100)
if !limiter.Allow() {
http.Error(w, "rate limit exceeded", http.StatusTooManyRequests)
return
}
مخزن الـ Limiter في العملية مع الإزالة
لخادم Go ذو نسخة واحدة، خزّن محددات المعدل لكل مستأجر في خريطة ذاكرة محمية بـ sync.RWMutex. تحتاج الخريطة إلى آلية تنظيف لإزالة الإدخالات القديمة للمستأجرين الذين لم يُرسلوا طلبات مؤخراً.
ابدأ goroutine التنظيف مرة واحدة عند تهيئة الخادم. على نظام يضم 500 مستأجر نشط مع إزالة بعد 15 دقيقة، تظل الخريطة صغيرة بما يكفي لألا يكون ضغط الذاكرة مشكلة.
تحديد المعدل بـ Redis لبيئات ECS متعددة النسخ
مخزن الـ limiter في العملية يفشل في بيئات النشر متعددة النسخ. إذا كانت ثلاث نسخ من مهام ECS تحتفظ كل منها بخريطة محددات معدل خاصة بها، يمكن لمستأجر بحد 100 طلب في الدقيقة إرسال 100 طلب في الدقيقة لكل نسخة ليصل إجمالياً إلى 300 طلب في الدقيقة.
لبيئات ECS ذات أكثر من نسخة واحدة، انقل عداد تحديد المعدل إلى Redis. استخدم INCR الذرية مع انتهاء الصلاحية على مفتاح per-tenant-per-minute:
func (r *RedisLimiter) Allow(ctx context.Context, tenantID string, limit int64) (bool, error) {
minuteKey := fmt.Sprintf("rl:%s:%d", tenantID, time.Now().Unix()/60)
count, err := r.rdb.Incr(ctx, minuteKey).Result()
if err != nil {
return true, fmt.Errorf("redis rate limit check failed: %w", err)
}
if count == 1 {
r.rdb.Expire(ctx, minuteKey, 120*time.Second)
}
return count <= limit, nil
}
السلوك fail-open عند أخطاء Redis مقصود: رفض كل حركة API لأن خدمة تحديد المعدل غير متاحة سيُسبب انقطاع خدمة أسوأ مما صُمم تحديد المعدل لمنعه.
رؤوس HTTP القياسية لتحديد المعدل
أرجع رؤوس حالة تحديد المعدل في كل استجابة API، ليس فقط في استجابات 429. يستخدمها العملاء لمراقبة استخدامهم الخاص وتنفيذ منطق التراجع.
الرؤوس القياسية:
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 47
X-RateLimit-Reset: 1748358060
Retry-After: 23
تخزين الحدود في PostgreSQL وتخزينها مؤقتاً في Redis
حدود معدل الطلبات هي خاصية تكوين المستأجر، وليست ثابتة مُبرمجة. خزّنها في PostgreSQL إلى جانب إعدادات خطة المستأجر وخزّن الحد المُحلَّل في Redis مع TTL لمدة 5 دقائق.
CREATE TABLE tenant_rate_limits (
tenant_id UUID PRIMARY KEY REFERENCES tenants(id),
requests_per_minute INTEGER NOT NULL DEFAULT 60,
burst_size INTEGER NOT NULL DEFAULT 120,
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
عندما يُرقّي مستأجر من الخطة القياسية إلى Pro، تحديث قيمة requests_per_minute في هذا الجدول يُصبح ساري المفعول خلال نافذة TTL التالية للـ cache (5 دقائق) بدون أي إعادة تشغيل للخادم أو نشر.
الدروس الأساسية من الإنتاج
تحديد المعدل لكل مستأجر آلية عدالة قبل أن يكون آلية حماية سعة. محددات المعدل في العملية تفشل بصمت في بيئات ECS متعددة النسخ: انقل حالة العداد إلى Redis. دائماً fail-open عند عدم توفر Redis. أرجع رؤوس تحديد المعدل في كل استجابة. خزّن تكوين الحد في PostgreSQL وخزّنه مؤقتاً في Redis حتى تصبح التغييرات سارية المفعول بدون نشر.


