На medium.com есть ряд статей со сравнением простых web-сервисов, написанных на разных языках. Одна из них Go vs Rust: Performance comparison for JWT verify and MySQL query и судя по ней, Go на 42% быстрее чем Rust. Я решил перепроверить и заодно поменять Gin на Fiber, Axis на Axum и MySQL на PostgreSQL.

Web-сервис будет принимать запрос с аутентификацией по токену JWT, искать в БД пользователя с данным email из JWT и возвращать его в виде json. Так как подобная аутентификация используется повсеместно, то тест актуальный.

Сперва готовим тестовую БД. Это будет PostgreSQL и разворачивать ее будем в Docker через compose. В папке, в которой будет наша БД, создаем файл init.sql. В нем мы создаем новую базу и в ней таблицу users:

CREATE DATABASE testbench;

\connect testbench;

CREATE TABLE users(
    email VARCHAR(255) NOT NULL PRIMARY KEY,
    first VARCHAR(255),
    last VARCHAR(255),
    country VARCHAR(255),
    city VARCHAR(255),
    age int
);

Далее там же создаем папку db и файл docker-compose.yaml следующего содержания:

services:
  postgres:
    image: postgres:alpine
    environment:
      - POSTGRES_PASSWORD=123456
    volumes:
      - ./db:/var/lib/postgresql/data
      # скрипт ниже выполнится при первом создании базы
      - ./init.sql:/docker-entrypoint-initdb.d/init.sql
    ports:
      - "5432:5432"

Создаем и запускаем контейнер:

$ docker-compose up

Далее нам нужно наполнить базу 100 000 записями. Для этого нужен генератор данных, в качестве которого используем Synth. Создаем файл с настройками генератора (обратите внимание, что email у нас первичный уникальный ключ):

{
    "type": "array",
    "length": {
        "type": "number",
        "constant": 1
    },
    "content": {
        "type": "object",
        "email": {
            "type": "unique",
            "content": {
                "type": "string",
                "faker": {
                    "generator": "free_email"
                }
            }
        },
        "first": {
            "type": "string",
            "faker": {
                "generator": "first_name"
            }
        },
        "last": {
            "type": "string",
            "faker": {
                "generator": "last_name"
            }
        },
        "city": {
            "type": "string",
            "faker": {
                "generator": "city_name"
            }
        },
        "country": {
            "type": "string",
            "faker": {
                "generator": "country_name"
            }
        },
        "age": {
            "type": "number",
            "subtype": "i32",
            "range": {
                "low": 18,
                "high": 55,
                "step": 1
            }
        }
    }
}

Запускаем генератор:

$ synth generate ./ --to postgres://postgres:123456@localhost:5432/testbench --size 100000

БД готова, пишем сами веб-сервисы.

Сначала на Go:
package main

import (
	"bufio"
	"database/sql"
	"log"
	"os"
	"strings"
	"time"

	"github.com/gofiber/fiber/v2"
	"github.com/golang-jwt/jwt/v5"
	_ "github.com/lib/pq"
)

type MyCustomClaims struct {
	Email string `json:"email"`
	jwt.RegisteredClaims
}

type User struct {
	Email  string
	First  string
	Last   string
	City   string
	Country string
	Age    int
}

var jwtSecret = "mysuperPUPERsecret100500security"

func getToken(c *fiber.Ctx) string {
	hdr := c.Get("Authorization")
	if hdr == "" {
		return ""
	}

	token := strings.Split(hdr, "Bearer ")[1]
	return token
}

