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

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

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


  1. ainu
    22.03.2024 12:01
    +1

    Спасибо за наводку на oha, попробую использовать вместо ab


  1. Gorthauer87
    22.03.2024 12:01
    +1

    А почему взяли Actix а не Axum? Второй же сейчас самый популярный в Rust.


    1. dpytaylo
      22.03.2024 12:01

      Actix выходит чуть быстрее по бенчмаркам, чем Axum, наверное поэтому его и брал автор оригинала


  1. xFFFF
    22.03.2024 12:01
    +2

    Почему .NET 8.0 RC2, а не Release?


    1. gturk
      22.03.2024 12:01
      +1

      Потому что оригинал статьи вышел в октябре 23, а дотнет релизнулся в ноябре


      1. xFFFF
        22.03.2024 12:01

        Не заметил, что перевод.)


  1. Andrey_Solomatin
    22.03.2024 12:01
    +2

    Было бы интересно посмотреть результаты на средних и крупных запросах.

    Еще HTTPS бы не мешало потестить. Хотя это уже потребует менять методику.

    Мне кажется в реальной жизни разница будет сильно меньше, возможно даже неразличимо меньше.


    1. redfox0
      22.03.2024 12:01

      https может терминировать тот же nginx или haproxy.


      1. Andrey_Solomatin
        22.03.2024 12:01

        Да, так рекомендовано делать внутри периметра для оптимизации трафика между серверами. Цифры для этого случая в статье и есть.


  1. Andrey_Solomatin
    22.03.2024 12:01

    Выгялдит как сравнение ряди сравнения.

    В реальности я скорее всего возьму простой фреймворк на Питоне. Узким местом скоее будет сеть или возможности сервиса.


    1. VanKrock
      22.03.2024 12:01

      Понятно, что для обычного бизнес-прложения это не важно, но вот для биржи, рекламного сервиса и всего, где важна скорость и обработка большого числа запросов это важно, rust в этом случае выглядит лучше всего ещё и из-за того, что работает без сборщика мусора, что может порой задержки, не даром в net8 потратили большие усилия на работу с паматью, хотя в go она изначально была хороша. Но вот писать на rust или go после с# совсем не приятно, для меня самым интересным выглядел D, но на нём нет толковых веб фреймворков, да и работу с памятью по типу как в rust вроде так и не завезли, но как язык очень приятно


    1. Syos
      22.03.2024 12:01
      +3

      Когда в теме не про питон увидел, что никто в комментах не написал про питон


  1. n0isy
    22.03.2024 12:01
    +3

    Необходимо сравнить заголовки ответа. Возможно наполнение заголовков в каком то из движков выше. Аналитика приходит, кешинг. В таком тексте каждый лишний байт приведёт к значительной просадке


    1. Andrey_Solomatin
      22.03.2024 12:01

      Если такие мелочи важны, то стоит еще посмотреть на что-то типа grpc.


    1. Vest
      22.03.2024 12:01
      +4

      Давным-давно, я профилировал одного индуса и отписывался тут в комментах. Тот тоже сравнивал два разных языка и что-то там решал.

      Так вот о тестах: Там зачастую играет роль наличие keep-alive соединения. Они ускоряют тесты очень сильно. Поэтому я не удивлён, что результаты такие разные.

      Люди, которые гоняют тесты предпочитают не думать над результатами, а просто смотрят на числа. И делают свои выводы.


  1. JackRowsen
    22.03.2024 12:01
    +3

    Так есть же уже многолетнее тестирование


    1. Timofeuz
      22.03.2024 12:01

      Там далеко не конфигурация по умолчанию.


  1. 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&section=data-r22


  1. VaVM
    22.03.2024 12:01

    Выглядит как соревнование грузовиков со снятыми кузовами - какой быстрее довезет отсутствующий груз.


  1. Alex-pvl
    22.03.2024 12:01
    +3

    Теперь мы знаем, что hello world лучше писать на rust


  1. Kravets2
    22.03.2024 12:01

    Может стоило в Bun&Elysia использовать кластера?


  1. micronull
    22.03.2024 12:01

    Узким местом чаще всего является IO и БД. Интересней было бы глянуть сравнения чего-то в этой области. Решений, архитектур, стеков и каких-то технологий.