Дисклеймер
Обратите внимание: я сам новичок как в Rust, так и в целом в программировании и в коде могут быть ошибки.
Статья состоит из компиляции моего немногочисленного опыта и мнения, а также немного сравнения характеристик двух сферических коней в вакууме.
UPD после публикации
Спасибо всем, кто указывал на ошибки! Узнал много нового и полезного, а попутно исправил код в статье под новую версию :)
О Rust я слышал ещё несколько лет назад и все его либо хвалили, либо порицали, по различным причинам. Но сам как-то не брался за него - мне, неподготовленному к подобному синтаксису и не знакомому с подобными языками хотя бы на базовом уровне, в то время он казался совершенно непонятным. Но вот спустя время для себя решил написать что-то похожее на бенчмарк для тестов локальных HTTP API-серверов.
Об этом и моём опыте и пишу статью - вдруг кому из новичков окажусь полезен.
Первая версия такого "бенчмарка" была написана на Go. В целом эта версия меня устраивала, Go хорошо подходит для небольших приложений и, в отличии от Rust, имеет библиотеку для работы с HTTP в стандартном пакете, а fasthttp работает ещё лучше. Но всё-же вес бинарника в целых 5 Мбайт (это уже после -ldflags "-s -w") немного смущал.
Понятное дело, что в мире, где некоторые люди пишут небольшие приложения на Java с итоговым весом под 100 Мбайт, моё приложение кажется очень лёгким, но лично меня это не устраивало.
В тот момент я и решил, что надо бы попробовать это исправить и переписать на Rust, т.к. на C++ у меня не хватит ни навыков, ни терпения.
Основные минусы первой версии "бенчмарка" на Go:
Вес итогового бинарника. Даже после
-ldflags "-s -w"
и стрипания (которое отнимает всего около 100-200 Кбайт) это как-то много.Потребление RAM выше, чем могло бы быть. Особенно разница чувствуется на небольшом количестве запросов, если запросов 10К или более - разницы почти нет.
Нестабильная работа "главной" Go-рутины, которая при целевом RPS (request per second) в 1К могла выдавать от 600 до ~800 запросов в секунду.
О плюсах и минусах Go и Rust в сравнении расскажу далее.
Итак, для лёгкой реализации идиоматичного приложения на Rust нам нужны легковесные потоки (они же - горутины), к счастью их нам может предоставить Tokio! Эта библиотека может дать нам функционал Go в виде корутин и каналов, но только в Rust и лучше.
"Лучше" в плане меньшего веса бинарника, и как мне кажется, большей производительности из-за самого языка.
Итак, "рантайм" мы себе нашли - Tokio
, но в Rust нет ещё и стандартной библиотеки для работы с HTTP, здесь я решил использовать Hyper
, т.к. Reqwest
просто огромна и работает даже хуже стандартной библиотеки в Go, а ureq
всё-равно больше, чем Hyper, а по производительности вряд ли отличается.
Также будем использовать парсер аргументов командной строки - argparse.
Итого Cargo.toml:
[package]
name = "akvy"
version = "0.2.0"
edition = "2021"
[dependencies]
tokio = { version = "1.24.2", features = ["full"] }
hyper = { version = "0.14", features = ["full"] }
argparse = "0.2.2"
[profile.release]
lto = true
strip = true
В профиле настройки для уменьшения размера. Strip т.к. всё-равно не предполагается отладка приложения вне дебаг режима, а бинарник хочется уменьшить максимально.
Начнём же разбирать код.
Для самих нетерпеливых вот ссылка на GitHub с актуальным кодом, а здесь мы разберём основные моменты с пояснениями.
Начать стоит с главной функции всего приложения
async fn get(uri: Uri, client: Client<HttpConnector>) {
// Записываем время начала, чтобы посчитать время ответа
let start = Instant::now();
// Совершаем запрос по переданному URL и клиенту.
match client.get(uri).await {
// Если ответ есть, но ответ не 200
Ok(res) => {
if !res.status().is_success() {
*ERRORS.lock().unwrap() += 1;
}
},
// Если иная ошибка
Err(_) => {
*ERRORS.lock().unwrap() += 1;
}
}
RESPONSE
.lock()
.unwrap()
.add(start.elapsed().as_millis() as u32);
}
Кстати о "глобальных переменных" - это два static Mutex<T>
// Под Mutex хранится структура с информацией
// о количестве запросов, минимальном, максимальном и среднем
// времени ответа от сервера
static RESPONSE: Mutex<ResponseTime> = Mutex::new(ResponseTime::new());
// Просто u128, в котором хранится количество ошибок.
// u128 потому, что можно ._.
static ERRORS: Mutex<u128> = Mutex::new(0);
Немного об Mutex<T>
Mutex<T> используется, чтобы безопасно читать и изменять переменные, работать с переменными под Mutex может только та функция, которая заблокировала этот Mutex, а после работы она разблокирует его и воспользоваться переменной сможет другая функция и т.д.
T - любой тип данных.
Сразу же рассмотрим функцию парсинга из текста в Uri:
fn parse_url(url: String) -> Uri {
// Если URL содержит HTTPS, то закрываем приложение
if !url.contains("https://") {
let uri = url.parse();
if uri.is_err() {
println!("URL error!");
exit(1)
}
return uri.unwrap();
}
println!("App work only with HTTP!");
exit(1)
}
Здесь всё стандартно, помимо проверки на содержание в строке https:// - дело в том, что изначально Hyper не поддерживает HTTPS, нужно подключать другие зависимости, а во-первых, это, скорее всего, добавит места бинарнику, во-вторых - приложение должно тестировать локальные HTTP-сервера, а не атаковать чужие HTTPS сайты, а в-третьих - мне лень пока.
В функции используется стандартный метод .parse()
, а всё остальное просто удобная оболочка.
Теперь пройдёмся по main() сверху вниз.
Задаём стандартные характеристики для приложения
let mut url_in = String::from("http://localhost:8080");
let mut rps: u16 = 10;
И парсим аргументы командной строки:
{
// Создаём объект парсера и описание
let mut ap = ArgumentParser::new();
ap.set_description("Set app parameters");
// Парсим URL в переменную url_in
ap.refer(&mut url_in)
.add_option(
&["-u", "--url"], // Флаги
Store, // Store - положить значение в переменную
"Target URL for bench"); // Описание для -h
// Парсим RPS в переменную rps
ap.refer(&mut rps)
.add_option(
&["-r", "--rps"],
Store,
"Target number of requests per second"
);
// Сам парсинг аргументов
ap.parse_args_or_exit();
}
Далее парсим нашу строку в Uri
и выводим характеристики бенчмарка в консоль:
let url = parse_url(url_in);
println!("\n{} | {}", url, rps);
// И записываем время начала теста
let start = Instant::now();
Также нужно создать наш "бесконечный" цикл, который будет с определённым интервалом вызывать функцию get(url) в отдельном таске (task, та же горутина).
// Задаём интервал, который будет в цикле
let mut interval = time::interval(Duration::from_micros(1_000_000 / rps as u64));
// Создаём объект клиента, чтобы копировать его в get()
let client = Client::new();
// Создаём главный таск,
// который в цикле будет создавать другие таски
tokio::spawn(async move {
loop {
// Клонируем URL и client из main в область видимости цикла,
// концепция владения ведь :)
let url = url.clone();
let client = client.clone();
// Создаём таск, в котором будет работать запрос
tokio::spawn(async move {
get(url, client).await; // await обязателен, т.к. функция async
});
// Ждём заданное время и обнуляем интервал,
// после повторяем цикл
interval.tick().await;
}
});
Здесь мы создаём Interval
с периодичностью в нужное нам время. Важно заметить, что не получится использовать просто tokio::time::sleep
т.к. на интервалы менее ~100 микросекунд такой цикл не будет способен. Sleep будет спать не меньше указанного времени, а больше может.
Т.к. главный цикл крутится в другом таске - приложение идёт дальше и нам нужно его корректно завершить. ИМХО лучший способ - обработать Ctrl + C в консоли:
// Создаём обработчик сигнала Ctrl + C
let mut stream = signal(SignalKind::interrupt()).unwrap();
// Ждём сигнала, не пускаем приложение дальше без него
stream.recv().await;
// Записываем время
let end = start.elapsed();
А далее следует огромный блок с выводом информации
// Тут, в целом, всё понятно и без описания
{
let req = RESPONSE.lock().unwrap();
let err = *ERRORS.lock().unwrap();
print!("\n\n");
println!("Elapsed: {:.2?}", end);
println!("Requests: {}", req.get_count());
println!("Errors: {}", err);
println!("Percent of errors: {:.2}%", percent_of_errors(req.get_count(), &err));
println!("Response time: \
\n - Min: {}ms \
\n - Max: {}ms \
\n - Average: {}ms", req.get_min(), req.get_max(), req.get_average());
}
И функция вычисления процента ошибок, что используется при выводе:
fn percent_of_errors(req: u32, err: &u128) -> f32 {
let res = (*err as f32 / req as f32) * 100.0;
if res > 0 as f32 {
res
} else {
0 as f32
}
}
Структура ResponseTime и её методы.
Если забыли, мы используем эту структуру в Mutex
в качестве глобальной переменной.
static RESPONSE: Mutex<ResponseTime> = Mutex::new(ResponseTime::new());
Изначально её не было ни в коде, ни в статье, соответственно. На её создание меня подтолкнул один из комментарием, что вместо Vec
с массивом из времён ответов можно использовать 4 переменные. И надеюсь, что я правильно понял идею...
Сама структура хранится в файле utils.rs
, а это уже отдельный crate (aka пакет, библиотека).
Структура выглядит так:
pub struct ResponseTime {
average: u32,
count: u32,
min: u32,
max: u32
}
И у неё несть несколько методов, которые нам стоит разобрать...
Во-первых это приватные методы проверки является ли переданное время ответа самым маленьким или самым большим из всех ранее переданных:
// Обе функции принимают ссылку на структуру,
// методами которой они являются.
// А также - сравнивоемое число u32.
fn min_check(&mut self, item: u32) {
self.min = self.min.min(item);
}
fn max_check(&mut self, item: u32) {
self.max = self.max.max(item);
}
Далее стоит разобрать главное "нововведение". Если раньше в приложении использовался вектор Vec<u32>
который хранил в себе время ответа для каждого запроса в отдельной переменной, то сейчас у нас используется лишь одна конкретная, не расширяемая переменная u32, которая в структуре ResponseTime
именуется average.
Преимущество в отсутствии аллокаций на куче и, по идее, большей производительности, чем при использовании Vec. Если я, конечно, всё правильно понял.
pub fn add(&mut self, new: u32) {
// В переменную помещается новое среднее арифметическое,
// вычисленное по такой вот формуле.
// На самом деле при использовании этой формулы теряется точность
// среднего арифметического, но по моим ощущениям - не сильно.
// Возможно есть формула по-лучше, но я нашёл только эту, из рабочих.
self.average = (self.average * self.count + new) / (self.count + 1);
self.count += 1;
// Вызываются описанные ранее функции с переданным новым значением.
self.min_check(new);
self.max_check(new);
}
// Возвращает ResponseTime с заранее заданными полями
pub const fn new() -> Self {
Self {
average: 0,
count: 0,
// При любом вызове min изменится на более корректное число,
// если поставить 0 - минимальным временем ответа будет 0...
min: 999_999_999,
max: 0
}
}
Сравним Go и Rust
Само это сравнение уже является неправильным, аморальным и должно караться полицией нравов, но мы это сделаем. Да, сравним высокоуровневый Go с низкоуровневым Rust. Само по себе это сравнение уже похвала для Go, ведь никто и не заикается сравнивать, например, Python и Rust в производительности, а Go - постоянно.
Меряемся циферками:
Все тесты проводились на моём ноутбуке - MacBook Air M1 8gb, HTTP запросы на http://httpbin.org/ip
Rust |
Go |
|
Вес бинарника |
1.5 Мбайт |
5.6 Мбайт |
Потребление RAM спустя минуту на 10К RPS |
28.6 Мбайт* |
25.7 Мбайт* |
Время выполнения 100К запросов при установленном лимите 10К в сек. |
10.03 сек. |
12.09 сек. |
*Результат минутного теста в Go:
{
"req_count": 471213,
"err_count": 441348,
"average_response_time_ms": 68.38669,
"max_response_time_ms": 7031,
"min_response_time_ms": 0,
"time_of_bench_sec": 61.92429,
"percent_of_errors": 93.6621
}
*Результат минутного теста в Rust:
http://httpbin.org/ip | 10000
Elapsed: 60.64s
Requests: 606176
Errors: 603539
Percent of errors: 99.56%
Response time:
- Min: 0ms
- Max: 36195ms
- Average: 17ms
Это что, получается, Go потребляет меньше ОЗУ, чем Rust? Пластмассовый мир победил?
Ну, не совсем... Как можно заметить из результатов обоих минутных тестов - Go недоделал ещё 130К положенных запросов, отсюда и потребление памяти меньше. Но всё-же он очень порадовал, а точнее не сам Go, а fasthttp
. Если бы мы использовали стандартную библиотеку http, то разрыв и по ОЗУ, и по количеству запросов был бы намного больше.
Понятное дело, что всё это просто циферки и они не отображают реального положения дел, но всё же они есть и я их показал. И да, это было ожидаемо.
Плюсы и минусы Rust в сравнении с Go
Плюсы:
Производительность
Размер бинарника
Отсутствие GC (Сборщика мусора)
Отсутствие рантайма
Хорошее ООП (Да, не стандартное, но этим оно и нравится мне, ИМХО)
Умный компилятор со множеством оптимизаций.
Совместимость по памяти. На Rust можно написать библиотеку к Go, Python, Ruby и т.д. Или использовать совместно с C/C++
Минусы:
Сложность в освоении. Как в освоении синтаксиса, концепции владения и времени жизни, так и в библиотеках, которыми пользоваться иногда в разы сложнее, чем в Go.
Сложнее делать кроссплатформенное приложение. Например, из под моего M1 не получится скомпилировать Rust в бинарник для Linux или Windows, а Go - легко.
VSCode, настроенный под Rust, просто отвратителен, опять же - ИМХО. Да и я не настраивал его три часа, как некоторые рекомендуют в таких ситуациях.
Сам не пробовал, но многие утверждают, что в Rust до сих пор бывают проблемы с async I/O. Утверждать не берусь, маловато опыта.
Собственно, это всё то немногое, что я успел узнать о Rust за пару месяцев ленивого изучения. Если нужен вывод - используйте то, что больше нравится. Go идеально подойдёт для API-серверов и подобного, где основная нагрузка - на сеть и накопители. А Rust хорошо подходит для вычислений. К тому же, никто не запрещает их совмещать.
Комментарии (55)
slava_k
00.00.0000 00:00+2Возможно, имеет смысл посмотреть в сторону K6 .
keigisdead Автор
00.00.0000 00:00Сама идея "написать свой бенчмарк" появилась, когда я ещё не нашёл другие удобные утилиты, но а после хотелось самому что-то подобное реализовать.
ivankudryavtsev
00.00.0000 00:00+18А почему в бенчмарке одни ошибки?
Requests: 606176 Errors: 603539 Percent of errors: 99.56%
Vest
00.00.0000 00:00+22Потому, что он ДДОСил чужой сервер, а тот его блокировал. Кстати, такой тест считается провальным.
Меня в этой истории не удивляет то, что человек просто переписал свою программу не вдаваясь в детали, почему предыдущая версия тормозила и какие у неё были боттлнеки.keigisdead Автор
00.00.0000 00:00Про ДДОС сервера верно, httpbin мог начать отвечать ошибками при RPS от 1К.
Я переписал приложение не потому, что не хотел чинить версию на Go, а потому, что хотел написать версию на Rust для обучения. Так-то понятно, что и Go версию можно оптимизировать, но размер бинарника вряд ли получится сильно уменьшить.
И в целом, ничего против Go не имею, это сравнение не языков, а двух "сферических коней в вакууме".
Mingun
00.00.0000 00:00+5Видимо, автор был уверен, что только ошибки и будут, вон, аж
u128
для подсчетов использовал :)
eee
00.00.0000 00:00-1Сравнивали с hey?
rusik2293
00.00.0000 00:00Глянул в исходник hey, вижу там net/http, это собственно стандартная библиотека про которую писал автор
keigisdead Автор
00.00.0000 00:00Нет, не сравнивал с другими бенчмарками совсем, ведь на соперничество с ними и не претендую, здесь сравнивал производительность именно моих решений на двух языках.
kivsiak
00.00.0000 00:00+6А что за проблема с кроскомпиляцией? Под m1 нет тулчейна?
keigisdead Автор
00.00.0000 00:00С M1 не получается скомпилировать под x86 Windows или Linux. Сам особо не вникал, но ни одно решение из интернета не сработало.
miga
00.00.0000 00:00+14Гошный код не показали, бенчмарк на чужом удаленном сервере (небось еще и по wifi). Даж не знаю, что автор тут тестировал, но точно не языки
ShashkovS
00.00.0000 00:00+6Размер бинаря же :)
"Лучше" в плане меньшего веса бинарника
rusik2293
00.00.0000 00:00Интересно на кой нужен меньший вес бинарника, с роутера кого-то дудосить или я не в теме. Просто привык иметь дело с продом в несколько десятков терабайт места на ссд, смысл мне за пару мегабайт код преписывать
DirectoriX
00.00.0000 00:00-2Как минимум потому что чем меньше бинарь — тем меньше вероятность выпадения «горячих участков» из кеша. Особенно в случае с GC-языками, когда сборщик может запускаться достаточно редко, чтоб быть изгнанным из кеша, но достаточно регулярно чтоб постоянно загружаться обратно.
0xd34df00d
00.00.0000 00:00+3Как минимум потому что чем меньше бинарь — тем меньше вероятность выпадения «горячих участков» из кеша.
Для уменьшения этой вероятности нужно, чтобы горячие участик были меньше размером. Размер всего бинаря тут не столь важен.
keigisdead Автор
00.00.0000 00:00Не то, чтобы нужен, но я изучаю почти параллельно несколько языков и было интересно, на сколько меньше будет бинарник на Rust. Сам Go изначально задуман для бекенда, где размер бинарника не особо важен, а я хотел маленькую утилиту.
t38c3j
00.00.0000 00:00Код в соседней репе автора, https://github.com/xeynyty/go-bench
miga
00.00.0000 00:00+16Горутина на каждый реквест, действительно, куда ж все время и память уходит :)
keigisdead Автор
00.00.0000 00:00Я и сам писал об этом:
[...а также немного сравнения характеристик двух сферических коней в вакууме.]
Этот тест совсем не показательный, просто сравнение двух моих приложений с похожей архитектурой на двух языках. Ну а главным "камнем преткновения" был размер итогового бинарника.
staticmain
00.00.0000 00:00+3и стрипания (которое отнимает всего около 100-200 Кбайт)
Да сколько раз посторять, что НЕЛЬЗЯ стрипать бинарники на go.
t38c3j
00.00.0000 00:00Если не требуется отлаживать или анализировать бинарник, то вполне годный компромисс. Или я чего-то не знаю?
staticmain
00.00.0000 00:00+2С 2011го года идеи срач, можно или нельзя. В мейлинг листе были сообщения, что это портит секции, что используется для менеджмента памяти рантайма.
Официального объявления о том, что это починено не было. Отдельные индивиды утверждают, что всё ок: https://honnef.co/articles/stripping-go-elf-binaries-is-safe/
DirectoriX
00.00.0000 00:00+1Почитал статейку по ссылке: как будто go-компилятор не умел (не умеет?) правильно собрать заголовки, из-за чего strip делал как привык, а не как надо в этих странных случаях.
Простите, но это бага компилятора, а не strip. По-хорошему, об этом надо кричать в баг-трекере, потому что некорректные заголовки бинаря — это вообще как так можно накодить компилятор??staticmain
00.00.0000 00:00+2Так а где я сказал, что это баг strip?
DirectoriX
00.00.0000 00:00+5Так я тоже нигде не утверждал, будто вы сказали что это баг strip.
Я хотел сказать, что обычно люди думают, что если нечто (бинарь) работало до воздействия (strip), но перестало после — значит виновато именно воздействие (и хотя в большинстве случаев это так, в этом конкретном примере виноваты сами бинари). И из вашегоНЕЛЬЗЯ стрипать бинарники на go
можно подумать, что это именно стрипанье их ломает, хотя оно лишь обнажает изначально некорректную структуру бинарей.
А также я хотел сказать, что если корректное стрипанье показывает проблему из-за некорректности структуры (которая, вообще-то, описана задолго до go) — НУЖНО стрипать, чтобы заваливать разработчиков go баг-репортами. Если сегодня некорректная структура приводит к проблемам после strip, то кто знает, что может сломаться завтра? Стандарты и спецификации (в данном случае ELF) не просто так существуют.
keigisdead Автор
00.00.0000 00:00Не натыкался на этот момент ранее, спасибо. Да и в целом оно не много веса отнимает, так что я его и не использовал особо, но указать стоило для понимания.
MountainGoat
00.00.0000 00:00+6С прошлого лета конструктор Mutex может быть static, поэтому уже можно создавать глобальные переменные и обойтись без lazy static.
Cheater
00.00.0000 00:00Стесняюсь спросить, зачем здесь асинхронщина с tokio? Если я правильно понял сценарий, то выполнить все запросы в цикле последовательно vs распараллелить никак не повлияет на результат теста, тк меряется сумма времён обработки запросов.
keigisdead Автор
00.00.0000 00:00Tokio использовал не из необходимости, а потому, что в Go приложении во всю использовалить корутины (горутины) и здесь хотелось чего-то похожего. К тому же это максимальное простое решение проблемы для новичка вроде меня. Т.е. мы просто на кажды запрос создаём отдельную корутину, которая и занимается обработкой своего запроса.
commanderkid
00.00.0000 00:00+2Особой разницы не заметил, да бинарник в 3 траза больше, потребление памяти такое же, а скорость на 10% больше. Не, в качестве изучения Rust норм. Короче паритет.
google_fan
00.00.0000 00:00+8Если вы считаете только max/min/average зачем вы храните массив со всеми данными (в хипе! С реалокациями!) , вместо того чтобы хранить четыре переменные u64?
Вы каждом цикле клонируете строку ( опять же - реаллокация в хипе!) вместо того чтобы завернуть строку в Arc и получить почти бесплатный clone()?
Какой рантайм токио вы использовали? Пробовали ли вы запустить это все в one thread runtime? Как писали выше у вас нет никакой конкурентности, вам не нужно запускать треды на каждом ядре.
keigisdead Автор
00.00.0000 00:00+1Спасибо за дельный совет, учту. Изначально хотел просто сделать идиоматически схожее приложение с Go версией.Под "рантаймом" подразумевал, скорее, некую схожесть с удобными корутинами в Go, ведь Tokio позволяет создавать миллионы тасков (task) и работать с ними также, как и в Go, что очень легко воспринимается при переходе между языками.
Касательно
Какой рантайм токио вы использовали? Пробовали ли вы запустить это все в one thread runtime? Как писали выше у вас нет никакой конкурентности, вам не нужно запускать треды на каждом ядре.
Я, если честно, не совсем понял. Как я уже указывал в дисклеймере в начале статьи - я и сам новичёк в Rust и многого не знаю, но спасибо за упоминание, поищу информацию об этом.
vlad4kr7
00.00.0000 00:00+2*ERRORS.lock().unwrap() += 1;
есть такое: https://doc.rust-lang.org/std/sync/atomic/index.html
s_f1
00.00.0000 00:00Плюсы:
Кто-то может пояснить, почему это «плюс» языка?
Отсутствие GC (Сборщика мусора)rutexd
00.00.0000 00:00+2Все просто. Сборщик мусора занимает цпу, память, лишает программиста бдительности и сам по себе предоставляет рантайм который определяет язык, его модель и опять же влияет на перфоманс. О том что он медленный и неэффективный в средних/больших проектах думаю даже и говорить не нужно.
Отсутствие гк в связке с идеей владения в контексте раста - даёт не только буст по перфомансу но так же лишает части проблем, как например гонка состояний и заставляет программиста думать над своим кодом. Понятно, что работает не всегда и не везде, но процент кода на порядок выше чем у скажем какого нибудь питона или го.
Vsevak
00.00.0000 00:00В гарантиях там есть довольно четкие грани - по поводу "гонки состояний" можно уточнить, что Rust без unsafe исключает data race, который UB, но позволяет создавать race condition, которые логические ошибки.
rutexd
00.00.0000 00:00Получить ошибки и гонки можно. Но это куда сложнее и из коробки этого вообще не сделать ибо тебе компилятор физически не даст этого сделать, не зная тонкостей и примудростей языка. На хабре даже кто то экспериментировал и сравнивал с Go и была статья.
Vsevak
00.00.0000 00:00+2GC - это решение проблем с ручным управлением памятью за счет накладных расходов в рантайме. Rust тоже решает проблемы с памятью, но позволяет получить определенные гарантии статически.
Qudres
00.00.0000 00:00Поскольку не приведен код на go, хочу уточнить у Вас один вопрос, как изменятся результаты если выключить gc.
keigisdead Автор
00.00.0000 00:00Отключать GC не пробовал, если честно. Но сам GC включается не так уж и часто, на сколько я мог заметить - само приложение обычно не живёт долго. В статье GC упоминал в целом, как особенность языка и его рантайма.
JohnScience
00.00.0000 00:00+1Большое спасибо за статью! А почему Arc<Mutex<u128>> вместо atomic'а?
keigisdead Автор
00.00.0000 00:00-1Спасибо за отзыв!
Я новичок в Rust и использую в первую очередь то, до чего легче всего додуматься :)
А об Arc я часто слышал в гайдах / лекциях, потому и использовал его.На самом деле сейчас переписываю код после прочтения комментариев под статьёй и скоро изменю её, в новой, предположительно, не будет Arc совсем, просто Mutex, т.к. это глобальная переменная и нет нужны считать количество ссылок. Но пока не точно :)
rehci
00.00.0000 00:00-1Если я правильно понял по результатам, то переписывание не стоило переписывания, Go показал себя вполне достойно.
guvernir4
00.00.0000 00:00-1Интересно узнать сколько ушло времени и сил на отладку кода на Rust. У rust всётаки порог вхождения повыше чем в go. Соответственно и отлаживать должно быть тяжелее.
keigisdead Автор
00.00.0000 00:00Сам Rust я, на момент написания, лениво изучал уже как пару месяцев. А на само прилодежение ушло пол дня, на первую версию. В основном спотыкался об особенности языка и Tokio, как те же sleep и interval в токио, котя в Go хватало лишь метода Sleep
nuald
00.00.0000 00:00+8Статья немного перекликается с работой, которую я проводил месяц назад. Скажу сразу, что для меня это был рабочий проект, так что он под NDA и исходники не могу выложить. Но у меня тоже было сравнение Go и Rust (и другие решения включая чистый Си), но только в совершенно другом масштабе: мне нужно было нагрузить наши железки высокоуровневым (высшие слои OSI) трафиком под завязку. iperf-тесты уже были, но нам нужен был реалистичный трафик.
Мои результаты на localhost на моем рабочем linux ноутбуке (задейственны все ядра CPU, никаких мьютексов и других пожирателей CPU, статистика собиралась lock-free алгоритмами, сокеты с TCP_NODELAY):
Go - 400 тыс запросов секунду
Tokio/hyper - 300 тыс запросов в секунду (да, у меня эта пара оказалась медленнее, но у меня была немного другая методика тестирования)
Чистый epoll (rust и C показали примерно одинаковые результаты) - 600 тыс запросов в секунду
io_uring (пробовал только Rust, программировать на Си все-равно не собирался), тестировал пакеты glommio и monoio - 800 тыс. Остановился на последнем потому что есть fallback на epoll (старые linux) и kqueue (macos).
Ещё хотел бы отметить про hyper - я от него в конце отказался полностью и написал специализированный генератор и парсер HTTP, благо протокол простой. Flamegraph показал что hyper тратит слишком много времени CPU, и хотя у него могут быть более корректные по стандарту парсеры, для benchmark-а он стал бутылочным горлышком.
JavaNoob
Не хватает в бенчмарке результатов apache ab
Ну и, думаю, с прямыми руками те же самые ЯП можно ускорить ещё на десятки процентов, а то и в разы.
"люди пишут небольшие приложения на Java с итоговым весом под 100 Мбайт"
пук в лужу. или саму jvm посчитали? тогда не забудьте ещё ОС посчитать, оно же JVM запускает. в итоге чё там, 2гб против 2.1гб выйдет
izogfif
Вроде через GraalVM можно сделать "нативный бинарник", в который нужные куски JVM будут вкомпилены / влинкованы статически. В результате будет меньше.
keigisdead Автор
В самой Java опыта не имею и именно на бекенд разработку, выполнение под JVM и подобное вообще не лезу, конкретно я имел ввиду скомпилированные бинарники из Java кода. Таких на GitHub хватает.
ИМХО не должна весить небольшая консольная утилита под 100 Мбайт, это уже перебор как-то :)
JavaNoob
>ИМХО не должна весить небольшая консольная утилита под 100 Мбайт
и зачем такие люди как ты статьи пишут? )
ls -l target/mtLoader-1.0-jar-with-dependencies.jar
3501 Mar 5 21:01 target/mtLoader-1.0-jar-with-dependencies.jar
3501 БАЙТ, Карл, 3501 БАЙТ!
внутри
` final String URL = "http://127.0.0.1:11456/tika"; HttpClient httpClient = HttpClient.newBuilder().build(); IntStream.range(1, 1_000_000).parallel().forEach(num -> {
try {
HttpRequest request = HttpRequest.newBuilder() .uri(URI.create(URL)) .GET() .build(); httpClient.send(request, HttpResponse.BodyHandlers.discarding()); } catch (Exception ee) { ee.printStackTrace(); } }); System.out.println("used memory: " + ((Runtime.getRuntime().totalMemory()-(Runtime.getRuntime().freeMemory())) / 1048576) + " MB"); System.out.println("allocated memory: " + (Runtime.getRuntime().totalMemory() / 1048576) + " MB"); }
`
запуск с лимитом 15МБ
java -Xmx15m -jar target/mtLoader-1.0-jar-with-dependencies.jar
used memory: 8 MB
allocated memory: 16 MB
шах и мат )))
ещё можно с 2017 года резать JRE на модули, гугли jigsaw, оставлю в качестве домашнего задания. Не думаю, что до 5МБ ужмется, но блин насколько это всё далеко от цитируемого бреда.
p.s. 100МБ jar даже очень жирные сервисы не занимают, у которых внутри и spring (30МБ) с вебсервером и IOC контейнером и куча бизнес-фарша.
p.p.s. Про apache ab то что скажем? Насколько помню, он легко 50k rps выжимает на локалхосте.