در فضای سیستمهای توزیعشده مدرن، محافظت از API شما در برابر نوسانات ترافیک و سوءاستفادههای مخرب غیرقابل مذاکره است. اگرچه شمارندههای پنجره ثابت ساده پیادهسازی آسانی دارند، اما اغلب با مشکلات مرزی مواجه میشوند که در آنها درخواستها دقیقاً در زمان بازنشانی پنجره دو برابر میشوند. الگوریتم سطل توکن جایگزین برتری ارائه میدهد و اجازه میدهد ترافیک نوسانی داشته باشید در حالی که نرخ متوسط ثابتی را حفظ میکنید. در این پست، به عمق پیادهسازی یک محدودکننده نرخ با عملکرد بالا و ایمن برای رشتهها در Go میپردازیم.
چرا الگوریتم سطل توکن؟
الگوریتم سطل توکن بر اساس یک اصل ساده کار میکند: توکنها با نرخ ثابت به سطل اضافه میشوند. هر درخواست یک توکن مصرف میکند. اگر سطل خالی باشد، درخواست رد یا به تعویق میافتد. این مدل انعطافپذیر است زیرا اجازه میدهد ترافیک کوتاهمدت تا حداکثر ظرفیت سطل رخ دهد و آن را برای APIهایی که نیاز به مدیریت نوسانات گاهبهگاه دارند بدون به خطر انداختن پایداری بلندمدت، ایدهآل میسازد.
چالشهای پیادهسازی در Go
هنگام پیادهسازی همزمانی در Go، ایمنی رشتهها (Thread Safety) حیاتی است. نمیتوانیم به سادگی از یک عدد صحیح استاندارد برای شمارش توکنها استفاده کنیم؛ ما به اصول همگامسازی برای جلوگیری از شرایط مسابقه نیاز داریم. ما از sync.Mutex برای محافظت از وضعیت خود استفاده خواهیم کرد. با این حال، برای سناریوهای با تراکم بالا، قفلگذاری روی هر درخواست میتواند به یک گلوگاه تبدیل شود. بنابراین، پیادهسازی ما با استفاده از محاسبات زمان کارآمد، تلاش میکند تا رقابت قفل را به حداقل برساند.
پیادهسازی اصلی
در زیر یک پیادهسازی مقاوم از سطل توکن آورده شده است. ما یک ساختار تعریف میکنیم که نرخ (توکن در ثانیه)، ظرفیت (حداکثر توکن) و تعداد فعلی توکنها را نگه میدارد. ما همچنین یک تایمر را برای ردیابی زمان آخرین شارژ مجدد راهاندازی میکنیم.
package ratelimiter
import (
"sync"
"time"
)
// TokenBucket یک محدودکننده نرخ ایمن برای رشتهها را پیادهسازی میکند.
type TokenBucket struct {
mu sync.Mutex
rate float64 // توکن در ثانیه
capacity float64 // حداکثر توکن مجاز
tokens float64 // توکنهای فعلی
lastTime time.Time // زمان آخرین شارژ مجدد
}
// NewTokenBucket یک نمونه محدودکننده نرخ جدید ایجاد میکند.
func NewTokenBucket(rate, capacity float64) *TokenBucket {
return &TokenBucket{
rate: rate,
capacity: capacity,
tokens: capacity, // شروع با ظرفیت کامل
lastTime: time.Now(),
}
}
// Allow بررسی میکند که آیا یک درخواست مجاز است یا خیر. توکنها را بر اساس زمان سپری شده شارژ میکند.
func (tb *TokenBucket) Allow() bool {
tb.mu.Lock()
defer tb.mu.Unlock()
now := time.Now()
elapsed := now.Sub(tb.lastTime).Seconds()
// شارژ توکنها بر اساس زمان سپری شده
tb.tokens += elapsed * tb.rate
if tb.tokens > tb.capacity {
tb.tokens = tb.capacity
}
tb.lastTime = now
// بررسی اینکه آیا توکن کافی داریم یا خیر
if tb.tokens >= 1.0 {
tb.tokens--
return true
}
return false
}
بهینهسازی برای تراکم بالا
پیادهسازی فوق به دلیل وجود mutex ایمن برای رشتهها است. با این حال، در محیطهای با تراکم بسیار بالا، نگه داشتن قفل برای مدت زمان هر درخواست میتواند بر تأخیر تأثیر بگذارد. برای بهینهسازی بیشتر، ممکن است در نظر بگیرید که از atomic.Float64 برای بهروزرسانیهای مسیر غیرحیاتی استفاده کنید، اگرچه رویکرد mutex معمولاً برای بیشتر دروازههای API کافی است. یک تکنیک بهینهسازی دیگر استفاده از رویکرد «پنجره لغزان» ترکیب شده با سطل توکن، یا پیادهسازی یک بافر حلقوی برای زمانهای ثبت درخواستهای اخیر است اگر به تحلیلهای پیچیدهتر در کنار محدودیت نیاز دارید.
مثال استفاده عملی
یکپارچهسازی این مورد در یک وبسرور Gin یا Echo ساده است. در اینجا نحوه استفاده از آن در یک زمینه میانی (Middleware) آورده شده است:
package main
import (
"fmt"
"net/http"
"github.com/gin-gonic/gin"
"yourproject/ratelimiter"
)
func main() {
// اجازه 10 درخواست در ثانیه، با ظرفیت نوسان 20
limiter := ratelimiter.NewTokenBucket(10, 20)
router := gin.Default()
router.GET("/api/resource", func(c *gin.Context) {
if !limiter.Allow() {
c.JSON(http.StatusTooManyRequests, gin.H{
"error": "محدودیت نرخ تجاوز یافته است",
})
return
}
c.JSON(http.StatusOK, gin.H{
"message": "موفقیت",
})
})
router.Run(":8080")
}
نتیجهگیری
پیادهسازی یک محدودکننده نرخ یک مهارت حیاتی برای هر توسعهدهنده بکاند است. با انتخاب الگوریتم سطل توکن، انعطافپذیری لازم برای مدیریت نوسانات ترافیک را کسب میکنید و در عین حال اطمینان حاصل میکنید که سیستم شما تحت بار کاری مقاوم باقی میماند. پیادهسازی Go ارائه شده در اینجا تعادلی میان سادگی استفاده و ایمنی رشتهها ایجاد میکند و به عنوان پایهای محکم برای APIهای سطح تولید عمل میکند. با مقیاسبندی بیشتر، در نظر بگیرید که راهحلهای محدودکننده نرخ توزیعشده مانند الگوریتمهای مبتنی بر Redis را بررسی کنید، اما برای سرویسهای تکنمونه، این رویکرد بومی Go کارآمد و قابل اعتماد است.