Мир веб-разработки предлагает бесконечное количество вариантов HTTP-фреймворков для разных языков программирования. Но как разработчикам понять, какие из них обеспечивают действительно высокую производительность? Под катом команда блога CodeReliant* проводит прямое сравнение некоторых из лучших претендентов на быстродействие. Рассматривает популярные варианты на Javascript/Bun, Java, C#, Go и Rust, проводит бенчмаркинг, оценивает их пропускную способность и время отклика при тестировании.

Выбранные для сравнительного анализа фреймворки имеют репутацию высокопроизводительных, — но посмотрим, как это понятие реализуется в разных стеках.

*Обращаем ваше внимание, что позиция автора может не всегда совпадать с мнением МойОфис.


Вот пять участников тестирования:

С помощью стресс-тестирования мы получим достоверные данные об их возможностях. Наше сравнение "лоб в лоб" фокусируется на скорости и масштабируемости с нулевой конфигурацией, чтобы вы могли выбрать правильный фреймворк для своего следующего веб-проекта с высоким трафиком. Если вы хотите ускорить работу 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, показали неплохую производительность, и, возможно, им потребуется дополнительная настройка, чтобы конкурировать с другими фреймворками.

Наверняка показатели выглядели бы иначе, если бы мы потратили некоторое время на настройку языка/фреймворков. Однако мы хотели, чтобы это сравнение было проведено с нулевой конфигурацией.