Использование Rust в серверах, написанных на других языках, для повышения производительности

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.

В этой статье мы рассмотрим несколько стратегий по постепенному добавлению Rust в сервер, написанный на другом языке, например JavaScript, Python, Java, Go, PHP, Ruby и т. д. Один из возможных кейсов для подобного добавления — вы профилировали сервер, нашли «горячую» функцию, не соответствующую требованиям производительности из‑за боттлнека по CPU, а обычные техники мемоизации или оптимизации алгоритма были бы невозможны или малоэффективны по той или иной причине. После чего вы пришли к выводу, что стоит посмотреть в сторону реализации данной функции на что‑то написанное на более производительном языке, например на Rust. Отлично, данная статья для вас.

Стратегии расположены по ступеням, где «ступень» — сокращение «ступень на пути к принятию Rust». Первой ступенью будет полное отсутствие Rust в кодовой базе. Последней — полное переписывание сервера на Rust.

В качестве примера, на котором будут производиться опыты и бенчмарки, будет выступать сервер, написанный на JavaScript, рантаймом для него будет Node.js. Несмотря на это, стратегии могут применяться для любого языка или рантайма.

Полный исходный код для каждого из примеров можно найти в этом репозитории.

Стратегии

Ступень 0: Без Rust

Предположим, у нас есть сервер Node.js с HTTP‑эндпоинтом, принимающим строку текста как параметр запроса и возвращающим PNG‑изображение текста, закодированного в виде QR‑кода размером 200 на 200 пикселей.

Код сервера мог бы выглядеть следующим образом:

const express = require('express');
const generateQrCode = require('./generate-qr.js');

const app = express();
app.get('/qrcode', async (req, res) => {
    const { text } = req.query;

    if (!text) {
        return res.status(400).send('missing "text" query param');
    }

    if (text.length > 512) {
        return res.status(400).send('text must be <= 512 bytes');
    }

    try {
        const qrCode = await generateQrCode(text);
        res.setHeader('Content-Type', 'image/png');
        res.send(qrCode);
    } catch (err) {
        res.status(500).send('failed generating QR code');
    }
});

app.listen(42069, '127.0.0.1');

А вот так выглядит наша «горячая» функция:

const QRCode = require('qrcode');

/**
 * @param {string} text - text to encode
 * @returns {Promise<Buffer>|Buffer} - qr code
 */
module.exports = function generateQrCode(text) {
    return QRCode.toBuffer(text, {
        type: 'png',
        errorCorrectionLevel: 'L',
        width: 200,
        rendererOpts: {
            // these options were chosen since
            // they offered the best balance
            // between speed and compression
            // during testing
            deflateLevel: 9, // 0 - 9
            deflateStrategy: 3, // 1 - 4
        },
    });
};

Мы можем обратиться к эндпоинту, сделав запрос к:

http://localhost:42069/qrcode?text=https://www.reddit.com/r/rustjerk/top/?t=all

Он корректно отдаст нам QR‑код в виде PNG:

Как бы то ни было, давайте отправим десятки тысяч запросов к серверу в течение 30 секунд и посмотрим на производительность:

Tier

Throughput

Avg Latency

p99 Latency

Avg Response

Memory

Tier 0

1464 req/sec

68 ms

96 ms

1506 bytes

1353 MB

Поскольку я не описал, как именно выполняется бенчмарк, данные результаты сами по себе не имеют большого смысла, мы не можем сказать «хорошая» или «плохая» ли это производительность. Это нормально, поскольку нас не интересует абсолютный результат, мы будем использовать его в качестве отправной точки и сравнивать результаты будущих реализаций с ним. Каждый сервер тестируется в одном и том же окружении, чтобы относительные сравнения были корректны.

Аномально большое использование памяти связано с запуском Node.js в «режиме кластера», который запускает 12 процессов — по одному на каждое из 12 ядер процессора на тестовой машине, где каждый из них — отдельный процесс Node.js, что и приводит к использованию 1300+ МБ памяти, несмотря на простоту нашего сервера. JS однопоточный, так что это если мы хотим полностью использовать многоядерный процессор, это — необходимое зло.

Ступень 1: CLI-утилита на Rust

В этой реализации мы перепишем горячую функцию на Rust, скомпилируем в качестве CLI‑утилиты и вызовем с нашего сервера.

