Skip to content

Оптимизация производительности в Axum

Axum, благодаря своей архитектуре на основе Tokio и async/await, изначально обеспечивает высокую производительность. Однако для достижения максимальной эффективности в высоконагруженных приложениях требуется применение дополнительных техник оптимизации.

Содержание

Основы асинхронной производительности

Оптимизация задач Tokio

rust
// Разделяйте вычислительно-интенсивные операции
async fn process_data(data: Vec<u8>) -> Result<Vec<u8>, Error> {
    // Перемещаем блокирующую операцию в отдельный поток
    let result = tokio::task::spawn_blocking(move || {
        // Здесь выполняется сложная CPU-интенсивная обработка
        expensive_calculation(data)
    }).await??;
    
    Ok(result)
}

// Избегайте блокировки асинхронного контекста
async fn process_user(user_id: i32, db: &DbPool) -> Result<User, Error> {
    // Плохо: блокирует поток
    // std::thread::sleep(std::time::Duration::from_millis(100));
    
    // Хорошо: асинхронное ожидание
    tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
    
    // Продолжение обработки
    db.query_one("SELECT * FROM users WHERE id = $1", &[&user_id]).await
}

Настройка количества рабочих потоков Tokio

rust
#[tokio::main(worker_threads = 16)]
async fn main() {
    // Количество потоков оптимизировано под многоядерный сервер
    // ...
}

// Альтернативная настройка через код
fn main() {
    // Определение оптимального количества потоков
    let num_cpus = std::thread::available_parallelism()
        .map(|n| n.get())
        .unwrap_or(2);
    
    // Создание runtime с настроенным числом потоков
    let runtime = tokio::runtime::Builder::new_multi_thread()
        .worker_threads(num_cpus)
        .enable_all()
        .build()
        .unwrap();
    
    // Запуск приложения
    runtime.block_on(async {
        // Код приложения
    });
}

Оптимизация маршрутизации

Эффективная структура маршрутов

rust
// Оптимизируйте порядок маршрутов: самые частые запросы размещайте сначала
let app = Router::new()
    // Часто используемые маршруты
    .route("/users/me", get(get_current_user))
    .route("/articles", get(list_articles))
    // Реже используемые маршруты
    .route("/users/:id", get(get_user_by_id))
    .route("/admin/settings", get(admin_settings));

// Используйте разделение на подмаршруты для более эффективного поиска
let api_routes = Router::new()
    .nest("/users", users_routes())
    .nest("/articles", articles_routes())
    .nest("/comments", comments_routes());

Использование StaticFiles вместо динамических обработчиков

rust
use tower_http::services::ServeDir;

// Эффективная обработка статических файлов
let app = Router::new()
    .nest_service("/static", ServeDir::new("static"))
    .route("/api/users", get(list_users));

Обработка соединений и пулы

Оптимизация пула базы данных

rust
use sqlx::postgres::PgPoolOptions;

async fn create_db_pool() -> Result<PgPool, Error> {
    // Настройка пула соединений
    let pool = PgPoolOptions::new()
        // Настройка максимального количества соединений
        // Соблюдайте баланс между памятью и конкурентностью
        .max_connections(32)
        // Устанавливаем минимальный размер пула
        .min_connections(5)
        // Максимальное время жизни соединения
        .max_lifetime(std::time::Duration::from_secs(1800))
        // Тайм-аут при превышении максимального количества соединений
        .acquire_timeout(std::time::Duration::from_secs(5))
        // Создание пула
        .connect(&std::env::var("DATABASE_URL")?)
        .await?;
    
    Ok(pool)
}

HTTP-клиент с повторным использованием соединений

rust
use reqwest::ClientBuilder;

fn create_http_client() -> reqwest::Client {
    ClientBuilder::new()
        // Повторное использование соединений
        .pool_max_idle_per_host(10)
        // Тайм-аут соединения
        .connect_timeout(std::time::Duration::from_secs(10))
        // Максимальное количество повторов
        .pool_max_idle_per_host(512)
        .build()
        .unwrap()
}

Работа с памятью

Избегайте клонирования больших данных

rust
// Плохо: ненужное клонирование
async fn handle_request(State(state): State<AppState>, Json(data): Json<Data>) {
    // Клонирование data, когда это не нужно
    process_data(data.clone()).await;
}

