Перейти к содержанию

Deno vs Rust - Сравнение производительности для проверки JWT и запроса MySQL

Posted on:12 июня 2023 г. at 07:53

Example Dynamic OG Image link

Введение

После публикации рекордного количества статей о сравнении производительности различных технологий, таких как Node.js, Deno, Bun, Rust, Go, Spring, Python и т.д. для простого hello world case, я последовательно получал комментарии, что статьи были хороши, но не были применимы непосредственно для реальных случаев использования. Меня попросили сделать то же самое для более «реальных» дел. Статьи также (и до сих пор) привлекли рекордное количество просмотров. Тем не менее, точка зрения была принята хорошо. Привет мир был лучшей отправной точкой, но точно не «реальный» случай.

Реальный сценарий использования

С этой статьи я начинаю новую серию, где я собираюсь сравнить ряд технологий для реального случая:

Это очень распространенный реальный случай. Для «Hello world» я видел технологии, предлагающие где-то от 70K до 200 тысяч RPS. RPS был высоким, потому что все, что делало приложение, возвращало простую строку. Конечно, мы не будем ожидать 200K RPS для сценария использования JWT + MySQL. Сколько мы получим, пока неясно.

В этой статье сравнивается Deno & Rust для данного случая использования. Это интересное сравнение, потому что Deno интерпретируется, в то время как Rust компилируется в машинный код. Это очень интересно еще и потому, что сам Deno написан на Rust. Кроме того, проверка JWT является интенсивной операцией ЦП. Скомпилированный язык должен быть быстрее, чем интерпретированный? Не так ли? Мы это узнаем очень скоро.

Испытательная установка

Все тесты выполняются на MacBook Pro M1 с 16G оперативной памяти.

Версии программного обеспечения:

На стороне Deno я использую Коа. Koa - это новый веб-фреймворк, разработанный командой, стоящей за Express, который призван быть меньшим, более выразительным и более надежным фундаментом для веб-приложений и API. Другими библиотеками на стороне Deno являются jsonwebtoken для проверки и декодирования JWT и mysql2 для выполнения запросов MySQL.

На стороне Rust я использую веб-фреймворк Actix. Другие пакеты, которые я использую: jsonwebtoken для проверки и декодирования JWT, и sqlx для выполнения запросов MySQL.

Тестер нагрузки HTTP построен на основе libcurl. Существует предварительно созданный список 100 000 JWT. Тестер выбирает случайные JWT и отправляет их в заголовок Authorization запроса HTTP.

База данных MySQL содержит таблицу users, которая содержит 6 столбцов:

mysql> desc users;
+--------+--------------+------+-----+---------+-------+
| Field  | Type         | Null | Key | Default | Extra |
+--------+--------------+------+-----+---------+-------+
| email  | varchar(255) | NO   | PRI | NULL    |       |
| first  | varchar(255) | YES  |     | NULL    |       |
| last   | varchar(255) | YES  |     | NULL    |       |
| city   | varchar(255) | YES  |     | NULL    |       |
| county | varchar(255) | YES  |     | NULL    |       |
| age    | int          | YES  |     | NULL    |       |
+--------+--------------+------+-----+---------+-------+
6 rows in set (0.00 sec)

Таблица пользователей предварительно заполнена записями 100 КБ:

mysql> select count(*) from users;
+----------+
| count(*) |
+----------+
|    99999 |
+----------+
1 row in set (0.01 sec)

Для каждого сообщения электронной почты, присутствующего в JWT, имеется соответствующая запись пользователя в базе данных MySQL.

Код

Deno

import Koa from "koa";
import Router from "@koa/router";
import jwt from "jsonwebtoken";
import mysql from "mysql2/promise";
import process from "node:process";

const app = new Koa();
const router = new Router();

const connection = await mysql.createConnection({
  host: "localhost",
  user: "dbuser",
  password: "dbpwd",
  database: "testdb",
});

const jwtSecret = process.env.JWT_SECRET;

function getToken(headers) {
  if (
    headers &&
    headers.authorization &&
    headers.authorization.startsWith("Bearer ")
  ) {
    return headers.authorization.split(" ")[1];
  }
}

