Оптимизация производительности в Axum
Axum, благодаря своей архитектуре на основе Tokio и async/await, изначально обеспечивает высокую производительность. Однако для достижения максимальной эффективности в высоконагруженных приложениях требуется применение дополнительных техник оптимизации.
Содержание
- Основы асинхронной производительности
- Оптимизация маршрутизации
- Обработка соединений и пулы
- Работа с памятью
- Кэширование
- Оптимизация баз данных
- Обработка JSON
- Сжатие и оптимизация передачи данных
- Профилирование и метрики
- Лучшие практики
Основы асинхронной производительности
Оптимизация задач 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(®istry.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-приложений — это постоянный и итеративный процесс. Начните с ключевых оптимизаций, измеряйте результаты и продолжайте улучшать наиболее критичные участки кода.