فارسی

استراتژی‌های محدودسازی نرخ را با تمرکز بر الگوریتم Token Bucket کاوش کنید. درباره پیاده‌سازی، مزایا، معایب و موارد استفاده عملی آن برای ساخت برنامه‌های پایدار و مقیاس‌پذیر بیاموزید.

محدودسازی نرخ: بررسی عمیق پیاده‌سازی الگوریتم Token Bucket

در چشم‌انداز دیجیتال متصل امروزی، تضمین پایداری و در دسترس بودن برنامه‌ها و API‌ها از اهمیت بالایی برخوردار است. محدودسازی نرخ با کنترل میزانی که کاربران یا کلاینت‌ها می‌توانند درخواست ارسال کنند، نقشی حیاتی در دستیابی به این هدف ایفا می‌کند. این پست وبلاگ به بررسی جامع استراتژی‌های محدودسازی نرخ، با تمرکز ویژه بر الگوریتم Token Bucket، پیاده‌سازی، مزایا و معایب آن می‌پردازد.

محدودسازی نرخ چیست؟

محدودسازی نرخ (Rate limiting) تکنیکی است که برای کنترل حجم ترافیک ارسالی به یک سرور یا سرویس در یک دوره زمانی مشخص استفاده می‌شود. این تکنیک از سیستم‌ها در برابر درخواست‌های بیش از حد محافظت کرده و از حملات منع سرویس (DoS)، سوءاستفاده و افزایش ناگهانی ترافیک جلوگیری می‌کند. با اعمال محدودیت بر تعداد درخواست‌ها، محدودسازی نرخ استفاده منصفانه را تضمین، عملکرد کلی سیستم را بهبود و امنیت را افزایش می‌دهد.

یک پلتفرم تجارت الکترونیک را در طول یک فروش فوق‌العاده تصور کنید. بدون محدودسازی نرخ، افزایش ناگهانی درخواست‌های کاربران می‌تواند سرورها را از کار بیندازد و منجر به زمان پاسخ‌دهی کند یا حتی قطعی سرویس شود. محدودسازی نرخ می‌تواند با محدود کردن تعداد درخواست‌هایی که یک کاربر (یا آدرس IP) می‌تواند در یک بازه زمانی معین ارسال کند، از این امر جلوگیری کرده و تجربه روان‌تری را برای همه کاربران تضمین کند.

چرا محدودسازی نرخ مهم است؟

محدودسازی نرخ مزایای متعددی دارد، از جمله:

الگوریتم‌های رایج محدودسازی نرخ

الگوریتم‌های متعددی می‌توانند برای پیاده‌سازی محدودسازی نرخ استفاده شوند. برخی از رایج‌ترین آن‌ها عبارتند از:

این پست وبلاگ به دلیل انعطاف‌پذیری و کاربرد گسترده، بر الگوریتم Token Bucket تمرکز خواهد کرد.

الگوریتم Token Bucket: توضیحی دقیق

الگوریتم Token Bucket یک تکنیک محدودسازی نرخ پرکاربرد است که تعادلی بین سادگی و کارایی ارائه می‌دهد. این الگوریتم با نگهداری مفهومی یک «سطل» که توکن‌ها را در خود جای داده است، کار می‌کند. هر درخواست ورودی یک توکن از سطل مصرف می‌کند. اگر سطل به اندازه کافی توکن داشته باشد، درخواست مجاز است؛ در غیر این صورت، درخواست رد می‌شود (یا بسته به پیاده‌سازی، در صف قرار می‌گیرد). توکن‌ها با نرخ مشخصی به سطل اضافه می‌شوند و ظرفیت موجود را دوباره پر می‌کنند.

مفاهیم کلیدی

چگونه کار می‌کند

  1. هنگامی که یک درخواست می‌رسد، الگوریتم بررسی می‌کند که آیا توکن کافی در سطل وجود دارد یا خیر.
  2. اگر توکن کافی وجود داشته باشد، درخواست مجاز است و تعداد مربوطه توکن از سطل حذف می‌شود.
  3. اگر توکن کافی وجود نداشته باشد، درخواست یا رد می‌شود (با بازگرداندن خطای «درخواست‌های بیش از حد»، معمولاً HTTP 429) یا برای پردازش بعدی در صف قرار می‌گیرد.
  4. مستقل از ورود درخواست‌ها، توکن‌ها به صورت دوره‌ای با نرخ پر شدن مجدد تعریف‌شده، تا سقف ظرفیت سطل، به سطل اضافه می‌شوند.