router.get("/", async (ctx, next) => {
  const rcvdJwt = getToken(ctx.request.headers);
  let email;
  try {
    const payload = jwt.verify(rcvdJwt, jwtSecret);
    email = payload.email;
  } catch (e) {
    return (ctx.response.status = 401);
  }

  const [rows] = await connection.query(
    `SELECT * FROM USERS WHERE EMAIL = '${email}' LIMIT 1`
  );
  ctx.response.body = rows[0];
});

app.use(router.routes()).use(router.allowedMethods());

app.listen(3000);

Rust

use actix_web::{web, get, App, HttpServer, HttpRequest, HttpResponse, Responder};
use serde::{Serialize, Deserialize};
use jsonwebtoken::{decode, Validation, DecodingKey, Algorithm};
use dotenv::dotenv;
use sqlx::mysql::{MySqlPool, MySqlPoolOptions};

#[derive(Debug, Serialize, Deserialize)]
struct Claims {
    iat: usize,
    email: String
}

#[derive(Debug, Deserialize, Serialize, sqlx::FromRow)]
#[allow(non_snake_case)]
pub struct User {
    pub email: String,
    pub first: Option<String>,
    pub last: Option<String>,
    pub city: Option<String>,
    pub county: Option<String>,
    pub age: Option<i32>
}

pub struct AppState {
    db: MySqlPool,
    jwt_secret: String
}

#[get("/")]
async fn get_user(
    req: HttpRequest,
    data: web::Data<AppState>,
) -> impl Responder {

    let validation = Validation::new(Algorithm::HS256);
    let mut auth_hdr: &str = req.headers().get(actix_web::http::header::AUTHORIZATION).unwrap().to_str().unwrap();
    auth_hdr = &auth_hdr.strip_prefix("Bearer ").unwrap();
    let token = match decode::<Claims>(&auth_hdr, &DecodingKey::from_secret(data.jwt_secret.as_ref()), &validation) {
      Ok(c) => c,
      Err(e) => {
        eprintln!("Application error: {e}");
        return HttpResponse::InternalServerError().into()
      }
    };

    let email: String = token.claims.email;
    let query_result = sqlx::query_as!(User, r#"SELECT *  FROM USERS WHERE email = ?"#, email)
        .fetch_one(&data.db)
        .await;

    match query_result {
        Ok(user) => {
            let user_response = serde_json::json!(user);
            return HttpResponse::Ok().json(user_response);
        }
        Err(sqlx::Error::RowNotFound) => {
            return HttpResponse::NotFound().into();
        }
        Err(_e) => {
            return HttpResponse::InternalServerError().into();
        }
    };
}

#[actix_web::main]
async fn main() -> std::io::Result<()> {

    dotenv().ok();
    env_logger::init();
    let database_url = std::env::var("DATABASE_URL").expect("DATABASE_URL must be set");
    let pool = match MySqlPoolOptions::new()
    .max_connections(300)
    .connect(&database_url)
    .await
    {
        Ok(pool) => pool,
        Err(err) => {
            println!("🔥 Failed to connect to the database: {:?}", err);
            std::process::exit(1);
        }
    };


    HttpServer::new(move || {
        App::new()
            .app_data(web::Data::new(AppState { db: pool.clone(),
                jwt_secret: std::env::var("JWT_SECRET").unwrap() }))
            .service(get_user)
    })
    .bind(("127.0.0.1", 3000))?
    .run()
    .await
}

Код Rust компилируется в режиме выпуска с помощью опции - release.

Результаты

Каждый тест выполняется для 500K запросов в целом. Уровни параллелизма - 10, 50 и 100 соединений. Перед проведением измерений выдается запрос на разогрев 1K.

Вот диаграммы с результатами:

Анализ

Во-первых, RPS значительно падает от высокого уровня hello world use case. Для простого варианта hello world Deno предложил ~ 55K RPS, в то время как Rust/Actix достиг ~ 175K RPS. В этом реальном случае Deno предлагает ~ 5K RPS, а Rust - ~ 15K RPS. Реальность сильно тяжела.

В целом, Rust бьет Deno по всем измерениям. Rust предлагает в три раза больше RPS по сравнению с Deno. Загрузка процессора Rust довольно высока, в то время как использование памяти минимально. Мы действительно ожидали, что Deno превзойдет Rust? Совсем нет!

Победа: Rust