Rate Limiting и Throttling в C#: Укрощаем потоки запросов

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

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 и многое другое.

Практические сценарии: Где и что применять?

Сценарий 1: Защита публичного API

Блоги

Рекомендаций по книгам