Мир веб-разработки предлагает бесконечное количество вариантов HTTP-фреймворков для разных языков программирования. Но как разработчикам понять, какие из них обеспечивают действительно высокую производительность? Под катом команда блога CodeReliant* проводит прямое сравнение некоторых из лучших претендентов на быстродействие. Рассматривает популярные варианты на Javascript/Bun, Java, C#, Go и Rust, проводит бенчмаркинг, оценивает их пропускную способность и время отклика при тестировании.
Выбранные для сравнительного анализа фреймворки имеют репутацию высокопроизводительных, — но посмотрим, как это понятие реализуется в разных стеках.
*Обращаем ваше внимание, что позиция автора может не всегда совпадать с мнением МойОфис.
Вот пять участников тестирования:
Java 21 + vertex 4.4.6
JS/Bun 1.0.6 + elysiajs 0.7
C# 12 + dotnet/ASP.NET 8.0 RC2
Go 1.21.3 + fiber 2.49.2
Rust 1.73.0 + actix-web 4
С помощью стресс-тестирования мы получим достоверные данные об их возможностях. Наше сравнение "лоб в лоб" фокусируется на скорости и масштабируемости с нулевой конфигурацией, чтобы вы могли выбрать правильный фреймворк для своего следующего веб-проекта с высоким трафиком. Если вы хотите ускорить работу API, создать системы с низкой задержкой или выжать максимум из ваших серверов, это сравнение поможет вам выбрать высокопроизводительный HTTP-фреймворк, подходящий для вашего технологического стека.
Окружение и тестовая система
Для тестирования мы запустим минимальную версию HTTP-сервера, возвращающую ответ hello world при запросе /
.
Запустим сервер на машине Hetzner:
ОС: Ubuntu 22.04.3 LTS
Ядро: 5.15.0-86-generic
Архитектура: ARM aarch64
Ресурсы: 4 vCPU & 8 GB RAM
Генератор клиентской нагрузки будет находиться на отдельной машине с аналогичными характеристиками, но большими ресурсами — 8 vCPU и 16 GB RAM.
Java и Vertex
Сгенерируем стартовый проект vertex, используя сайт. Также мы будем использовать java 21-oracle
, которую только что установили с помощью sdkman.
package io.codereliant.performance;
import io.vertx.core.AbstractVerticle;
import io.vertx.core.Promise;
public class MainVerticle extends AbstractVerticle {
@Override
public void start(Promise<Void> startPromise) throws Exception {
vertx.createHttpServer().requestHandler(req -> {
req.response()
.putHeader("content-type", "text/plain")
.end("Hello World!");
}).listen(80, http -> {
if (http.succeeded()) {
startPromise.complete();
System.out.println("HTTP server started on port 80");
} else {
startPromise.fail(http.cause());
}
});
}
}
Просто изменив порт на 80
вместо 8888
и изменив возвращаемую строку на Hello World
вместо текста по умолчанию, мы соберем наш сервер с помощью mvn package
и запустим его с помощью java -jar target/performance-1.0.0-SNAPSHOT-fat.jar
.
java -jar target/performance-1.0.0-SNAPSHOT-fat.jar
HTTP server started on port 80
Oct 15, 2023 10:56:15 PM io.vertx.core.impl.launcher.commands.VertxIsolatedDeployer
INFO: Succeeded in deploying verticle
Bun и Elysia
Elysia упрощает создание проекта: сперва вызываем bun create elysia perf-app
, а затем — bun run index.ts
.
Для справки мы используем Bun 1.0.6
и Elysia 0.7.0.
import { Elysia } from "elysia";
const app = new Elysia().get("/", () => "Hello World").listen(80);
console.log(
`? Elysia is running at ${app.server?.hostname}:${app.server?.port}`
);
C# и Dotnet
Для C# 12 и dotnet 8.0 RC мы будем использовать ASP.NET Core Minimal APIs.
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/", () => "Hello World!");
app.Run();
dotnet new web -o perf-app
cd perf-app
dotnet build -c Release
ASPNETCORE_URLS="http://*:80/" ./bin/Release/net8.0/dotnet-app
Golang и Fiber
Fiber — это основанный на Fasthttp веб-фреймворк для Go, который был разработан для обеспечения высокой производительности.
mkdir go-app
cd go-app
go mod init github.com/codereliant/go-app
go get -u github.com/gofiber/fiber/v2
touch main.go
Затем в файле main.go мы используем пример hello world с лендинга https://gofiber.io/.
package main
import (
"log"
"github.com/gofiber/fiber/v2"
)
func main() {
app := fiber.New()
app.Get("/", func (c *fiber.Ctx) error {
return c.SendString("Hello, World!")
})
log.Fatal(app.Listen(":80"))
}
Для сборки и запуска примера можно использовать эти две строки:
go build main.go
./main
Rust & Actix-web
Информация с главного сайта actix-web:
Actix Web — это мощный, практичный и чрезвычайно быстрый веб-фреймворк для Rust.
Мы можем создать проект с помощью:
cargo new actix-hello
cd actix-hello
Затем заменяем содержимое src/main.rs
на содержимое ниже, взятое со страницы actix-web getting started:
use actix_web::{web, App, HttpResponse, HttpServer, Responder};
async fn manual_hello() -> impl Responder {
HttpResponse::Ok().body("Hey there!")
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
HttpServer::new(|| {
App::new()
.route("/", web::get().to(manual_hello))
})
.bind(("0.0.0.0", 80))?
.run()
.await
}
Наконец, просто собираем бинарник и запускаем его:
cargo build --release
# .......
# Finished release [optimized] target(s) in 1m 58s
./target/release/actix-hello
Клиент
В качестве клиента будем использовать oha — инструмент для бенчмаркинга HTTP, написанный на rust и вдохновленный Hey.
Как установить oha, вы можете посмотреть на странице github.
Запустим bombardier с 500 соединениями для 3 миллионов запросов, и повторим этот эксперимент трижды.
oha -c 500 -n 3000000 http://perf-experiment-host/
Ответ должен выглядеть примерно так:
Результаты
Для каждого из вариантов «язык + фреймворк» мы трижды провели бенчмаркинг приложения, а затем отобрали лучшие данные для сравнения.
Пропускная способность
Rust/Actix-web лидирует с самой высокой пропускной способностью, за ним следует Go/fiber.
C#/ASP.NET, несмотря на свою популярность, отстает от лидеров в этом бенчмарке.
Java/Vertex и Bun/Elysia демонстрируют сопоставимые и средние значения пропускной способности.
Задержка
На графике показаны задержки различных HTTP-фреймворков по трем различным параметрам:
99,9% Latency: задержка, при которой обрабатывается 99,9% запросов.
99 % Latency: задержка, при которой обрабатывается 99 % запросов.
Средняя задержка: среднее время ожидания для всех запросов.
Из графика можно сделать несколько выводов:
Rust/Actix-web и Go/fiber демонстрируют не только впечатляющую пропускную способность, но и более низкие задержки по всем трем метрикам.
C#/ASP.NET демонстрирует относительно меньшую среднюю задержку по сравнению с некоторыми другими фреймворками, несмотря на более низкую пропускную способность.
Заметна разница между задержками 99 % и 99,9 %, что подчеркивает вариативность времени обработки запросов в рамках фреймворков.
Выводы
В нашем исследовании Rust/Actix-web стал явным победителем, получив не только самую высокую пропускную способность, но и сверхнизкие задержки по всем метрикам. Следом идет Go/fiber, который впечатляет сочетанием высокой скорости обработки запросов и эффективного времени отклика. Хотя C#/ASP.NET, возможно, и не дотягивает до двух лидеров по пропускной способности, его среднее время задержки было вполне конкурентоспособным, и это говорит о том, что он по-прежнему является оптимальным выбором для многих приложений. С другой стороны, такие фреймворки, как Java/Vertex и Bun/Elysia, показали неплохую производительность, и, возможно, им потребуется дополнительная настройка, чтобы конкурировать с другими фреймворками.
Наверняка показатели выглядели бы иначе, если бы мы потратили некоторое время на настройку языка/фреймворков. Однако мы хотели, чтобы это сравнение было проведено с нулевой конфигурацией.
Комментарии (22)
Gorthauer87
22.03.2024 12:01+1А почему взяли Actix а не Axum? Второй же сейчас самый популярный в Rust.
dpytaylo
22.03.2024 12:01Actix выходит чуть быстрее по бенчмаркам, чем Axum, наверное поэтому его и брал автор оригинала
Andrey_Solomatin
22.03.2024 12:01+2Было бы интересно посмотреть результаты на средних и крупных запросах.
Еще HTTPS бы не мешало потестить. Хотя это уже потребует менять методику.
Мне кажется в реальной жизни разница будет сильно меньше, возможно даже неразличимо меньше.redfox0
22.03.2024 12:01https может терминировать тот же nginx или haproxy.
Andrey_Solomatin
22.03.2024 12:01Да, так рекомендовано делать внутри периметра для оптимизации трафика между серверами. Цифры для этого случая в статье и есть.
Andrey_Solomatin
22.03.2024 12:01Выгялдит как сравнение ряди сравнения.
В реальности я скорее всего возьму простой фреймворк на Питоне. Узким местом скоее будет сеть или возможности сервиса.VanKrock
22.03.2024 12:01Понятно, что для обычного бизнес-прложения это не важно, но вот для биржи, рекламного сервиса и всего, где важна скорость и обработка большого числа запросов это важно, rust в этом случае выглядит лучше всего ещё и из-за того, что работает без сборщика мусора, что может порой задержки, не даром в net8 потратили большие усилия на работу с паматью, хотя в go она изначально была хороша. Но вот писать на rust или go после с# совсем не приятно, для меня самым интересным выглядел D, но на нём нет толковых веб фреймворков, да и работу с памятью по типу как в rust вроде так и не завезли, но как язык очень приятно
n0isy
22.03.2024 12:01+3Необходимо сравнить заголовки ответа. Возможно наполнение заголовков в каком то из движков выше. Аналитика приходит, кешинг. В таком тексте каждый лишний байт приведёт к значительной просадке
Andrey_Solomatin
22.03.2024 12:01Если такие мелочи важны, то стоит еще посмотреть на что-то типа grpc.
Vest
22.03.2024 12:01+4Давным-давно, я профилировал одного индуса и отписывался тут в комментах. Тот тоже сравнивал два разных языка и что-то там решал.
Так вот о тестах: Там зачастую играет роль наличие keep-alive соединения. Они ускоряют тесты очень сильно. Поэтому я не удивлён, что результаты такие разные.
Люди, которые гоняют тесты предпочитают не думать над результатами, а просто смотрят на числа. И делают свои выводы.
vasyakolobok77
22.03.2024 12:01+1Это не просто сравнение ради сравнения, это просто бред какой-то.Типа возьмем рандомные фреймворки, запустим на машинках в каком-то облаке и посмотрим, что из этого выйдет. Сказать, что тут много нюансов - ничего не сказать:
– throughput / latency сильно зависят от конкурентности запросов,
– приложения на jit яп сильно зависят от прогрева, выделенных ресурсов, настроек сборки мусора,
– все приложения зависят от архитектуры, те же java/c# jit компиляторы не так чтобы оптимизированы под arm-ы, их десятками лет полировали под x86,
– все зависят от настроек системы, настроек сети, и в целом от расположения машин в сети датацентра.Как уже упоминали ранее, в более профессиональных бенчмарках результаты подобных plaintext тестов лежат рядом в пределах погрешности +-5% https://www.techempower.com/benchmarks/#hw=ph&test=plaintext§ion=data-r22
VaVM
22.03.2024 12:01Выглядит как соревнование грузовиков со снятыми кузовами - какой быстрее довезет отсутствующий груз.
micronull
22.03.2024 12:01Узким местом чаще всего является IO и БД. Интересней было бы глянуть сравнения чего-то в этой области. Решений, архитектур, стеков и каких-то технологий.
ainu
Спасибо за наводку на oha, попробую использовать вместо ab