// Хорошо: использование ссылок или Arc
use std::sync::Arc;

#[derive(Clone)]
struct AppState {
    config: Arc<Config>,
    db: PgPool,
}

async fn handle_request(State(state): State<AppState>, Json(data): Json<Data>) {
    // Переносим владение без клонирования
    process_data(data).await;
}

Управление буферами и память хранилища данных

rust
use bytes::{Bytes, BytesMut};

// Эффективная работа с буферами
async fn process_large_file(mut stream: impl tokio::io::AsyncRead + Unpin) -> Result<(), Error> {
    // Создание буфера фиксированного размера
    let mut buffer = BytesMut::with_capacity(8192);
    
    while let Ok(n) = stream.read_buf(&mut buffer).await {
        if n == 0 {
            break;
        }
        
        // Обработка данных в буфере
        process_chunk(&buffer[..n]).await?;
        
        // Очистка буфера для следующей порции данных
        buffer.clear();
    }
    
    Ok(())
}

Кэширование

Встроенный кэш с помощью moka

rust
use moka::future::Cache;
use std::time::Duration;

#[derive(Clone)]
struct AppState {
    db: PgPool,
    // Кэш пользователей: ключ = id, значение = User
    user_cache: Cache<i32, User>,
}

impl AppState {
    fn new(db: PgPool) -> Self {
        // Создание кэша с настройками
        let user_cache = Cache::builder()
            // Максимальный размер
            .max_capacity(10_000)
            // TTL (время жизни) каждого элемента
            .time_to_live(Duration::from_secs(300))
            // Время бездействия, после которого элемент удаляется
            .time_to_idle(Duration::from_secs(600))
            .build();
        
        Self { db, user_cache }
    }
}

async fn get_user(Path(user_id): Path<i32>, State(state): State<AppState>) -> Result<Json<User>, Error> {
    // Попытка получить из кэша
    if let Some(user) = state.user_cache.get(&user_id).await {
        return Ok(Json(user));
    }
    
    // Если нет в кэше, читаем из БД
    let user = sqlx::query_as::<_, User>("SELECT * FROM users WHERE id = $1")
        .bind(user_id)
        .fetch_one(&state.db)
        .await?;
    
    // Сохраняем в кэш
    state.user_cache.insert(user_id, user.clone()).await;
    
    Ok(Json(user))
}

Кэширование HTTP-ответов

rust
use std::collections::HashMap;
use std::sync::Mutex;
use axum::http::{HeaderMap, StatusCode};

#[derive(Clone)]
struct AppState {
    response_cache: Arc<Mutex<HashMap<String, (HeaderMap, Vec<u8>, StatusCode)>>>,
}

async fn cached_endpoint(
    req: Request<Body>, 
    State(state): State<AppState>,
    next: Next,
) -> Result<Response, StatusCode> {
    // Формирование ключа кэша
    let cache_key = format!("{}:{}", req.method(), req.uri());
    
    // Проверка наличия в кэше
    {
        let cache = state.response_cache.lock().unwrap();
        if let Some((headers, body, status)) = cache.get(&cache_key) {
            // Возврат кэшированного ответа
            let mut response = Response::builder()
                .status(*status);
            
            for (name, value) in headers.iter() {
                response = response.header(name, value);
            }
            
            return Ok(response
                .body(Body::from(body.clone()))
                .unwrap());
        }
    }
    
    // Если нет в кэше, обрабатываем запрос
    let mut response = next.run(req).await;
    
    // Кэшируем результат, если это GET запрос
    if req.method() == Method::GET {
        let status = response.status();
        let headers = response.headers().clone();
        
        // Читаем тело ответа
        let body = hyper::body::to_bytes(response.into_body())
            .await
            .unwrap()
            .to_vec();
        
        // Сохраняем в кэш
        {
            let mut cache = state.response_cache.lock().unwrap();
            cache.insert(cache_key, (headers.clone(), body.clone(), status));
        }
        
        // Возвращаем новый ответ из сохраненных данных
        let mut response_builder = Response::builder()
            .status(status);
        
        for (name, value) in headers.iter() {
            response_builder = response_builder.header(name, value);
        }
        
        return Ok(response_builder
            .body(Body::from(body))
            .unwrap());
    }
    
    Ok(response)
}

Оптимизация баз данных

Эффективные запросы

