На 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:
Результаты Rust:
Разница между 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.
Вывод
В обоих языках постоянно появляются новые, более производительные библиотеки. Например на данный момент для 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)
PrinceKorwin
29.11.2023 03:53+5Хм. Судя по коду у Rust первые 10 запросоа идут в пустой пул коннектов и ждёт подключения. А в коде на Go первый запрос пойдет в БД с уже подготовленным коннектор.
Вполне возможно это и даёт выигрыш. Создание коннекта - медленная операция.
Gorthauer87
29.11.2023 03:53+1Тогда получается, что надо вначале прогреть сервис, а потом его уже измерять? В целом, это же звучит логично. Плюс, наверное, надо мерить скорость на Linux машинах. Там же обычно весь бекенд, которому скорость нужна, крутится.
itmind Автор
29.11.2023 03:53Если этот так, то можно как то в Rust подготовить заранее подключения не выполняя запросов?
PrinceKorwin
29.11.2023 03:53+1https://docs.rs/sqlx/latest/sqlx/pool/struct.PoolOptions.html#method.min_connections
itmind Автор
29.11.2023 03:53+1Скорее всего не в этом дело. Я запускал сервис и последовательно выполнял по 3 замера с 10,50 и 100 соединениями не перезапуская сервис (9 замеров). Таким образом пул бы "прогрелся" уже после первого замера
PrinceKorwin
29.11.2023 03:53+1Тогда, возможно, стоит посмотреть в сторону используемых крейтов. Как пример: sqlx in some cases more than 60 times slower than Diesel
Также посмотреть сколько суммарно логов генерируется. Они тоже хорошо так на производительность влияют.
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.
itmind Автор
29.11.2023 03:53После замены lib/pq на pgxpool показатели увеличились на 52% до Req/s: 9193.11
TimurBaldin
29.11.2023 03:53+5Сорян, но это сравнение теплого с мягким. Так как языки кардинально отличаются по своим целям.
Если у вас высокая нагрузка и вам нужно её держать и есть ограничения по памяти и времени отклика, ничего лучше С++ и Rust нет.
Если для вас не так важна память (а она сейчас очень дешёвая) и не критичны задержки, то выбирайте Java, C#, Python.
А если нужен вспомогательный сервис, с минимум бизнес логики и достаточно высокими I/O нагрузками и у вас нет под рукой разработчиков на Rust или С++, то тут может подойти Go.
P.S Бенчмарки обычно не гоняются на ПК
itmind Автор
29.11.2023 03:53Но на Go пишутся не вспомогательные сервисы, а высоконагруженные сложные системы. Например Kubernetes, prometheus, вроде Grafana.
mosinnik
29.11.2023 03:53+4но это же вспомогательные обслуживающие сервисы и там по факту ноль бизнес логики с точки зрения нормального бизнеса, так что человек прав)
TimurBaldin
29.11.2023 03:53+1Инструменты пишутся на разных языках, это далеко не всегда означает, что эти языки рационально было использовать в этих проектах.
С точки зрения бизнеса, нанять профессиональную команду разработчиков на Go стоит кратно дороже (по деньгам и времени), чем аналогичную команду на python, java и т.д...А профит от команды гоферов не перевешивает затраты на них, сорян
itmind Автор
29.11.2023 03:53+2Понятно, что бизнес диктует на чем писать и какой стек использовать. Но рынок Go разработчиков растет, а вот c Rust пока все плохо. Но если писать с нуля, в стартапе например, то выбирают обычно последние инструменты и технологии и в частности часто выбирают между Go и Rust. Java требует очень много памяти, python медленный и т.п. И может лучше вложиться немного в разработчиков, чем потом годами переплачивать за железо и бороться с тормозами.
TimurBaldin
29.11.2023 03:53"то выбирают обычно последние инструменты и технологии"
Технология не должна быть последней или модной, она должна решать свою задачу с приемлемой для бизнеса степенью эффективности и обеспечивать низкую стоимость поддержки)
"Java требует очень много памяти, python медленный и т.п."
1) Память дешёвая, а труд разработчиков дорогой . Java native image и виртуальные потоки если что помогут
2) Python не медленный, на нём спокойно можно писать нагруженные приложения , благодаря своей гибкости, это язык прекрасно работает с нативными расширениями.
"лучше вложиться немного в разработчиков, чем потом годами переплачивать за железо и бороться с тормозами"
А может этим будут заниматься сами разработчики?Курсы ? я выше писал, что нанять квалифицированных прогеров, это сложная задача. А нанять, прогеров на "новых" языках это задача ещё сложнее.
Поэтому "новые" языки, должны обеспечивать такие преимущества, чтобы эту задачку со звездочкой выгодно было решать...К сожалению, преимущества весьма слабые.
ogregor
29.11.2023 03:53-1Готовить сани летом надо. Ну и потом канареечное развертывание. В какой то момент можно просто взять паузу по зарплате и пойти туда где меньше но интересующий стек. Зато когда все захотят синьора, можно будет без сильной конкуренции просить Овер, что покроет собой прошлые просадки
domix32
29.11.2023 03:53+1Вы кажется слабо понимается, что такое "высоконагруженные".
itmind Автор
29.11.2023 03:53Ну я думаю у Grafana Cloud и нагрузка большая. Или "высоконагруженные" это только Google да Яндекс?
domix32
29.11.2023 03:53+1all you have to do to begin creating dashboards and querying metrics data is to configure data sources
Это работает с большими данными, но не обязано делать это в реалтайме. Если вы не делаете какой-нибудь тикеры крипты поверх графаны или если у вас в неё не смотрят несколько тысяч человек единовременно (зачем?), то врядли это можно назвать "высоконагруженным".
Prometheus как БД возможно относится к высоконагруженным, но я не уверен, что оно способно работать на уровне скажем ClickHouse. Хотя, возможно, я как-то неправильно интерпретирую сферу их применения.
Vamp
29.11.2023 03:53+2Prometheus работает по pull модели. То есть он сам ходит по сервисам и собирает с них метрики в удобном для себя темпе. Это точно не хайлоад.
И да, как и с графаной, хайлоадом это становится, когда "смотрят несколько тысяч человек единовременно (зачем?)".
PrinceKorwin
29.11.2023 03:53+1Если для вас не так важна память (а она сейчас очень дешёвая)
Скажите это Apple. У них апгрейд памяти стоит как крыло от Боинга.
Сорри за оффтоп. Просто наболело :)
amkartashov
29.11.2023 03:53Непонятен смысл бенчмарков. Обычно бенчмарк должен дать ответ-сравнение нескольких альтернативных технологий. Чтобы потом можно было из них выбрать.
Здесь же сравнивается одна конкретная программа с другой конкретной программой. Нельзя вычленить из бенчмарков роль сетевой задержки, роль библиотеки обработки json или драйвера базы данных и т.д. Таким образом нельзя ничего определённого сказать.
Ещё непонятно, использовалась ли одна и та же база данных, или запуск постгреса и генерация данных производились заново.
Ну и результаты бенчмарков скриншотиками - фу.
Upd: вполне может быть что сравнивались не программы а prepared vs non-prepared statements. Я бы переделал на явные prepared statement которые инициализируются один раз.n0dwis
29.11.2023 03:53+1Я бы предложил наоборот - non-prepared statement. Поскольку они привязываются к соединению, а в случае пула соединений не факт, что следующий запрос будет использовать это же prepared statement.
itmind Автор
29.11.2023 03:53Смысл в том, что вот думает разработчик на чем писать новый проект, выбирает из нескольких вариантов, а для объективного выбора не хватает информации - люди пишут одно, в тестах выходит другое. И вот чем больше будет разных сравнений, тем лучше.
Результаты скриншотами мне показались красивее, чем просто текст с терминала копировать. Или вы про то, что нужно графики сделать?
amkartashov
29.11.2023 03:53+2Данные бенчмарки сравнивают ваши две конкретные программы целиком, а не Rust vs Go или sqlx::postgres vs github.com/lib/pq - чем это поможет разработчику?
Текстовая информация с картинки не копируется, не индексируется поисковиком.itmind Автор
29.11.2023 03:53Языки сравниваются через программы на них написанные. В данной статье сравниваются стеки Fiber-lib/pq vs Axum-sqlx как одни из самых популярных библиотек для разработки web-сервисов. Возможно кому то это сравние даст дополнительную пищу для размышлений и сделает перевес в ту или иную сторону.
amkartashov
29.11.2023 03:53+4Пока что я вижу сравнение уровня одного и того же программиста (no offence, мой уровень наверняка хуже) в разных языках/фреймворках.
dsh2dsh
29.11.2023 03:53а для объективного выбора не хватает информации
Как будто этот бенчмарк - для объективного выбора. Искусственные бенчмарки показывают радиус сферы коня и ничего более. К реальной жизни и реальным задачам - всё это не имеет никакого отношения.
V1tol
29.11.2023 03:53Есть очень простой способ проверить от чего зависит скорость в Rust версии - от оверхеда библиотек или он всё же упирается в I/O. Можно добавить LTO в релизную сборку и прогнать тесты ещё раз.
[profile.release] codegen-units = 1 lto = true
Ещё я использую для синтетических тестов на локальной машине wrk - самый производительный генератор трафика, который я пробовал. Не знаю, какая у вас машина, но может так быть, что Cassowary отбирает у сервисов процессорное время и это влияет на результаты.
damal
29.11.2023 03:53+4Похоже, что Golang JWT decoder не делает `verifySignature`, как это делает Rust.
Т.е. вместо `return []byte(jwtSecret), nil
` должно быть что-то более сложное (например `ParseRSAPublicKeyFromPEM`).aegoroff
29.11.2023 03:53автор вроде написал в конце "В Rust я пробовал менять sqlx на tokio-postgres, убирал расшифровку JWT, результат тот же.", говорит не помогло
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");
medigor
29.11.2023 03:53+2Да, помогает здорово, у меня GO 25k rps, Rust 30k rps, на 20% быстрее.
а если переделать на юниксокет, и lto=true, то раст становится на 40% быстрее (31.8k vs 44.6k)
Но тест все равно неправильный, надо в контейнерах с лимитами запускать, а лучше на выделенных тачках или виртуалках.И дизель там синхронный.
Pitou
29.11.2023 03:53Я не поленился, переписал пример на tokio-postgres. Получил разницу производительности в 10% на 50 пользователях. Впрочем, оба приложения грузят процессор на 15% и упираются не то в контейнер с БД, не то в контейнер с cassowary. Но вариант на rust кушает 9мб памяти, а go целых 19мб. Необоснованно много, я считаю. В общем и целом паритет.
vasyash
29.11.2023 03:53Обоснованно вполне. Разные механизмы работы с памятью. В golang работает GC, а в раст всё завязано на механизм владения, и кажется благодаря этому в куче "мусор" никогда не собирается.
itmind Автор
29.11.2023 03:53У меня на tokio-postgres показатели почему то как на sqlx. Какой пул использовали или без пула?
Pitou
29.11.2023 03:53+1deadpool-postgres для пула. Но я не настоящий растовщик, почему выбрал его и что есть ещё не скажу. А вот tokio-posgtres выбирал вполне осознанно - так подход получается 1 в 1 как с go, без ORM и прокладок (даже если они бесплатные, как дизель). Разве что есть важный нюанс - tokio-postgres не использует prepared statements по умолчанию, а в go либе они используются всегда, я даже не уверен что их можно отключить.
itmind Автор
29.11.2023 03:53Добавил в git стек ntex + deadpool-postgres + tokio-posgtres (с кешированием statements).
Без lto проигрывает Go на 0,5%, c lto выигрывает 0,5%. В общем на фоне погрешности. Замена axum на ntex преимущества не дало.
Преимущество Rust только в потреблении памяти получается.
itmind Автор
29.11.2023 03:53Изменил указанный параметр и провел замеры. По итогу Go и Rust сравнялись у меня. Дополнил статью.
PrinceKorwin
29.11.2023 03:53Мир-Дружба-Жевачка! :)
А вообще я давно понял, что делать бенчи очень сложно. И если получился неожиданный результат, то дело 146% в неправильных тестах.
Делать перформанс тесты - это целая наука.
titan_pc
29.11.2023 03:53-1Человек пошёл и доказал давно известную историю, что бенчмарки забугорья, не привязанные к реальности - показывают только то, что разогнать Rust быстрее чем Go способна лишь малая часть айтишного населения. Которая просто смогла и хотела сделать именно это. А если просто взять либы, прикрутить одно к другому, то производительность будет средняя, и даже хуже, если взять что-то не то.
PrinceKorwin
29.11.2023 03:53С чего бы это? Каждый язык предоставляет несколько разных библиотек для решения одной и той же задачи. Какие-то фичастее, какие-то быстрее, другие медленнее, но удобней в использовании. Выбор это хорошо.
В текущем примере автор выбрал не самую быструю библиотеку.
Это никак не означает, что только малое количество программистов на Rust способны "разогнать" скорость путем выбора другого крейта. :)
n0dwis
29.11.2023 03:53Вчера поигрался с кодом - уже с pgx и diesel, но ещё без тюнинга. Получил такие результаты - rust - 35k rps, go - 42k rps.
Но, когда добавил замеры времени работы именно http хендлера, то в обоих случаях время выполнения было в пределах 120-205 мкс. Т.е., похоже, на скорость, в основном, влияет Axum, а не sqlx или парсинг jwt. А вот настройка diesel эту разницу компенсирует.
medigor
29.11.2023 03:53да уже есть ответ, в чем проблема:
https://habr.com/ru/articles/777072/comments/#comment_26212898
alexfilus
29.11.2023 03:53+1Помимо количества и времени работы запросов хотелось бы увидеть потребление процессора и памяти.
Сдаётся мне что в условиях достаточного количества коннектов к пг, но не достаточного количества процов Rust обгонит Go.
tessob
Ура! Надеюсь, что все кто пришел за “blazingly fast” уйдут наконец писать на Go и пена немного сойдёт.
Автору могу порекомендовать попробовать свои силы на TechEmpower, а то в тамошних состязаниях Go что-то совсем не вывозит. Кто-то должен это пресечь.
Bromles
Только почему-то в реальной жизни это работает в обратную сторону, и компании после переноса своего прода с Go на Rust наблюдают 60% прироста в скорости (Cloudflare, Discord и прочие, и это только те, кто сказал об этом публично и был достаточно популярен, чтобы это заметили).
В целом тут не очень оптимально бенч для раста сделан, как минимум один sqlx чего стоит
Мб на досуге покручу код, если время будет
micronull
Уже есть ряд решений, которые могут решить одну из причин, а именно:
Арены (пока сырые).
sync.Pool
(мьютексы)https://github.com/dgraph-io/ristretto
Ну и нужно знать про различные хаки, как например:
_ = some[n]
, гдеn
- последний элемент слайса.domix32
как минимум хотелось бы выводить такое из разряда "хаков" в идиоматический код.
domix32
Кстати, вторая проблема го - это то, как скалируют нагрузку на приложения. Когда достигается некоторый лимит добавляется больше инстансов приложения, а не больше места в тредпуле. В итоге бОльшая часть профита от зелёных тредов в какой-то момент перестаёт существовать, т.к. приложение фактически приложение работает на одном ядре.
domix32
Прирост получается часто из-за того что GC больше не беспокоит исполнение. На такой нагрузке они наверняка и не почуяли бы memory pressure. Поднять RPS до 10к+ и посмотреть где начнётся просадка. Опять же местные тесты никак не рисовали, что там с памятью происходит и сколько каждый инстанс кушает памяти. На 100 соединениях зелёные треды вполне ожидаемо выигрывают более тяжеловестным ржавым.
itmind Автор
Запустил с параметрами -c 300 -n 100000
Go: 10489 Req/s
Rust: 4327 Req/s
В чем то у Rust явно проблема, возможно в sqlx. Переделаю на diesel и проверю еще раз.
domix32
Какой-нибудь flamefraph натравить и посмотреть на что тратится раст.
Ilay_Developer
В проде всё совсем иначе. С Go переходят на Rust. Да и Go - это упор на Web. Rust - мультипродуктовый язык. Но главная особенность - это замена C++ в критических местах.
PrinceKorwin
Ещё ниша у Rust это замена C где пишут либы для более высокоуровневых языков (PHP, Python например).
Результат получается по скорости такой же, но код стабильнее и безопаснее.