مثال

یک Token Bucket با ظرفیت ۱۰ توکن و نرخ پر شدن مجدد ۲ توکن در ثانیه را تصور کنید. در ابتدا، سطل پر است (۱۰ توکن). در اینجا نحوه رفتار الگوریتم ممکن است به این صورت باشد:

پیاده‌سازی الگوریتم Token Bucket

الگوریتم Token Bucket را می‌توان در زبان‌های برنامه‌نویسی مختلف پیاده‌سازی کرد. در اینجا نمونه‌هایی در Golang، Python و Java آورده شده است:

Golang

```go package main import ( "fmt" "sync" "time" ) // TokenBucket represents a token bucket rate limiter. type TokenBucket struct { capacity int tokens int rate time.Duration lastRefill time.Time mu sync.Mutex } // NewTokenBucket creates a new TokenBucket. func NewTokenBucket(capacity int, rate time.Duration) *TokenBucket { return &TokenBucket{ capacity: capacity, tokens: capacity, rate: rate, lastRefill: time.Now(), } } // Allow checks if a request is allowed based on token availability. func (tb *TokenBucket) Allow() bool { tb.mu.Lock() defer tb.mu.Unlock() now := time.Now() tb.refill(now) if tb.tokens > 0 { tb.tokens-- return true } return false } // refill adds tokens to the bucket based on the elapsed time. func (tb *TokenBucket) refill(now time.Time) { elapsed := now.Sub(tb.lastRefill) newTokens := int(elapsed.Seconds() * float64(tb.capacity) / tb.rate.Seconds()) if newTokens > 0 { tb.tokens += newTokens if tb.tokens > tb.capacity { tb.tokens = tb.capacity } tb.lastRefill = now } } func main() { bucket := NewTokenBucket(10, time.Second) for i := 0; i < 15; i++ { if bucket.Allow() { fmt.Printf("Request %d allowed\n", i+1) } else { fmt.Printf("Request %d rate limited\n", i+1) } time.Sleep(100 * time.Millisecond) } } ```

Python

```python import time import threading class TokenBucket: def __init__(self, capacity, refill_rate): self.capacity = capacity self.tokens = capacity self.refill_rate = refill_rate self.last_refill = time.time() self.lock = threading.Lock() def allow(self): with self.lock: self._refill() if self.tokens > 0: self.tokens -= 1 return True return False def _refill(self): now = time.time() elapsed = now - self.last_refill new_tokens = elapsed * self.refill_rate self.tokens = min(self.capacity, self.tokens + new_tokens) self.last_refill = now if __name__ == '__main__': bucket = TokenBucket(capacity=10, refill_rate=2) # 10 tokens, refills 2 per second for i in range(15): if bucket.allow(): print(f"Request {i+1} allowed") else: print(f"Request {i+1} rate limited") time.sleep(0.1) ```

Java

```java import java.util.concurrent.locks.ReentrantLock; import java.util.concurrent.TimeUnit; public class TokenBucket { private final int capacity; private double tokens; private final double refillRate; private long lastRefillTimestamp; private final ReentrantLock lock = new ReentrantLock(); public TokenBucket(int capacity, double refillRate) { this.capacity = capacity; this.tokens = capacity; this.refillRate = refillRate; this.lastRefillTimestamp = System.nanoTime(); } public boolean allow() { try { lock.lock(); refill(); if (tokens >= 1) { tokens -= 1; return true; } else { return false; } } finally { lock.unlock(); } } private void refill() { long now = System.nanoTime(); double elapsedTimeInSeconds = (double) (now - lastRefillTimestamp) / TimeUnit.NANOSECONDS.toNanos(1); double newTokens = elapsedTimeInSeconds * refillRate; tokens = Math.min(capacity, tokens + newTokens); lastRefillTimestamp = now; } public static void main(String[] args) throws InterruptedException { TokenBucket bucket = new TokenBucket(10, 2); // 10 tokens, refills 2 per second for (int i = 0; i < 15; i++) { if (bucket.allow()) { System.out.println("Request " + (i + 1) + " allowed"); } else { System.out.println("Request " + (i + 1) + " rate limited"); } TimeUnit.MILLISECONDS.sleep(100); } } } ```