rust
// Плохо: получение всех полей, когда нужны только некоторые
async fn get_user_bad(db: &PgPool, id: i32) -> Result<User, Error> {
    sqlx::query_as::<_, User>("SELECT * FROM users WHERE id = $1")
        .bind(id)
        .fetch_one(db)
        .await
}

// Хорошо: выборка только нужных полей
#[derive(sqlx::FromRow)]
struct UserBasicInfo {
    id: i32,
    name: String,
    email: String,
}

async fn get_user_good(db: &PgPool, id: i32) -> Result<UserBasicInfo, Error> {
    sqlx::query_as::<_, UserBasicInfo>("SELECT id, name, email FROM users WHERE id = $1")
        .bind(id)
        .fetch_one(db)
        .await
}

Пакетная обработка

rust
async fn create_users_batch(db: &PgPool, users: Vec<NewUser>) -> Result<(), Error> {
    // Формирование запроса для пакетной вставки
    let mut query_builder: QueryBuilder<Postgres> = QueryBuilder::new(
        "INSERT INTO users (name, email, created_at) "
    );
    
    query_builder.push_values(users, |mut b, user| {
        b.push_bind(user.name)
            .push_bind(user.email)
            .push_bind(chrono::Utc::now());
    });
    
    // Выполнение запроса
    query_builder.build().execute(db).await?;
    
    Ok(())
}

Обработка JSON

Эффективное использование serde

rust
use serde::{Deserialize, Serialize};

// Оптимизация десериализации: пропуск неизвестных полей
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
struct CreateUser {
    name: String,
    email: String,
    #[serde(default)]
    age: Option<u8>,
}

// Условная сериализация полей
#[derive(Debug, Serialize)]
struct User {
    id: i32,
    name: String,
    email: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    age: Option<u8>,
    #[serde(skip_serializing)]
    password_hash: String,
}

Использование simd-json для высокопроизводительной обработки JSON

rust
use axum::{
    extract::FromRequest,
    http::StatusCode,
    response::{IntoResponse, Response},
    Json,
};
use serde::de::DeserializeOwned;

// Собственный экстрактор для оптимизированной десериализации JSON
struct FastJson<T>(pub T);

#[axum::async_trait]
impl<S, T> FromRequest<S> for FastJson<T>
where
    T: DeserializeOwned,
    S: Send + Sync,
{
    type Rejection = Response;

    async fn from_request(req: Request, state: &S) -> Result<Self, Self::Rejection> {
        let bytes = hyper::body::to_bytes(req.into_body())
            .await
            .map_err(|e| (StatusCode::BAD_REQUEST, e.to_string()).into_response())?;
            
        let mut bytes_vec = bytes.to_vec();
        
        // Использование simd-json для быстрой десериализации
        let result = match unsafe { simd_json::from_slice::<T>(&mut bytes_vec) } {
            Ok(value) => Ok(FastJson(value)),
            Err(e) => Err((StatusCode::BAD_REQUEST, e.to_string()).into_response()),
        };
        
        result
    }
}

Сжатие и оптимизация передачи данных

Использование сжатия через Tower

rust
use axum::Router;
use tower_http::compression::{CompressionLayer, predicate::SizeAbove};

// Создание роутера с поддержкой сжатия
let app = Router::new()
    .route("/api/users", get(list_users))
    // Добавляем слой сжатия
    .layer(
        CompressionLayer::new()
            // Сжимаем только ответы больше 1024 байт
            .compress_when(SizeAbove::new(1024))
    );

Потоковая передача больших ответов

rust
use axum::{
    response::IntoResponse,
    body::StreamBody,
};
use tokio_util::io::ReaderStream;
use std::fs::File;

async fn stream_large_file() -> impl IntoResponse {
    // Открытие файла
    let file = tokio::fs::File::open("large_file.mp4").await.unwrap();
    
    // Создание потока из файла
    let stream = ReaderStream::new(file);
    let body = StreamBody::new(stream);
    
    // Формирование ответа с потоком
    (
        [
            ("content-type", "video/mp4"),
            ("content-disposition", "attachment; filename=\"video.mp4\""),
        ],
        body,
    )
}

Профилирование и метрики

Интеграция с Prometheus

rust
use axum::{routing::get, Router};
use prometheus::{
    HistogramOpts, HistogramVec, IntCounterVec, Opts, Registry,
};

