Rate Limiting и Throttling в C#: Как защитить API от перегрузок и сохранить нервы пользователей
Представьте: вы запустили крутой API. Сервер летает, база данных отвечает мгновенно. Но вдруг — бац! — один нерадивый клиент (или, что еще хуже, DDoS-атака) начинает долбить вашу точку входа тысячами запросов в секунду. Сервер падает, база задыхается, а реальные пользователи видят 503-ю ошибку. Знакомо?
Решение этой проблемы — rate limiting (ограничение скорости) и throttling (дросселирование). В C# у нас есть все инструменты, чтобы внедрить эти механизмы элегантно и эффективно. В этой статье мы не просто пройдемся по теории — мы напишем реальный код, разберем популярные алгоритмы и выясним, что выбрать под конкретную задачу. Пристегнитесь, будет жарко!
Rate Limiting vs Throttling: В чем разница?
Хотя эти термины часто путают, между ними есть четкая грань:
- Rate Limiting — это «жесткая» блокировка. Если пользователь превысил лимит (например, 100 запросов в минуту), сервер возвращает HTTP 429 (Too Many Requests). Запросы просто не доходят до бизнес-логики.
- Throttling — это «мягкое» замедление. Сервер принимает запрос, но обрабатывает его с задержкой или снижает приоритет. Пользователь не получает ошибку, но скорость его работы падает.
В C# мы можем реализовать оба подхода, часто комбинируя их для разных сценариев.
Алгоритмы Rate Limiting: От простого к сложному
Прежде чем писать код, давайте разберемся с математикой. Существует четыре «золотых» алгоритма, которые лежат в основе 90% решений.
1. Fixed Window (Фиксированное окно)
Самый простой способ. Мы делим время на фиксированные интервалы (например, 1 минута) и считаем запросы в каждом окне. Как только счетчик достигает лимита — блокируем.
Плюсы: Простота реализации. Минусы: Проблема «краевого эффекта» (burst при переходе между окнами).
public class FixedWindowRateLimiter
{
private readonly int _limit;
private readonly TimeSpan _windowSize;
private int _counter;
private DateTime _windowStart;
public FixedWindowRateLimiter(int limit, TimeSpan windowSize)
{
_limit = limit;
_windowSize = windowSize;
_windowStart = DateTime.UtcNow;
}
public bool AllowRequest()
{
var now = DateTime.UtcNow;
if (now - _windowStart >= _windowSize)
{
// Сброс окна
_counter = 0;
_windowStart = now;
}
if (_counter >= _limit)
return false; // Too Many Requests
Interlocked.Increment(ref _counter);
return true;
}
}
2. Sliding Window (Скользящее окно)
Эволюция Fixed Window. Вместо жестких границ мы учитываем запросы в скользящем временном отрезке. Обычно реализуется через логирование временных меток (например, в кольцевом буфере).
Плюсы: Сглаживает всплески. Минусы: Чуть сложнее в реализации, требует памяти для хранения меток.
public class SlidingWindowRateLimiter
{
private readonly int _limit;
private readonly TimeSpan _windowSize;
private readonly Queue<DateTime> _timestamps = new();
public SlidingWindowRateLimiter(int limit, TimeSpan windowSize)
{
_limit = limit;
_windowSize = windowSize;
}
public bool AllowRequest()
{
var now = DateTime.UtcNow;
lock (_timestamps)
{
// Удаляем устаревшие метки
while (_timestamps.Count > 0 && now - _timestamps.Peek() > _windowSize)
_timestamps.Dequeue();
if (_timestamps.Count >= _limit)
return false;
_timestamps.Enqueue(now);
return true;
}
}
}
3. Token Bucket (Ведро токенов)
Классический алгоритм для сетевого трафика. У нас есть «ведро» с токенами. Токены добавляются с постоянной скоростью (например, 10 токенов в секунду). Каждый запрос забирает один токен. Если ведро пусто — запрос блокируется или ставится в очередь.
Плюсы: Отлично работает с burst-трафиком (позволяет накопить токены). Минусы: Нужно аккуратно управлять состоянием в многопоточной среде.
public class TokenBucketRateLimiter
{
private readonly int _capacity; // Макс. размер ведра
private readonly double _tokensPerSecond; // Скорость пополнения
private double _tokens;
private DateTime _lastRefill;
public TokenBucketRateLimiter(int capacity, double tokensPerSecond)
{
_capacity = capacity;
_tokensPerSecond = tokensPerSecond;
_tokens = capacity;
_lastRefill = DateTime.UtcNow;
}
public bool TryConsume(int tokens = 1)
{
lock (this)
{
RefillTokens();
if (_tokens < tokens)
return false;
_tokens -= tokens;
return true;
}
}
private void RefillTokens()
{
var now = DateTime.UtcNow;
var elapsed = (now - _lastRefill).TotalSeconds;
_tokens = Math.Min(_capacity, _tokens + elapsed * _tokensPerSecond);
_lastRefill = now;
}
}
4. Leaky Bucket (Дырявое ведро)
Обратный вариант Token Bucket. Запросы «капают» в ведро с фиксированной скоростью. Если ведро переполняется — запросы отбрасываются. Идеально для сглаживания пикового трафика.
Плюсы: Гарантирует равномерную обработку. Минусы: Может быть излишне строгим для burst-сценариев.
Throttling в C#: Когда скорость — это роскошь
Throttling (дросселирование) чаще используется на стороне клиента (когда вы вызываете внешний API и не хотите получить бан) или для фоновых задач. В C# для этого удобно использовать библиотеку Polly.
using Polly;
using Polly.RateLimiting;
// Создаем политику: не более 10 запросов в секунду, с очередью на 2
var rateLimiter = Policy
.RateLimit(10, TimeSpan.FromSeconds(1), 2);
// Используем
try
{
await rateLimiter.ExecuteAsync(async () =>
{
// Ваш HTTP-запрос
var response = await httpClient.GetAsync("https://api.example.com");
return response;
});
}
catch (RateLimitRejectedException ex)
{
// Слишком много запросов, ждем
Console.WriteLine($"Превышен лимит. Повторите через {ex.RetryAfter} мс");
}
Polly — это must-have для любого .NET-разработчика. Она умеет не только throttling, но и retry, circuit breaker, timeout и многое другое.