Rate Limiting и Throttling в Rust: Обуздаем потоки данных

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

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

Начать курс

Когда Rust встречает перегрузку: почему ваш сервис «падает»?

Представьте, что вы написали на Rust идеальный высоконагруженный сервис. Он летает, использует все преимущества zero-cost абстракций и владения памятью. Но вот беда — приходит один «прожорливый» клиент и начинает долбить ваш сервер тысячами запросов в секунду. Что происходит? Падает база данных? Задыхается очередь? Или, что еще хуже, вы получаете бан от внешнего API, на который сами же ходите?

В Rust, как и в любом другом языке, без rate limiting (ограничения скорости) и throttling (дросселирования) никуда. Это не просто «хорошая практика» — это вопрос выживания вашего приложения в production. Но в Rust есть одна особенность: мы любим делать все эффективно и с нуля. Однако изобретать велосипед не придется — у нас есть мощные крейты (библиотеки) и элегантные паттерны.

В этой статье мы разберем, как правильно реализовать ограничение запросов в Rust, используя как классические алгоритмы (Token Bucket), так и современные асинхронные подходы. Вы узнаете, как защитить свой API от DDoS-атак, как не превысить лимиты внешних сервисов и как написать свой собственный throttler, который будет работать быстрее, чем на Go или Python.

Rate Limiting vs Throttling: в чем разница?

Прежде чем нырять в код, давайте раз и навсегда разберемся с терминами. Часто их путают, но разница критична.

  • Rate Limiting — это политика безопасности. Вы говорите: «Клиент X может делать не более 100 запросов в минуту». Если лимит превышен — возвращаете 429 Too Many Requests. Это защита вашего бэкенда.
  • Throttling — это механизм управления скоростью. Вы сами решаете, с какой скоростью обрабатывать запросы, чтобы не перегрузить CPU, I/O или базу данных. Клиент может слать 1000 запросов, но вы будете обрабатывать их по 10 в секунду, остальные — ставить в очередь.

В Rust оба подхода можно реализовать с помощью одного и того же инструментария, но с разной логикой. Мы рассмотрим оба.

Алгоритмы: Token Bucket и Leaky Bucket

Самые популярные алгоритмы для rate limiting — это Token Bucket и Leaky Bucket. Они просты, как валенок, но невероятно эффективны.

Token Bucket (Ведро с токенами)

Представьте ведро, в которое каждую секунду капает N токенов. Когда приходит запрос, он забирает один токен. Если ведро пусто — запрос отклоняется. Rust идеально подходит для реализации этого алгоритма благодаря атомарным операциям.

Leaky Bucket (Дырявое ведро)

Здесь ведро имеет дырку в дне, через которую вытекает фиксированное количество запросов в секунду. Запросы попадают в ведро сверху. Если ведро переполняется — лишние запросы отбрасываются. Этот алгоритм лучше для сглаживания пиковых нагрузок.

В Rust мы не будем писать эти алгоритмы руками (хотя это и познавательно). Вместо этого используем крейт governor — золотой стандарт rate limiting в экосистеме Rust.

Практика: Устанавливаем Governor

Крейт governor — это быстрая, асинхронная и потокобезопасная реализация Token Bucket. Он поддерживает как синхронный, так и асинхронный режимы, работает с tokio и async-std.

Добавляем в Cargo.toml:

[dependencies]
governor = "0.6"
tokio = { version = "1", features = ["full"] }

А теперь напишем простой rate limiter для HTTP-сервера на базе hyper или axum. Для простоты используем axum.

use axum::{
    extract::Request,
    http::StatusCode,
    middleware::{self, Next},
    response::Response,
    routing::get,
    Router,
};
use governor::{DefaultDirectRateLimiter, Quota, RateLimiter};
use std::num::NonZeroU32;
use std::sync::Arc;
use std::time::Duration;

/// Создаем rate limiter: 10 запросов в секунду
fn create_rate_limiter() -> Arc<DefaultDirectRateLimiter> {
    // NonZeroU32 гарантирует, что лимит не равен нулю
    let quota = Quota::per_second(NonZeroU32::new(10).unwrap())
        .allow_burst(NonZeroU32::new(5).unwrap()); // разрешаем burst в 5 запросов
    Arc::new(RateLimiter::direct(quota))
}

/// Middleware для rate limiting
async fn rate_limit_middleware(
    req: Request,
    next: Next,
    limiter: Arc<DefaultDirectRateLimiter>,
) -> Result<Response, StatusCode> {
    // Проверяем, есть ли токен
    match limiter.check() {
        Ok(_) => Ok(next.run(req).await),
        Err(_) => Err(StatusCode::TOO_MANY_REQUESTS),
    }
}

#[tokio::main]
async fn main() {
    let limiter = create_rate_limiter();

    let app = Router::new()
        .route("/", get(|| async { "Hello, Rust!" }))
        .layer(middleware::from_fn_with_state(
            limiter,
            rate_limit_middleware,
        ));

    println!("Server running on http://127.0.0.1:3000");
    axum::Server::bind(& "0.0.0.0:3000".parse().unwrap())
        .serve(app.into_make_service())
        .await
        .unwrap();
}

Этот код — база. Вы можете гибко настраивать Quota: per_second, per_minute, per_hour. Метод allow_burst позволяет временно пропускать больше запросов, если клиент "накопил" токены. Это идеально для API с переменной нагрузкой.

Throttling: контроль скорости обработки

Rate limiting — это про отказ. Throttling — про управление очередью. В Rust для этого удобно использовать каналы tokio или async-channel. Рассмотрим паттерн "Worker Pool" с ограничением скорости.

Допустим, у нас есть задача: обрабатывать входящие данные, но не быстрее 100 штук в секунду, чтобы не перегрузить базу данных.

use tokio::sync::Semaphore;
use tokio::time::{sleep, Duration};

/// Ограничитель скорости на основе Semaphore
pub struct Throttler {
    semaphore: Semaphore,
    interval: Duration,
}

impl Throttler {
    /// Создаем throttler: max_rate — макс. количество операций в секунду
    pub fn new(max_rate: u64) -> Self {
        let semaphore = Semaphore::new(max_rate as usize);
        let interval = Duration::from_secs(1);
        Self { semaphore, interval }
    }

    /// Ожидает разрешения на выполнение
    pub async fn acquire(& self) {
        let _permit = self.semaphore.acquire().await                

Блоги

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