Вы когда-нибудь ловили себя на том, что пытаетесь выжать каждую миллисекунду из своего 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);
    }
}

? Что здесь происходит?

  1. Мы используем Arc и Mutex для совместного использования и защиты буферов параллельным потокобезопасным способом.

  2. BufferPool создает пул буферов при запуске, каждый с фиксированной емкостью.

  3. 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; // Возвращаем его в пул
    });
}

? Что происходит?

  1. Мы используем семафор для управления максимальным количеством одновременных подключений. В конце концов, мы не собираемся расплавлять наши серверы. ?

  2. tokio::spawn создает легкие задачи, и каждая из них получает буфер из нашего пула

  3. Буферы очищаются и перерабатываются эффективно. Потому что мы заботимся о наших буферах, а память на свалке — это прошлый год. ?

? Почему это так быстро?

Избегая постоянного выделения и освобождения памяти, мы сокращаем накладные расходы и задержку. Наши буферы всегда готовы, как и ваш лучший друг, который всегда готов потусоваться. ?

Итак, если вы создаете HTTP-сервер и хотите превзойти Nginx, попробуйте Buffer Pools. Ваши пользователи (и ваши серверы) будут вам благодарны! ?

Есть вопросы или вы хотите обсудить другие безумные оптимизации? Оставьте комментарий ниже! Или просто расскажите мне, как продвигается ваш последний проект Rust. Я весь внимание!

Исходники проекта где это использую BufferPool: https://github.com/evgenyigumnov/cblt

Комментарии (12)