Когда 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