// Создание и настройка метрик
fn setup_metrics() -> Router {
    // Создание реестра Prometheus
    let registry = Registry::new();
    
    // Счетчик запросов
    let request_counter = IntCounterVec::new(
        Opts::new("http_requests_total", "Total number of HTTP requests"),
        &["method", "path", "status"],
    ).unwrap();
    
    // Гистограмма времени отклика
    let request_histogram = HistogramVec::new(
        HistogramOpts::new(
            "http_request_duration_seconds",
            "HTTP request duration in seconds",
        ),
        &["method", "path"],
    ).unwrap();
    
    // Регистрация метрик
    registry.register(Box::new(request_counter.clone())).unwrap();
    registry.register(Box::new(request_histogram.clone())).unwrap();
    
    // Обработчик для выдачи метрик
    async fn metrics_handler(registry: Extension<Registry>) -> impl IntoResponse {
        let encoder = prometheus::TextEncoder::new();
        let mut buffer = Vec::new();
        encoder.encode(&registry.gather(), &mut buffer).unwrap();
        String::from_utf8(buffer).unwrap()
    }
    
    // Создание маршрутизатора с метриками
    Router::new()
        .route("/metrics", get(metrics_handler))
        .layer(Extension(registry))
        .layer(Extension(request_counter))
        .layer(Extension(request_histogram))
}

Middleware для трассировки производительности

rust
use std::time::Instant;
use tower::Service;
use tower::ServiceBuilder;
use axum::response::IntoResponse;
use tracing::{info, instrument};

// Middleware для измерения времени выполнения запросов
#[derive(Clone)]
struct TimingMiddleware<S> {
    inner: S,
}

impl<S, B> Service<Request<B>> for TimingMiddleware<S>
where
    S: Service<Request<B>>,
    S::Response: IntoResponse,
    S::Error: std::fmt::Display,
    B: std::fmt::Debug + Send + 'static,
{
    type Response = S::Response;
    type Error = S::Error;
    type Future = std::pin::Pin<Box<dyn std::future::Future<Output = Result<Self::Response, Self::Error>> + Send>>;

    fn poll_ready(&mut self, cx: &mut std::task::Context<'_>) -> std::task::Poll<Result<(), Self::Error>> {
        self.inner.poll_ready(cx)
    }

    fn call(&mut self, req: Request<B>) -> Self::Future {
        let method = req.method().clone();
        let uri = req.uri().clone();
        let start = Instant::now();

        let future = self.inner.call(req);

        Box::pin(async move {
            let result = future.await;
            let elapsed = start.elapsed();

            info!(
                target: "request_timing",
                method = %method,
                uri = %uri,
                elapsed_ms = %elapsed.as_millis(),
                "Request completed"
            );

            result
        })
    }
}

// Использование в приложении
let app = Router::new()
    .route("/api/users", get(list_users))
    .layer(
        ServiceBuilder::new()
            .layer_fn(|svc| TimingMiddleware { inner: svc })
    );

Лучшие практики

1. Асинхронность и корректная работа с Tokio

  • Избегайте блокирующих операций в асинхронном контексте
  • Используйте spawn_blocking для CPU-интенсивных задач
  • Применяйте select! для параллельного выполнения нескольких асинхронных операций
  • Устанавливайте оптимальное количество рабочих потоков Tokio

2. Эффективная работа с данными

  • Минимизируйте клонирование и копирование данных
  • Используйте Bytes и BytesMut для эффективной работы с бинарными данными
  • Применяйте кэширование для часто запрашиваемых данных
  • Для передачи больших файлов используйте потоковую обработку

3. Оптимизация базы данных

  • Запрашивайте только необходимые поля
  • Используйте подготовленные выражения и пакетную обработку
  • Добавляйте индексы для часто запрашиваемых полей
  • Оптимизируйте настройки пула соединений

4. Мониторинг и метрики

  • Настройте сбор метрик производительности
  • Отслеживайте длительность обработки запросов
  • Мониторьте использование ресурсов (CPU, память, сеть)
  • Регулярно выполняйте профилирование и оптимизацию узких мест

Оптимизация производительности Axum-приложений — это постоянный и итеративный процесс. Начните с ключевых оптимизаций, измеряйте результаты и продолжайте улучшать наиболее критичные участки кода.