func main() {
	app := fiber.New()
	db, err := sql.Open("postgres", "user=postgres password=123456 dbname=testbench sslmode=disable")

	if err != nil {
		return
	}

	defer db.Close()

	db.SetMaxOpenConns(10)
	db.SetMaxIdleConns(10)

	app.Get("/", func(c *fiber.Ctx) error {
		tokenString := getToken(c)
		if tokenString == "" {
			return c.SendStatus(fiber.StatusUnauthorized)
		}
		token, err := jwt.ParseWithClaims(tokenString, &MyCustomClaims{}, func(token *jwt.Token) (interface{}, error) {
			return []byte(jwtSecret), nil
		})

		if err != nil {
			log.Println(err)
			return c.SendStatus(fiber.StatusUnauthorized)
		}

		claims := token.Claims.(*MyCustomClaims)

		query := "SELECT * FROM users WHERE email=$1"
		row := db.QueryRow(query, claims.Email)

		var user User = User{}
		err2 := row.Scan(&user.Email, &user.First, &user.Last, &user.Country, &user.City, &user.Age)
		if err2 == sql.ErrNoRows {
			return c.SendStatus(fiber.StatusNotFound)
		}
		if err2 != nil {
			log.Println(err2)
			return c.SendStatus(fiber.StatusInternalServerError)
		}

		return c.JSON(user)
	})

	//вспомогательная ручка
	app.Get("/randomtoken", func(c *fiber.Ctx) error {
		file, err := os.Create("tokens.txt")
		if err != nil {
			log.Println(err)
			return c.SendStatus(fiber.StatusInternalServerError)
		}

		writer := bufio.NewWriter(file)

		rows, err := db.Query("SELECT * FROM USERS OFFSET floor(random() * 100000) LIMIT 10")
		if err != nil {
			return c.SendStatus(fiber.StatusInternalServerError)
		}

		for rows.Next() {
			var user User
			err = rows.Scan(&user.Email, &user.First, &user.Last, &user.Country, &user.City, &user.Age)
			if err != nil {
				log.Println(err)
				return c.SendStatus(fiber.StatusInternalServerError)
			}

			claims := MyCustomClaims{
				user.Email,
				jwt.RegisteredClaims{
					ExpiresAt: jwt.NewNumericDate(time.Now().Add(24 * time.Hour)),
				},
			}

			token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
			ss, err := token.SignedString([]byte(jwtSecret))

			if err != nil {
				log.Println(err)
				return c.SendStatus(fiber.StatusInternalServerError)
			}

			_, err = writer.WriteString(ss + "\n")
			if err != nil {
				file.Close()
				log.Println(err)
				return c.SendStatus(fiber.StatusInternalServerError)
			}

		}

		writer.Flush()
		file.Close()

		return c.SendFile(file.Name())
	})

	log.Fatal(app.Listen(":3000"))
}

Теперь на Rust
use axum::{
    extract::State,
    http::{header::AUTHORIZATION, HeaderMap, StatusCode},
    response::IntoResponse,
    routing::get,
    Json, Router,
};
use jsonwebtoken::{decode, Algorithm, DecodingKey, Validation};
use serde::{Deserialize, Serialize};
use sqlx::{postgres::PgPoolOptions, Pool, Postgres};

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

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

type ConnectionPool = Pool<Postgres>;

