Вы когда-нибудь ловили себя на том, что пытаетесь выжать каждую миллисекунду из своего HTTP-сервера? Возможно, вы слышали, что «Nginx — король скорости», и думали: «Вызов принят!» Что ж, давайте поговорим об обработке небольшого контента (менее 100 КБ) в десять раз быстрее обычного.
Секретный соус? Эффективное управление памятью с помощью буферных пулов. ?
? Проблема
Каждому HTTP-запросу нужен буфер для обработки контента. Начнем с простого:
let mut buf = Vec::with_capacity(8192);
Звучит достаточно невинно, не так ли? Но для высокопроизводительного сервера выделение и освобождение этих буферов тысячи раз в секунду является серьезным узким местом. Нам нужно что-то более быстрое, более эффективное — что-то, что заставит попотеть даже Nginx. ?
?♂️ Решение: буферный пул!
Я создал BufferPool, который предварительно выделяет буферы и повторно использует их, все в великолепном асинхронном Rust:
use std::sync::Arc;
use tokio::sync::Mutex;
pub type SmartVector = Arc<Mutex<Vec<u8>>>;
pub struct BufferPool {
pool: Arc<Mutex<Vec<SmartVector>>>,
}
impl BufferPool {
pub fn new(buffer_count: usize, buffer_size: usize) -> Self {
let pool = (0..buffer_count)
.map(|_| Arc::new(Mutex::new(Vec::with_capacity(buffer_size))))
.collect();
BufferPool {
pool: Arc::new(Mutex::new(pool)),
}
}
pub async fn get_buffer(&self) -> Option<SmartVector> {
let mut pool = self.pool.lock().await;
pool.pop()
}
pub async fn return_buffer(&self, buffer: SmartVector) {
let mut pool = self.pool.lock().await;
pool.push(buffer);
}
}
? Что здесь происходит?
Мы используем Arc и Mutex для совместного использования и защиты буферов параллельным потокобезопасным способом.
BufferPool создает пул буферов при запуске, каждый с фиксированной емкостью.
get_buffer извлекает буфер из пула, а return_buffer возвращает его обратно. Просто и мило!
?️ Профессиональное использование BufferPool
Посмотрите на основной цикл, где происходит магия:
let max_connections = 5000;
let BUF_SIZE = 8192;
let semaphore = Arc::new(Semaphore::new(max_connections));
let buffer_pool = Arc::new(BufferPool::new(max_connections, BUF_SIZE));
loop {
let semaphore = semaphore.clone();
let permit = semaphore.acquire_owned().await?;
let buffer_pool_arc = buffer_pool.clone();
tokio::spawn(async move {
let _permit = permit; // Сохраняем разрешение, пока не закончим обработку
// Получаем буфер из пула
let buffer = buffer_pool_arc.get_buffer().await.unwrap();
// ? Делаем что-то быстрое и удивительное с буфером здесь
buffer.lock().await.clear(); // Очищаем буфер для повторного использования
buffer_pool_arc.return_buffer(buffer).await; // Возвращаем его в пул
});
}
? Что происходит?
Мы используем семафор для управления максимальным количеством одновременных подключений. В конце концов, мы не собираемся расплавлять наши серверы. ?
tokio::spawn создает легкие задачи, и каждая из них получает буфер из нашего пула
Буферы очищаются и перерабатываются эффективно. Потому что мы заботимся о наших буферах, а память на свалке — это прошлый год. ?
? Почему это так быстро?
Избегая постоянного выделения и освобождения памяти, мы сокращаем накладные расходы и задержку. Наши буферы всегда готовы, как и ваш лучший друг, который всегда готов потусоваться. ?
Итак, если вы создаете HTTP-сервер и хотите превзойти Nginx, попробуйте Buffer Pools. Ваши пользователи (и ваши серверы) будут вам благодарны! ?
Есть вопросы или вы хотите обсудить другие безумные оптимизации? Оставьте комментарий ниже! Или просто расскажите мне, как продвигается ваш последний проект Rust. Я весь внимание!
Исходники проекта где это использую BufferPool: https://github.com/evgenyigumnov/cblt