مزایای الگوریتم Token Bucket

معایب الگوریتم Token Bucket

موارد استفاده از الگوریتم Token Bucket

الگوریتم Token Bucket برای طیف گسترده‌ای از موارد استفاده محدودسازی نرخ مناسب است، از جمله:

پیاده‌سازی Token Bucket در سیستم‌های توزیع‌شده

پیاده‌سازی الگوریتم Token Bucket در یک سیستم توزیع‌شده نیازمند ملاحظات ویژه‌ای برای تضمین سازگاری و جلوگیری از شرایط رقابتی (race conditions) است. در اینجا برخی از رویکردهای رایج آورده شده است:

مثال با استفاده از Redis (مفهومی)

استفاده از Redis برای یک Token Bucket توزیع‌شده شامل بهره‌گیری از عملیات اتمی آن (مانند `INCRBY`, `DECR`, `TTL`, `EXPIRE`) برای مدیریت تعداد توکن‌ها است. جریان اصلی به این صورت خواهد بود:

  1. بررسی وجود سطل: بررسی کنید آیا کلیدی برای کاربر/نقطه پایانی API در Redis وجود دارد یا خیر.
  2. ایجاد در صورت لزوم: اگر وجود ندارد، کلید را ایجاد کنید، تعداد توکن‌ها را برابر با ظرفیت اولیه قرار دهید و یک زمان انقضا (TTL) متناسب با دوره پر شدن مجدد تنظیم کنید.
  3. تلاش برای مصرف توکن: به صورت اتمی تعداد توکن‌ها را کاهش دهید. اگر نتیجه >= ۰ باشد، درخواست مجاز است.
  4. مدیریت اتمام توکن: اگر نتیجه < ۰ باشد، کاهش را برگردانید (به صورت اتمی دوباره افزایش دهید) و درخواست را رد کنید.
  5. منطق پر شدن مجدد: یک فرآیند پس‌زمینه یا یک کار دوره‌ای می‌تواند سطل‌ها را پر کند و توکن‌ها را تا سقف ظرفیت اضافه کند.

ملاحظات مهم برای پیاده‌سازی‌های توزیع‌شده:

جایگزین‌های Token Bucket

در حالی که الگوریتم Token Bucket یک انتخاب محبوب است، تکنیک‌های دیگر محدودسازی نرخ ممکن است بسته به نیازمندی‌های خاص مناسب‌تر باشند. در اینجا مقایسه‌ای با برخی جایگزین‌ها آورده شده است:

انتخاب الگوریتم مناسب:

انتخاب بهترین الگوریتم محدودسازی نرخ به عواملی مانند موارد زیر بستگی دارد:

بهترین شیوه‌ها برای محدودسازی نرخ

پیاده‌سازی مؤثر محدودسازی نرخ نیازمند برنامه‌ریزی و ملاحظات دقیق است. در اینجا برخی از بهترین شیوه‌ها برای دنبال کردن آورده شده است:

نتیجه‌گیری

محدودسازی نرخ یک تکنیک ضروری برای ساخت برنامه‌های پایدار و مقیاس‌پذیر است. الگوریتم Token Bucket یک روش انعطاف‌پذیر و مؤثر برای کنترل نرخی است که کاربران یا کلاینت‌ها می‌توانند درخواست ارسال کنند، که از سیستم‌ها در برابر سوءاستفاده محافظت می‌کند، استفاده منصفانه را تضمین می‌کند و عملکرد کلی را بهبود می‌بخشد. با درک اصول الگوریتم Token Bucket و پیروی از بهترین شیوه‌ها برای پیاده‌سازی، توسعه‌دهندگان می‌توانند سیستم‌های قوی و قابل اعتمادی بسازند که حتی از پس پرتقاضاترین بارهای ترافیکی نیز برآیند.

این پست وبلاگ یک نمای کلی جامع از الگوریتم Token Bucket، پیاده‌سازی، مزایا، معایب و موارد استفاده آن ارائه داد. با بهره‌گیری از این دانش، می‌توانید به طور مؤثر محدودسازی نرخ را در برنامه‌های خود پیاده‌سازی کرده و پایداری و در دسترس بودن سرویس‌های خود را برای کاربران در سراسر جهان تضمین کنید.

محدودسازی نرخ: بررسی عمیق پیاده‌سازی الگوریتم Token Bucket | MLOG