async fn root(headers: HeaderMap, State(pool): State<ConnectionPool>) -> impl IntoResponse {
    let jwt_secret = "mysuperPUPERsecret100500security";
    let validation = Validation::new(Algorithm::HS256);

    let auth_header = headers.get(AUTHORIZATION).expect("no authorization header");
    let mut auth_hdr: &str = auth_header.to_str().unwrap();
    auth_hdr = &auth_hdr.strip_prefix("Bearer ").unwrap();

    let token = match decode::<Claims>(
        &auth_hdr,
        &DecodingKey::from_secret(jwt_secret.as_ref()),
        &validation,
    ) {
        Ok(c) => c,
        Err(e) => {
            println!("Application error: {e}");
            return (StatusCode::INTERNAL_SERVER_ERROR, "invalid token").into_response();
        }
    };

    let email = token.claims.email;
    let query_result: Result<User, sqlx::Error> =
        sqlx::query_as(r#"SELECT *  FROM USERS WHERE email=$1"#)
            .bind(email)
            .fetch_one(&pool)
            .await;

    match query_result {
        Ok(user) => {
            return (StatusCode::ACCEPTED, Json(user)).into_response();
        }
        Err(sqlx::Error::RowNotFound) => {
            return (StatusCode::NOT_FOUND, "user not found").into_response();
        }
        Err(_e) => {
            println!("error: {}", _e.to_string());
            return (StatusCode::INTERNAL_SERVER_ERROR, "error").into_response();
        }
    };
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let database_url = String::from("postgres://postgres:123456@localhost/testbench");
    let pool = PgPoolOptions::new()
        .max_connections(10)
        .connect(&database_url)
        .await
        .expect("can't connect to database");

    println!("DB connect success");

    let app = Router::new()
        .route("/", get(root))
        .with_state(pool);

    let listener = tokio::net::TcpListener::bind("127.0.0.1:3000")
        .await
        .unwrap();
    axum::serve(listener, app).await?;
    Ok(())
}

// NOTE ----
// Rust code has been built in release mode for all performance tests

Для Go собираем исполняемый файл командой: go build, для Rust: cargo build --release

Далее собственно тестирование:

ПК: Windows 11 22H2, Intel(R) Core(TM) i5-10400 CPU @ 2.90GHz, 32 Гб.

Go 1.21.4

Rust 1.74.0

Делать будем 100К запросов при 10,50 и 100 одновременных подключениях.

Используем Cassowary. Получаем 10 "случайных" токенов запустив веб-сервис на Go: http://127.0.0.1:3000/randomtoken

Выбираем любой и запускаем тесты (подставив свой токен):

$ cassowary.exe run -u http://127.0.0.1:3000 -c 10 -n 100000 -H "Authorization:Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6ImFsZWphbmRyaW5fZG9sb3JlbXF1ZUBnbWFpbC5jb20iLCJleHAiOjE3MDEzMDYyMTl9.5mT3KVV9Q69yd5gx-z97LVr6tgNA1yVJxpeJEXSq6U0"

Делаем по 3 запуска и берем максимальный результат.

Результаты Go:

Go 10
Go 10
Go 50
Go 50
Go 100
Go 100

Результаты Rust:

Rust 10
Rust 10
Rust 50
Rust 50
Rust 100
Rust 100

Разница между 10,50 и 100 одновременными подключениями в Go небольшая, а в Rust вообще нет. Это связанно с тем, что количество соединений в пуле БД ограничено 10-тью. Так было в изначальном сравнении и я не стал это менять (все равно соединений в пуле всегда в разы меньше чем запросов).

Итог

Go быстрее Rust на 35% в данном сценарии.

В Rust я пробовал менять sqlx на tokio-postgres, убирал расшифровку JWT, результат тот же.

Ссылка на Github для желающих проверить на своих серверах.

UPD

Причина низкой производительности Rust оказалась в библиотеке sqlx. Замена sqlx на diesel сравняла Go и Rust. (в Go тоже поменял pq на pgx). А вот памяти Go использовал в 3 раза больше чем Rust.

Go 50
Go 50
Rust 50
Rust 50

Вывод

В обоих языках постоянно появляются новые, более производительные библиотеки. Например на данный момент для Rust предпочтительнее ntex, для Go можно взять gnet (правда это только замена пакету net, а не полноценный фреймворк). На обоих языках можно сделать отличную оптимизацию, писать unsafe код. На Go легче писать, ошибки компилятора понятнее. Rust более гибкий, но часто не понятные ошибки компиляции и многословное описания ограничений generics, зато быстрее и использует меньше памяти.

Бонус

Сделал еще сравнение CPU bounds задачи: вычисление соли для получения хэша md5 с четырьмя нулями в конце. Код для Go взял из статьи на Хабре (однопоток) и немного оптимизировал, сделал по аналогии в Rust. Rust оказался быстрее на 65% чем Go

Go
package main

import (
	"crypto/md5"
	"fmt"
	"time"
)

func getHashSync(in string) (string, int) {
	var salt int
	for {
		if h, ok := getHashIf(in, salt); ok {
			return h, salt
		}
		salt++
	}
}

func getHashIf(in string, salt int) (string, bool) {
	if x := md5.Sum([]byte(fmt.Sprintf("%d.%s", salt, in))); isHashAcceptable(x) {
		return fmt.Sprintf("%x", x), true
	}
	return "", false
}

func isHashAcceptable(x [16]byte) bool {
	return x[15] == 0 && x[14] == 0 && x[13] == 0 && x[12] == 0
}

const phrase = "Это проврека"

func main() {
	fmt.Println("Start...")
	defer func(t time.Time) {
		fmt.Printf("done with %v\n", time.Since(t))
	}(time.Now())

	var hash, salt = getHashSync(phrase)
	fmt.Printf("%s salt=%d\n", hash, salt)
}

Rust
use md5;
use std::time::Instant;

fn get_hash_sync(input: &str) -> (String, u32) {
    let mut salt: u32 = 0;
    loop {
        if let Some(hash) = get_hash_if(input, salt) {
            return (hash, salt);
        }
        salt += 1;
    }
}

fn get_hash_if(input: &str, salt: u32) -> Option<String> {
    let digest: md5::Digest = md5::compute(format!("{}.{}", salt, input).as_bytes());

    if is_hash_acceptable(digest) {
        return Some(format!("{:x}", digest));
    }
    None
}

fn is_hash_acceptable(bytes: md5::Digest) -> bool {
    bytes[15] == 0 && bytes[14] == 0 && bytes[13] == 0 && bytes[12] == 0
}

const PHRASE: &str = "Это проврека";

fn main() {
    println!("Start...");
    let start_time = Instant::now();

    let (hash, salt) = get_hash_sync(PHRASE);
    println!("{} salt={}", hash, salt);

    let elapsed: std::time::Duration = start_time.elapsed();
    let minutes = elapsed.as_secs() / 60;
    let seconds = elapsed.as_secs() % 60;
    println!("done with {:02}m{:02}s", minutes, seconds);

    let mut input = String::new();
    let _ = std::io::stdin().read_line(&mut input);
}

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


  1. tessob
    29.11.2023 03:53
    -8

    Ура! Надеюсь, что все кто пришел за “blazingly fast” уйдут наконец писать на Go и пена немного сойдёт.

    Автору могу порекомендовать попробовать свои силы на TechEmpower, а то в тамошних состязаниях Go что-то совсем не вывозит. Кто-то должен это пресечь.


    1. Bromles
      29.11.2023 03:53
      +5

      Только почему-то в реальной жизни это работает в обратную сторону, и компании после переноса своего прода с Go на Rust наблюдают 60% прироста в скорости (Cloudflare, Discord и прочие, и это только те, кто сказал об этом публично и был достаточно популярен, чтобы это заметили).

      В целом тут не очень оптимально бенч для раста сделан, как минимум один sqlx чего стоит

      Мб на досуге покручу код, если время будет


      1. micronull
        29.11.2023 03:53
        -1

        компании после переноса своего прода с Go на Rust наблюдают 60% прироста в скорости

        Уже есть ряд решений, которые могут решить одну из причин, а именно:

        Ну и нужно знать про различные хаки, как например: _ = some[n], где n - последний элемент слайса.


        1. domix32
          29.11.2023 03:53

          про различные хаки

          как минимум хотелось бы выводить такое из разряда "хаков" в идиоматический код.


        1. domix32
          29.11.2023 03:53
          +1

          Кстати, вторая проблема го - это то, как скалируют нагрузку на приложения. Когда достигается некоторый лимит добавляется больше инстансов приложения, а не больше места в тредпуле. В итоге бОльшая часть профита от зелёных тредов в какой-то момент перестаёт существовать, т.к. приложение фактически приложение работает на одном ядре.


      1. domix32
        29.11.2023 03:53
        +2

        Прирост получается часто из-за того что GC больше не беспокоит исполнение. На такой нагрузке они наверняка и не почуяли бы memory pressure. Поднять RPS до 10к+ и посмотреть где начнётся просадка. Опять же местные тесты никак не рисовали, что там с памятью происходит и сколько каждый инстанс кушает памяти. На 100 соединениях зелёные треды вполне ожидаемо выигрывают более тяжеловестным ржавым.


        1. itmind Автор
          29.11.2023 03:53

          Запустил с параметрами -c 300 -n 100000

          Go: 10489 Req/s

          Rust: 4327 Req/s

          В чем то у Rust явно проблема, возможно в sqlx. Переделаю на diesel и проверю еще раз.


          1. domix32
            29.11.2023 03:53
            +1

            Какой-нибудь flamefraph натравить и посмотреть на что тратится раст.


    1. Ilay_Developer
      29.11.2023 03:53
      +1

      В проде всё совсем иначе. С Go переходят на Rust. Да и Go - это упор на Web. Rust - мультипродуктовый язык. Но главная особенность - это замена C++ в критических местах.


      1. PrinceKorwin
        29.11.2023 03:53
        +1

        Ещё ниша у Rust это замена C где пишут либы для более высокоуровневых языков (PHP, Python например).

        Результат получается по скорости такой же, но код стабильнее и безопаснее.


  1. IkaR49
    29.11.2023 03:53
    +1

    Интересно, почему среднее и медиана на Go меньше, чем на расте, но 95-й перцентиль намного больше, чем на Расте? Это как так?


    1. Deosis
      29.11.2023 03:53
      +2

      Возможно, в 95 перцентиль попала сборка мусора


  1. PrinceKorwin
    29.11.2023 03:53
    +5

    Хм. Судя по коду у Rust первые 10 запросоа идут в пустой пул коннектов и ждёт подключения. А в коде на Go первый запрос пойдет в БД с уже подготовленным коннектор.

    Вполне возможно это и даёт выигрыш. Создание коннекта - медленная операция.


    1. Gorthauer87
      29.11.2023 03:53
      +1

      Тогда получается, что надо вначале прогреть сервис, а потом его уже измерять? В целом, это же звучит логично. Плюс, наверное, надо мерить скорость на Linux машинах. Там же обычно весь бекенд, которому скорость нужна, крутится.


    1. itmind Автор
      29.11.2023 03:53

      Если этот так, то можно как то в Rust подготовить заранее подключения не выполняя запросов?


      1. PrinceKorwin
        29.11.2023 03:53
        +1

        https://docs.rs/sqlx/latest/sqlx/pool/struct.PoolOptions.html#method.min_connections


    1. itmind Автор
      29.11.2023 03:53
      +1

      Скорее всего не в этом дело. Я запускал сервис и последовательно выполнял по 3 замера с 10,50 и 100 соединениями не перезапуская сервис (9 замеров). Таким образом пул бы "прогрелся" уже после первого замера


      1. PrinceKorwin
        29.11.2023 03:53
        +1

        Тогда, возможно, стоит посмотреть в сторону используемых крейтов. Как пример: sqlx in some cases more than 60 times slower than Diesel

        Также посмотреть сколько суммарно логов генерируется. Они тоже хорошо так на производительность влияют.


  1. Kano
    29.11.2023 03:53
    +2

    Проверка скорости работы по умолчанию настроенных библиотек


    1. itmind Автор
      29.11.2023 03:53

      Но ведь "настройка" работает для обеих сторон. Т.е. если "донастроить" например библиотеки Go, то он получит еще большее преимущество?


      1. DarkEld3r
        29.11.2023 03:53

        Может да, может нет, если дефолтные настройки удачнее выбраны.


  1. amf1k
    29.11.2023 03:53
    +1

    Библиотека https://github.com/lib/pq для Go, не рекомендуема для использования, попробуйте использовать https://github.com/jackc/pgx (причем именно pgxpool)

    Hidden text

    For users that require new features or reliable resolution of reported bugs, we recommend using pgx which is under active development.


    1. itmind Автор
      29.11.2023 03:53

      После замены lib/pq на pgxpool показатели увеличились на 52% до Req/s: 9193.11


  1. TimurBaldin
    29.11.2023 03:53
    +5

    Сорян, но это сравнение теплого с мягким. Так как языки кардинально отличаются по своим целям.

    Если у вас высокая нагрузка и вам нужно её держать и есть ограничения по памяти и времени отклика, ничего лучше С++ и Rust нет.

    Если для вас не так важна память (а она сейчас очень дешёвая) и не критичны задержки, то выбирайте Java, C#, Python.

    А если нужен вспомогательный сервис, с минимум бизнес логики и достаточно высокими I/O нагрузками и у вас нет под рукой разработчиков на Rust или С++, то тут может подойти Go.

    P.S Бенчмарки обычно не гоняются на ПК


    1. itmind Автор
      29.11.2023 03:53

      Но на Go пишутся не вспомогательные сервисы, а высоконагруженные сложные системы. Например Kubernetes, prometheus, вроде Grafana.


      1. mosinnik
        29.11.2023 03:53
        +4

        но это же вспомогательные обслуживающие сервисы и там по факту ноль бизнес логики с точки зрения нормального бизнеса, так что человек прав)


      1. TimurBaldin
        29.11.2023 03:53
        +1

        Инструменты пишутся на разных языках, это далеко не всегда означает, что эти языки рационально было использовать в этих проектах.

        С точки зрения бизнеса, нанять профессиональную команду разработчиков на Go стоит кратно дороже (по деньгам и времени), чем аналогичную команду на python, java и т.д...А профит от команды гоферов не перевешивает затраты на них, сорян


        1. itmind Автор
          29.11.2023 03:53
          +2

          Понятно, что бизнес диктует на чем писать и какой стек использовать. Но рынок Go разработчиков растет, а вот c Rust пока все плохо. Но если писать с нуля, в стартапе например, то выбирают обычно последние инструменты и технологии и в частности часто выбирают между Go и Rust. Java требует очень много памяти, python медленный и т.п. И может лучше вложиться немного в разработчиков, чем потом годами переплачивать за железо и бороться с тормозами.


          1. TimurBaldin
            29.11.2023 03:53

            "то выбирают обычно последние инструменты и технологии"

            Технология не должна быть последней или модной, она должна решать свою задачу с приемлемой для бизнеса степенью эффективности и обеспечивать низкую стоимость поддержки)

            "Java требует очень много памяти, python медленный и т.п."

            1) Память дешёвая, а труд разработчиков дорогой . Java native image и виртуальные потоки если что помогут

            2) Python не медленный, на нём спокойно можно писать нагруженные приложения , благодаря своей гибкости, это язык прекрасно работает с нативными расширениями.

            "лучше вложиться немного в разработчиков, чем потом годами переплачивать за железо и бороться с тормозами"

            А может этим будут заниматься сами разработчики?Курсы ? я выше писал, что нанять квалифицированных прогеров, это сложная задача. А нанять, прогеров на "новых" языках это задача ещё сложнее.

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


          1. ogregor
            29.11.2023 03:53
            -1

            Готовить сани летом надо. Ну и потом канареечное развертывание. В какой то момент можно просто взять паузу по зарплате и пойти туда где меньше но интересующий стек. Зато когда все захотят синьора, можно будет без сильной конкуренции просить Овер, что покроет собой прошлые просадки


      1. domix32
        29.11.2023 03:53
        +1

        Вы кажется слабо понимается, что такое "высоконагруженные".


        1. itmind Автор
          29.11.2023 03:53

          Ну я думаю у Grafana Cloud и нагрузка большая. Или "высоконагруженные" это только Google да Яндекс?


          1. domix32
            29.11.2023 03:53
            +1

            all you have to do to begin creating dashboards and querying metrics data is to configure data sources

            Это работает с большими данными, но не обязано делать это в реалтайме. Если вы не делаете какой-нибудь тикеры крипты поверх графаны или если у вас в неё не смотрят несколько тысяч человек единовременно (зачем?), то врядли это можно назвать "высоконагруженным".

            Prometheus как БД возможно относится к высоконагруженным, но я не уверен, что оно способно работать на уровне скажем ClickHouse. Хотя, возможно, я как-то неправильно интерпретирую сферу их применения.


            1. Vamp
              29.11.2023 03:53
              +2

              Prometheus работает по pull модели. То есть он сам ходит по сервисам и собирает с них метрики в удобном для себя темпе. Это точно не хайлоад.

              И да, как и с графаной, хайлоадом это становится, когда "смотрят несколько тысяч человек единовременно (зачем?)".


    1. PrinceKorwin
      29.11.2023 03:53
      +1

      Если для вас не так важна память (а она сейчас очень дешёвая)

      Скажите это Apple. У них апгрейд памяти стоит как крыло от Боинга.

      Сорри за оффтоп. Просто наболело :)


      1. TimurBaldin
        29.11.2023 03:53

        Кажется у них всё стоит как сам боинг ))


        1. Tsimur_S
          29.11.2023 03:53

          Кроме Боинга, он с наценкой.


  1. amkartashov
    29.11.2023 03:53

    Непонятен смысл бенчмарков. Обычно бенчмарк должен дать ответ-сравнение нескольких альтернативных технологий. Чтобы потом можно было из них выбрать.

    Здесь же сравнивается одна конкретная программа с другой конкретной программой. Нельзя вычленить из бенчмарков роль сетевой задержки, роль библиотеки обработки json или драйвера базы данных и т.д. Таким образом нельзя ничего определённого сказать.

    Ещё непонятно, использовалась ли одна и та же база данных, или запуск постгреса и генерация данных производились заново.

    Ну и результаты бенчмарков скриншотиками - фу.

    Upd: вполне может быть что сравнивались не программы а prepared vs non-prepared statements. Я бы переделал на явные prepared statement которые инициализируются один раз.


    1. n0dwis
      29.11.2023 03:53
      +1

      Я бы предложил наоборот - non-prepared statement. Поскольку они привязываются к соединению, а в случае пула соединений не факт, что следующий запрос будет использовать это же prepared statement.


    1. itmind Автор
      29.11.2023 03:53

      Смысл в том, что вот думает разработчик на чем писать новый проект, выбирает из нескольких вариантов, а для объективного выбора не хватает информации - люди пишут одно, в тестах выходит другое. И вот чем больше будет разных сравнений, тем лучше.

      Результаты скриншотами мне показались красивее, чем просто текст с терминала копировать. Или вы про то, что нужно графики сделать?


      1. amkartashov
        29.11.2023 03:53
        +2

        Данные бенчмарки сравнивают ваши две конкретные программы целиком, а не Rust vs Go или sqlx::postgres vs github.com/lib/pq - чем это поможет разработчику?

        Текстовая информация с картинки не копируется, не индексируется поисковиком.


        1. itmind Автор
          29.11.2023 03:53

          Языки сравниваются через программы на них написанные. В данной статье сравниваются стеки Fiber-lib/pq vs Axum-sqlx как одни из самых популярных библиотек для разработки web-сервисов. Возможно кому то это сравние даст дополнительную пищу для размышлений и сделает перевес в ту или иную сторону.


          1. amkartashov
            29.11.2023 03:53
            +4

            Пока что я вижу сравнение уровня одного и того же программиста (no offence, мой уровень наверняка хуже) в разных языках/фреймворках.


      1. dsh2dsh
        29.11.2023 03:53

        а для объективного выбора не хватает информации

        Как будто этот бенчмарк - для объективного выбора. Искусственные бенчмарки показывают радиус сферы коня и ничего более. К реальной жизни и реальным задачам - всё это не имеет никакого отношения.


  1. V1tol
    29.11.2023 03:53

    Есть очень простой способ проверить от чего зависит скорость в Rust версии - от оверхеда библиотек или он всё же упирается в I/O. Можно добавить LTO в релизную сборку и прогнать тесты ещё раз.

    [profile.release]
    codegen-units = 1
    lto = true

    Ещё я использую для синтетических тестов на локальной машине wrk - самый производительный генератор трафика, который я пробовал. Не знаю, какая у вас машина, но может так быть, что Cassowary отбирает у сервисов процессорное время и это влияет на результаты.


  1. beho1der
    29.11.2023 03:53
    +1

    Не понятно что все придираются,стандартные стеки\библиотеки. В начале статья я рассчитывал на паритет,не понятно почему go лидер...


    1. orekh
      29.11.2023 03:53
      +2

      Все придираются к тому, что автор не узнал в чём же причина разницы в скорости.


  1. damal
    29.11.2023 03:53
    +4

    Похоже, что Golang JWT decoder не делает `verifySignature`, как это делает Rust.
    Т.е. вместо `return []byte(jwtSecret), nil` должно быть что-то более сложное (например `ParseRSAPublicKeyFromPEM`).


    1. aegoroff
      29.11.2023 03:53

      автор вроде написал в конце "В Rust я пробовал менять sqlx на tokio-postgres, убирал расшифровку JWT, результат тот же.", говорит не помогло


  1. vasyash
    29.11.2023 03:53
    +7

    @itmind, проблема вся в том, что дизель каждый раз перед "селектом" делает тестирование соединения, то есть обращается к базе в два раза чаще. Чтобы этого избежать нужно поправить код так, как я сделал ниже. Кстати в pgx просто даже настройки такой не нашёл.

    После настройки разница в скорости около 10%

    UPD. протестировал в сотню потоков, Rust даже обогнал Go

    let pool = Pool::builder()
            .max_size(10)
            .min_idle(Some(10))
            .test_on_check_out(false) //!!!!!!
            .build(manager)
            .expect("can't connect to database");


    1. medigor
      29.11.2023 03:53
      +2

      Да, помогает здорово, у меня GO 25k rps, Rust 30k rps, на 20% быстрее.
      а если переделать на юниксокет, и lto=true, то раст становится на 40% быстрее (31.8k vs 44.6k)

      Но тест все равно неправильный, надо в контейнерах с лимитами запускать, а лучше на выделенных тачках или виртуалках.

      И дизель там синхронный.


    1. Pitou
      29.11.2023 03:53

      Я не поленился, переписал пример на tokio-postgres. Получил разницу производительности в 10% на 50 пользователях. Впрочем, оба приложения грузят процессор на 15% и упираются не то в контейнер с БД, не то в контейнер с cassowary. Но вариант на rust кушает 9мб памяти, а go целых 19мб. Необоснованно много, я считаю. В общем и целом паритет.


      1. vasyash
        29.11.2023 03:53

        Обоснованно вполне. Разные механизмы работы с памятью. В golang работает GC, а в раст всё завязано на механизм владения, и кажется благодаря этому в куче "мусор" никогда не собирается.


      1. itmind Автор
        29.11.2023 03:53

        У меня на tokio-postgres показатели почему то как на sqlx. Какой пул использовали или без пула?


        1. Pitou
          29.11.2023 03:53
          +1

          deadpool-postgres для пула. Но я не настоящий растовщик, почему выбрал его и что есть ещё не скажу. А вот tokio-posgtres выбирал вполне осознанно - так подход получается 1 в 1 как с go, без ORM и прокладок (даже если они бесплатные, как дизель). Разве что есть важный нюанс - tokio-postgres не использует prepared statements по умолчанию, а в go либе они используются всегда, я даже не уверен что их можно отключить.


          1. itmind Автор
            29.11.2023 03:53

            Добавил в git стек ntex + deadpool-postgres + tokio-posgtres (с кешированием statements).

            Без lto проигрывает Go на 0,5%, c lto выигрывает 0,5%. В общем на фоне погрешности. Замена axum на ntex преимущества не дало.

            Преимущество Rust только в потреблении памяти получается.


    1. itmind Автор
      29.11.2023 03:53

      Изменил указанный параметр и провел замеры. По итогу Go и Rust сравнялись у меня. Дополнил статью.


      1. PrinceKorwin
        29.11.2023 03:53

        Мир-Дружба-Жевачка! :)

        А вообще я давно понял, что делать бенчи очень сложно. И если получился неожиданный результат, то дело 146% в неправильных тестах.

        Делать перформанс тесты - это целая наука.


  1. titan_pc
    29.11.2023 03:53
    -1

    Человек пошёл и доказал давно известную историю, что бенчмарки забугорья, не привязанные к реальности - показывают только то, что разогнать Rust быстрее чем Go способна лишь малая часть айтишного населения. Которая просто смогла и хотела сделать именно это. А если просто взять либы, прикрутить одно к другому, то производительность будет средняя, и даже хуже, если взять что-то не то.


    1. PrinceKorwin
      29.11.2023 03:53

      С чего бы это? Каждый язык предоставляет несколько разных библиотек для решения одной и той же задачи. Какие-то фичастее, какие-то быстрее, другие медленнее, но удобней в использовании. Выбор это хорошо.

      В текущем примере автор выбрал не самую быструю библиотеку.

      Это никак не означает, что только малое количество программистов на Rust способны "разогнать" скорость путем выбора другого крейта. :)


  1. n0dwis
    29.11.2023 03:53

    Вчера поигрался с кодом - уже с pgx и diesel, но ещё без тюнинга. Получил такие результаты - rust - 35k rps, go - 42k rps.

    Но, когда добавил замеры времени работы именно http хендлера, то в обоих случаях время выполнения было в пределах 120-205 мкс. Т.е., похоже, на скорость, в основном, влияет Axum, а не sqlx или парсинг jwt. А вот настройка diesel эту разницу компенсирует.


    1. medigor
      29.11.2023 03:53

      да уже есть ответ, в чем проблема:
      https://habr.com/ru/articles/777072/comments/#comment_26212898


  1. alexfilus
    29.11.2023 03:53
    +1

    Помимо количества и времени работы запросов хотелось бы увидеть потребление процессора и памяти.
    Сдаётся мне что в условиях достаточного количества коннектов к пг, но не достаточного количества процов Rust обгонит Go.