Когда ваш API начинает задыхаться: Введение в rate limiting
Представьте ситуацию: вы написали идеальный сервер на C++. Он быстрый, эффективный, использует все ядра процессора. Вы запускаете его в продакшн, и... через пять минут он падает. Что случилось? Оказалось, какой-то скрипт-хулиган решил «проверить» ваш API, отправляя тысячи запросов в секунду.
Знакомо? Если нет — вам повезло. Но рано или поздно каждый разработчик сталкивается с необходимостью контролировать нагрузку. Именно здесь на сцену выходят rate limiting и throttling — два мощных механизма, которые спасают серверы от перегрузок и обеспечивают равномерное распределение ресурсов.
В этой статье мы разберёмся, как реализовать эти механизмы на C++. Вы узнаете не только теорию, но и получите рабочий код, который можно сразу использовать в своих проектах. Поехали!
Rate Limiting vs Throttling: В чём разница?
Прежде чем погружаться в код, давайте чётко разделим эти два понятия. Многие путают их, но разница есть, и она критична.
Rate Limiting (Ограничение скорости)
Это «шлагбаум». Вы говорите: «За одну секунду — не больше 10 запросов. Всё, что сверху, — в бан (или очередь)». Rate limiting защищает ваш API от лавинообразных запросов, DDoS-атак и случайных всплесков трафика.
Throttling (Дросселирование)
Это «регулятор громкости». Вы не блокируете запросы полностью, а замедляете их обработку. Например, если клиент отправляет 100 запросов в секунду, вы обрабатываете их, но с задержкой в 10 мс между каждым. Throttling — более «вежливый» способ управления нагрузкой, он не отбрасывает данные, а просто сглаживает пики.
В реальных проектах эти техники часто комбинируют: сначала throttling для сглаживания, а при превышении критического порога — rate limiting с возвратом ошибки 429 (Too Many Requests).
Алгоритмы rate limiting в C++
Существует несколько классических алгоритмов. Мы разберём два самых популярных и эффективных: Token Bucket и Sliding Window. Оба легко реализуются на C++ и дают отличные результаты.
1. Token Bucket (Ведро токенов)
Представьте ведро, в которое каждую секунду капает N токенов. Каждый запрос забирает один токен. Если ведро пустое — запрос отклоняется. Просто и гениально.
Вот реализация на современном C++ с использованием std::chrono:
#include <chrono>
#include <mutex>
#include <atomic>
class TokenBucket {
public:
TokenBucket(size_t max_tokens, double refill_rate)
: max_tokens_(max_tokens)
, refill_rate_(refill_rate)
, current_tokens_(max_tokens)
, last_refill_(std::chrono::steady_clock::now()) {}
bool try_consume() {
std::lock_guard<std::mutex> lock(mutex_);
// Вычисляем, сколько токенов накопилось с прошлого раза
auto now = std::chrono::steady_clock::now();
auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(now - last_refill_);
// Пополняем ведро
double tokens_to_add = elapsed.count() * refill_rate_ / 1000.0;
current_tokens_ = std::min(max_tokens_, current_tokens_ + tokens_to_add);
last_refill_ = now;
// Пытаемся взять токен
if (current_tokens_ > 0) {
--current_tokens_;
return true;
}
return false;
}
private:
std::mutex mutex_;
size_t max_tokens_; // Максимальная ёмкость ведра
double refill_rate_; // Токенов в секунду
double current_tokens_; // Текущее количество токенов
std::chrono::steady_clock::time_point last_refill_;
};
Использование:
TokenBucket limiter(10, 1.0); // 10 токенов, пополнение 1 токен/сек
// В обработчике запроса:
if (limiter.try_consume()) {
process_request();
} else {
send_429_error();
}
Плюсы: Простота, мало памяти, работает для равномерного трафика.
Минусы: Не идеален для burst-трафика (всплесков).
2. Sliding Window (Скользящее окно)
Этот алгоритм более точен. Вы запоминаете временные метки последних N запросов и проверяете, сколько из них попало в последнюю секунду (или любой другой интервал).
Реализация с кольцевым буфером для эффективности:
#include <vector>
#include <chrono>
#include <mutex>
class SlidingWindowLimiter {
public:
SlidingWindowLimiter(size_t max_requests, std::chrono::milliseconds window_duration)
: max_requests_(max_requests)
, window_duration_(window_duration)
, timestamps_(max_requests, std::chrono::steady_clock::time_point::min())
, head_(0), count_(0) {}
bool try_consume() {
std::lock_guard<std::mutex> lock(mutex_);
auto now = std::chrono::steady_clock::now();
// Удаляем устаревшие метки
while (count_ > 0 && (now - timestamps_[(head_ - count_ + max_requests_) % max_requests_]) > window_duration_) {
--count_;
}
// Проверяем лимит
if (count_ < max_requests_) {
timestamps_[head_] = now;
head_ = (head_ + 1) % max_requests_;
++count_;
return true;
}
return false;
}
private:
std::mutex mutex_;
size_t max_requests_;
std::chrono::milliseconds window_duration_;
std::vector<std::chrono::steady_clock::time_point> timestamps_;
size_t head_;
size_t count_;
};
Плюсы: Точность, хорошо обрабатывает burst-трафик, легко настраивается.
Минусы: Требует больше памяти (хранит метки).
Throttling в C++: Плавное замедление
А теперь перейдём к throttling. Здесь наша задача — не блокировать, а регулировать скорость обработки. Классический подход — использовать таймеры или std::this_thread::sleep_for.
Пример простого throttler, который ограничивает количество операций в секунду:
#include <chrono>
#include <thread>
#include <atomic>
class SimpleThrottler {
public:
explicit SimpleThrottler(double operations_per_second)
: min_interval_(std::chrono::microseconds(static_cast<int>(1'000'000 / operations_per_second)))
, last_time_(std::chrono::steady_clock::now()) {}
void throttle() {
auto now = std::chrono::steady_clock::now();
auto elapsed = std::chrono::duration_cast<std::chrono::microseconds>(now - last_time_);
if (elapsed < min_interval_) {
std::this_thread