Rate Limiting Stratejileri: Token Bucket, Leaky Bucket ve Sliding Window ile API'ları Korumak
API'larınızı kötü niyetli saldırılardan, hatalı istemcilerden veya aşırı kullanımdan (overuse) korumak, hizmet kalitesi (QoS) ve maliyet kontrolü için hayati önem taşır. Rate Limiting (Hız Sınırlama), belirli bir zaman aralığında bir istemcinin (IP, API Key, Kullanıcı) yapabileceği istek sayısını sınırlayan bir savunma mekanizmasıdır. Bu yazıda, en yaygın 4 rate limiting algoritmasını (Token Bucket, Leaky Bucket, Fixed Window, Sliding Window) ve bunların .NET'te nasıl uygulanacağını ele alıyoruz.
1. Neden Rate Limiting? Temel Kavramlar
-
Kötüye Kullanımı Önleme (Abuse Prevention): Brute-force saldırıları (şifre deneme), DDoS saldırıları veya hatalı bot trafiğini engeller.
-
Kaynak Adaleti (Resource Fairness): Bir istemcinin tüm kaynakları (veritabanı bağlantısı, thread, bant genişliği) tüketmesini engeller.
-
Maliyet Kontrolü: Üçüncü parti API'ler (ör. OpenAI, Google Maps) genellikle istek başına ücretlendirilir. Rate limiting, maliyetin kontrolden çıkmasını önler.
-
Ölçeklenebilirlik (Scalability): Sistemi maksimum kapasitede çalıştırarak, ani trafik patlamalarına karşı dayanıklılık sağlar.
Rate limiting, genellikle API Gateway (YARP/Ocelot) veya uygulama katmanında (Middleware) uygulanır.
2. Rate Limiting Algoritmaları
A. Token Bucket (Token Kovası)
Çalışma Mantığı:
-
Sabit bir kapasitede (bucket) token (jeton) bulunur.
-
Belirli bir hızda (rate) token'lar kovaya eklenir (ör. saniyede 10 token).
-
Bir istek geldiğinde, kovadan bir token tüketilir.
-
Eğer kovada token yoksa, istek reddedilir (429 Too Many Requests).
Artıları:
-
Esneklik: Ani trafik patlamalarına (burst) izin verir (çünkü kova dolduğunda biriken token'lar kullanılabilir).
-
Basit Uygulama: Hafif ve hızlıdır.
Eksileri:
-
Tutarlılık Zorluğu: Dağıtık sistemlerde token durumunu paylaşmak (Redis ile) ek maliyet getirir.
Uygulama Örneği (Basit In-Memory):
csharp
public class TokenBucket
{
private readonly int _capacity;
private readonly double _tokensPerSecond;
private double _tokens;
private DateTime _lastRefill;
public TokenBucket(int capacity, double tokensPerSecond)
{
_capacity = capacity;
_tokensPerSecond = tokensPerSecond;
_tokens = capacity;
_lastRefill = DateTime.UtcNow;
}
public bool TryConsume()
{
RefillTokens();
if (_tokens >= 1)
{
_tokens -= 1;
return true;
}
return false;
}
private void RefillTokens()
{
var now = DateTime.UtcNow;
var delta = (now - _lastRefill).TotalSeconds;
_tokens = Math.Min(_capacity, _tokens + delta * _tokensPerSecond);
_lastRefill = now;
}
}
Kullanım Senaryosu: Genel API limitleri, kullanıcı başına istek sınırı.
B. Leaky Bucket (Sızdıran Kova)
Çalışma Mantığı:
-
Sabit kapasiteli bir kova vardır.
-
Gelen istekler kovaya eklenir (kuyruğa alınır).
-
Kovadan sabit bir hızla (rate) istekler çıkar ve işlenir (sızdırır).
-
Kova dolduğunda gelen istekler reddedilir.
Artıları:
-
Düzgün Trafik (Smoothing): Ani patlamaları yumuşatır, trafik akışını sabit hale getirir.
-
Öngörülebilir: İşlem hızı sabit olduğu için sistem yükü öngörülebilirdir.
Eksileri:
-
Burst Desteği Yok: Ani trafik artışlarında istekler reddedilir (Token Bucket'a göre dezavantaj).
-
Kuyruk Yönetimi: Kuyruk bellek tüketir ve yönetimi karmaşıktır.
Uygulama Örneği (ConcurrentQueue ile):
csharp
public class LeakyBucket
{
private readonly int _capacity;
private readonly TimeSpan _leakInterval;
private readonly Queue<DateTime> _queue;
private readonly Timer _timer;
public LeakyBucket(int capacity, TimeSpan leakInterval)
{
_capacity = capacity;
_leakInterval = leakInterval;
_queue = new Queue<DateTime>();
_timer = new Timer(Leak, null, TimeSpan.Zero, leakInterval);
}
public bool TryEnqueue()
{
lock (_queue)
{
if (_queue.Count < _capacity)
{
_queue.Enqueue(DateTime.UtcNow);
return true;
}
return false;
}
}
private void Leak(object state)
{
lock (_queue)
{
if (_queue.Count > 0)
_queue.Dequeue();
}
}
}
Kullanım Senaryosu: Veritabanı yazma işlemleri, batch işlemleri, sürekli veri akışı (data streaming).
C. Fixed Window Counter (Sabit Pencere Sayacı)
Çalışma Mantığı:
-
Zaman, sabit aralıklara (ör. 1 dakika) bölünür.
-
Her aralık için bir sayaç tutulur.
-
Bir istek geldiğinde, içinde bulunduğu aralığın sayacı artırılır.
-
Sayaç limite ulaştıysa istek reddedilir.
Artıları:
-
Çok Basit: Uygulaması ve anlaşılması en kolay algoritmadır.
-
Hafif: Çok az bellek ve CPU kullanır.
Eksileri:
-
Sınır Sorunu (Boundary Issue): Pencere sınırlarında, istemci limitin iki katı kadar istek gönderebilir (ör. 59. saniye ve 61. saniyede 100'er istek). Bu, ani patlamalara neden olur.
Uygulama Örneği (In-Memory):
csharp
public class FixedWindowCounter
{
private readonly int _limit;
private readonly TimeSpan _window;
private int _count;
private DateTime _windowStart;
public FixedWindowCounter(int limit, TimeSpan window)
{
_limit = limit;
_window = window;
_windowStart = DateTime.UtcNow;
}
public bool TryConsume()
{
var now = DateTime.UtcNow;
if (now - _windowStart >= _window)
{
// Yeni pencere
_count = 0;
_windowStart = now;
}
if (_count < _limit)
{
_count++;
return true;
}
return false;
}
}
Kullanım Senaryosu: Basit, düşük trafikli API'ler, admin panelleri.
D. Sliding Window (Kayan Pencere)
Çalışma Mantığı:
-
Sabit pencere sayacının sınır sorununu çözmek için, pencere zaman içinde kayan (sliding) bir yapıya sahiptir.
-
İsteklerin zaman damgaları (timestamp) veya daha hassas bir sayaç (ör. Redis Sorted Set) ile takip edilir.
-
Her istek geldiğinde, mevcut andan (now) itibaren geriye doğru pencere uzunluğu kadar olan istekler sayılır.
-
Sayaç limite ulaştıysa istek reddedilir.
Artıları:
-
Daha Adil: Sınır sorununu ortadan kaldırır.
-
Hassas: Trafik akışını daha doğru yansıtır.
Eksileri:
-
Daha Fazla Bellek: Her isteğin zaman damgasını saklamak gerekebilir (dağıtık sistemlerde Redis Sorted Set ile).
-
Karmaşıklık: Uygulaması daha zordur.
Uygulama Örneği (Redis Sorted Set ile):
csharp
// Redis'te Sorted Set kullanımı (IDistributedCache ile)
public async Task<bool> IsAllowedAsync(string key, int limit, TimeSpan window)
{
var now = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
var windowStart = now - (long)window.TotalSeconds;
var redis = GetRedisDatabase(); // StackExchange.Redis
var sortedSetKey = $"ratelimit:{key}";
// 1. Pencere dışındaki eski istekleri temizle
await redis.SortedSetRemoveRangeByScoreAsync(sortedSetKey, 0, windowStart);
// 2. Mevcut penceredeki istek sayısını al
var count = await redis.SortedSetLengthAsync(sortedSetKey);
// 3. Limit kontrolü
if (count < limit)
{
// 4. Yeni isteği ekle (score = timestamp)
await redis.SortedSetAddAsync(sortedSetKey, Guid.NewGuid().ToString(), now);
await redis.KeyExpireAsync(sortedSetKey, window); // TTL
return true;
}
return false;
}
Kullanım Senaryosu: Yüksek trafikli, adil olması gereken API'ler (finans, kritik işlemler).
3. .NET'te Rate Limiting Middleware (Yerleşik Çözüm)
.NET 7 ve üzeri, System.Threading.RateLimiting namespace'i altında yerleşik rate limiting desteği sunar. Bu, yukarıdaki algoritmaların çoğunu zaten implemente eder.
csharp
// Program.cs
using System.Threading.RateLimiting;
var builder = WebApplication.CreateBuilder(args);
// Token Bucket (FixedWindowLimiter)
builder.Services.AddRateLimiter(options =>
{
options.AddFixedWindowLimiter("Fixed", opt =>
{
opt.PermitLimit = 100; // 100 istek
opt.Window = TimeSpan.FromMinutes(1);
opt.QueueProcessingOrder = QueueProcessingOrder.OldestFirst;
opt.QueueLimit = 0; // Kuyruk yok
});
options.AddSlidingWindowLimiter("Sliding", opt =>
{
opt.PermitLimit = 100;
opt.Window = TimeSpan.FromMinutes(1);
opt.SegmentsPerWindow = 10; // 10 segment (6 saniyelik dilimler)
});
options.AddTokenBucketLimiter("TokenBucket", opt =>
{
opt.TokenLimit = 100;
opt.QueueLimit = 10;
opt.ReplenishmentPeriod = TimeSpan.FromSeconds(10);
opt.TokensPerPeriod = 20;
});
// Global veya endpoint bazında uygulama
options.GlobalLimiter = PartitionedRateLimiter.Create<HttpContext, string>(
httpContext => RateLimitPartition.GetFixedWindowLimiter(
partitionKey: httpContext.User.Identity?.Name ?? httpContext.Connection.RemoteIpAddress?.ToString(),
factory: partition => new FixedWindowRateLimiterOptions
{
PermitLimit = 100,
Window = TimeSpan.FromMinutes(1)
}
));
});
var app = builder.Build();
app.UseRateLimiter(); // Middleware'i ekle
app.MapGet("/api/data", () => "Hello, World!")
.RequireRateLimiting("Fixed"); // Belirli bir policy kullan
4. Algoritma Karşılaştırma Tablosu
| Algoritma | Burst (Ani Yük) Desteği | Adalet (Fairness) | Uygulama Karmaşıklığı | Bellek Maliyeti | Kullanım Senaryosu |
|---|---|---|---|---|---|
| Token Bucket | ✅ Yüksek | 🟡 Orta | 🟢 Düşük | 🟢 Düşük | Genel API, kullanıcı başına limit |
| Leaky Bucket | ❌ Düşük | ✅ Yüksek | 🟡 Orta | 🔴 Yüksek (kuyruk) | Veritabanı yazma, batch işlemler |
| Fixed Window | ✅ Orta (sınır sorunu) | 🟡 Orta | 🟢 Çok Düşük | 🟢 Düşük | Basit, düşük trafikli API'ler |
| Sliding Window | ✅ Orta | ✅ Yüksek | 🔴 Yüksek | 🔴 Orta-Yüksek | Yüksek trafik, hassas limit ihtiyacı |
5. Dağıtık Rate Limiting (Redis ile)
Tek sunuculu uygulamalarda in-memory rate limiting yeterlidir. Ancak birden fazla sunucu (Web Farm, Kubernetes) varsa, istemciler farklı sunuculara istek gönderebilir ve toplam limit aşılabilir. Bu durumda, rate limiter state'ini dağıtık bir cache'de (Redis) saklamak gerekir.
-
Redis Sorted Set: Yukarıdaki sliding window örneğinde olduğu gibi, her isteği bir sorted set'e ekleyip penceredeki istek sayısını hesaplamak.
-
Redis Lua Script: Atomik işlem için Lua script kullanarak hem okuma hem yazma yapmak (race condition önleme).
-
Dağıtık Rate Limiting Kütüphaneleri:
AspNetCoreRateLimit(Redis desteği),Polly.RateLimitgibi.
6. Yaygın Tuzaklar ve İpuçları
-
Token Bucket'ta Replenishment Period (Yenileme Periyodu): Token'ları saniyede 10 eklemek yerine, toplu olarak (ör. 60 saniyede 600 token) eklemek, sürekli yenileme maliyetini azaltır.
-
Rate Limiting'i Yalnızca Gateway'de Uygulamak: Bazı servisler doğrudan gateway dışından çağrılabilir (iç servis çağrıları). Bu nedenle, rate limiting'i hem gateway'de hem de servis katmanında (opsiyonel) uygulayın.
-
Daha İyi Kullanıcı Deneyimi: İstek reddedildiğinde (
429 Too Many Requests) yanıt başlığınaRetry-Afterbilgisini ekleyerek istemciye ne zaman tekrar deneyeceğini bildirin. -
Limitlerin Dinamik Olması: Kullanıcı abonelik seviyesine göre limitler dinamik olarak değişebilir. Bu durumda, partition key'i doğru seçmek (ör. API Key) ve limitleri bir konfigürasyon dosyasından veya veritabanından okumak gerekir.
Sonuç:
Rate limiting, API'larınızın can damarıdır. Hangi algoritmayı seçeceğiniz, uygulamanızın trafik desenine ve ihtiyaçlarına bağlıdır. Token Bucket, çoğu genel senaryo için en iyi dengeyi sunar. Fixed Window basit projeler için yeterlidir, ancak sınır sorununu göz önünde bulundurun. Sliding Window, yüksek hassasiyet gerektiren durumlar için tercih edilir. .NET 7+ ile gelen yerleşik Rate Limiter sayesinde, bu algoritmaları sıfırdan yazmak zorunda değilsiniz; sadece doğru yapılandırmayı yapın ve middleware'i ekleyin. Unutmayın, hızlı ve adil bir API, mutlu kullanıcılar demektir.