Начнем с переписывания функции на Rust:

/** qr_lib/lib.rs **/

use qrcode::{QrCode, EcLevel};
use image::Luma;
use image::codecs::png::{CompressionType, FilterType, PngEncoder};

pub type StdErr = Box<dyn std::error::Error>;

pub fn generate_qr_code(text: &str) -> Result<Vec<u8>, StdErr> {
    let qr = QrCode::with_error_correction_level(text, EcLevel::L)?;
    let img_buf = qr.render::<Luma<u8>>()
        .min_dimensions(200, 200)
        .build();
    let mut encoded_buf = Vec::with_capacity(512);
    let encoder = PngEncoder::new_with_quality(
        &mut encoded_buf,
        // these options were chosen since
        // they offered the best balance
        // between speed and compression
        // during testing
        CompressionType::Default,
        FilterType::NoFilter,
    );
    img_buf.write_with_encoder(encoder)?;
    Ok(encoded_buf)
}

Затем превратим ее в CLI‑утилиту:

/** qr_cli/main.rs **/

use std::{env, process};
use std::io::{self, BufWriter, Write};
use qr_lib::StdErr;

fn main() -> Result<(), StdErr> {
    let mut args = env::args();
    if args.len() != 2 {
        eprintln!("Usage: qr-cli <text>");
        process::exit(1);
    }

    let text = args.nth(1).unwrap();
    let qr_png = qr_lib::generate_qr_code(&text)?;

    let stdout = io::stdout();
    let mut handle = BufWriter::new(stdout.lock());
    handle.write_all(&qr_png)?;

    Ok(())
}

Мы можем использовать ее следующим образом:

qr-cli https://youtu.be/cE0wfjsybIQ?t=74 > crab-rave.png

Она корректно дает нам следующий QR-код:

Теперь обновим горячую функцию сервера для использование CLI-утилиты:

const { spawn } = require('child_process');
const path = require('path');
const qrCliPath = path.resolve(__dirname, './qr-cli');

/**
 * @param {string} text - text to encode
 * @returns {Promise<Buffer>} - qr code
 */
module.exports = function generateQrCode(text) {
    return new Promise((resolve, reject) => {
        const qrCli = spawn(qrCliPath, [text]);
        const qrCodeData = [];
        qrCli.stdout.on('data', (data) => {
            qrCodeData.push(data);
        });
        qrCli.stderr.on('data', (data) => {
            reject(new Error(`error generating qr code: ${data}`));
        });
        qrCli.on('error', (err) => {
            reject(new Error(`failed to start qr-cli ${err}`));
        });
        qrCli.on('close', (code) => {
            if (code === 0) {
                resolve(Buffer.concat(qrCodeData));
            } else {
                reject(new Error('qr-cli exited unsuccessfully'));
            }
        });
    });
};

Давайте посмотрим, как это повлияло на производительность:

Абсолютные замеры

Tier

Throughput

Avg Latency

p99 Latency

Avg Response

Memory

Tier 0

1464 req/sec

68 ms

96 ms

1506 bytes

1353 MB

Tier 1

2572 req/sec Источник: https://habr.com/ru/companies/beget/articles/854458/


Интересные статьи

Интересные статьи

1С-Битрикс официально присвоил Digital HR компания «Улей» максимальный уровень компетенции «Крупные корпоративные внедрения». Компетенция подтверждает профессиональные навыки компании в разработк...
The Pokemon Company проведёт расследование в отношении Pocketpair, создателя нашумевшей игры Palworld. Ранее игроки заметили в Palworld множество элементов, схожих с Pokemon.
Приветствую всех на Хабре. Я Сергей Михнов, CEO компании Пуск, интегратора Битрикс24. В свое время мы выбрали особый путь – специализацию на коробочной версии Битрикс24. В коммуникациях на сайте или п...
Привет! Меня зовут Саша Шутай, я тимлид в AGIMA. В прошлой статье я рассказывал, что делать, если на проекте Bitrix сожительствует с Vue.js и поисковые боты не видят контента сайта. А в этой помогу ра...
Примечание: полный исходный код проекта, а также пояснения о его использовании и чтении можно найти на Github [здесь]. Я сделал перерыв в своей работе над магистерской диссертацией, чтобы потр...