天天看點

2023 年 Web 伺服器基準測試:NodeJS vs Java vs Rust vs Go

作者:技術的遊戲

現在是2023年,是時候進行一次新的Web伺服器基準測試了!

結果對我來說有些出乎意料!

2023 年 Web 伺服器基準測試:NodeJS vs Java vs Rust vs Go

一個Web伺服器必須能夠處理大量請求,盡管瓶頸在于IO。這次我決定比較最流行的、速度極快的現代架構的性能。

以下是有關實作細節的許多詳細資訊。如果您隻想了解結果,請直接前往文章底部以節省時間。如果您對測試的執行方式感興趣,請繼續閱讀 :)

我們的瓶頸将是一個帶有一些資料的Postgres資料庫。是以,我們的Web伺服器必須能夠在不阻塞的情況下盡可能多地處理每秒請求數。在接收到資料後,它應該将答案序列化為JSON并傳回有效的HTTP響應。

将測試哪些技術

* Spring WebFlux + Kotlin

- 傳統的JVM

- GraalVM原生映像

* NodeJS + Express

* Rust

- Rocket

- Actix Web

我的配置

CPU:Intel Core i7–9700K 3.60 GHz(8個核心,無超線程)

RAM:32 GB

作業系統:Windows 11(版本22h2)

Docker:Docker for Desktop(Windows版)版本4.16.3,啟用了WSL2支援-由Microsoft提供的預設資源配置

Postgres:*使用以下Docker指令啟動*

```

docker run -d --name my-postgres -e POSTGRES_PASSWORD=postgres -e POSTGRES_USER=postgres -e POSTGRES_DB=goods -p 5432:5432 postgres:15.2

```

資料庫連接配接池大小:最多50個連接配接。每個Web伺服器都将使用此數量以保持相同的條件。

資料庫初始化:

```sql

CREATE TABLE goods(

id BIGSERIAL NOT NULL PRIMARY KEY ,

name VARCHAR(255) NOT NULL,

description TEXT NULL,

price INT NOT NULL

);

INSERT INTO goods (name, description, price)

VALUES ('Apple', 'Red fruit', 100),

('Orange', 'Orange fruit', 150),

('Banana', 'Yellow fruit', 200),

('Pineapple', 'Yellow fruit', 250),

('Melon', 'Green fruit', 300);

```

我決定不在資料庫中存儲太多的資料,以避免對資料庫性能産生影響。我假設Postgres能夠緩存所有的資料,并且大部分時間都将用于網絡IO。

基準測試工具集

工具:k6(v0.42.0)

腳本:

```

import http from 'k6/http';

export default function () {

http.get('http://localhost:8080/goods');

}

```

每次運作測試的指令都是相同的:

```

k6 run --vus 1000 --duration 30s .\load_testing.js

```

由于我們将有一個簡單的端點,它将以 JSON 格式從 DB 傳回資料清單,是以我剛剛添加了一個擷取測試。 每個架構的所有測試都使用相同的腳本和指令運作。

NodeJS + Express Web 伺服器實作

NodeJS version:

```

node --version

v18.14.0

```

package.json:

```

{

"name": "node-api-postgres",

"version": "1.0.0",

"description": "RESTful API with Node.js, Express, and PostgreSQL",

"main": "index.js",

"license": "MIT",

"dependencies": {

"express": "^4.18.2",

"pg": "^8.9.0"

}

}

```

index.js:

```

const express = require('express')

const app = express()

const port = 8080

const { Pool } = require('pg')

const pool = new Pool({

host: 'localhost',

port: 5432,

user: 'postgres',

password: 'postgres',

database: 'goods',

max: 50,

idleTimeoutMillis: 30000,

connectionTimeoutMillis: 2000,

})

const getGoods = (request, response) => {

pool.query('SELECT * FROM goods', (error, results) => {

if (error) {

throw error

}

response.status(200).json(results.rows)

})

}

app.get('/goods', getGoods)

pool.connect((err, client, done) => {

console.log(err)

app.listen(port, () => {

console.log(`App running on port ${port}.`)

})

})

```

Spring WebFlux + R2DBC + Kotlin 實作

Java version:

```

java --version

openjdk 17.0.5 2022-10-18

OpenJDK Runtime Environment GraalVM CE 22.3.0 (build 17.0.5+8-jvmci-22.3-b08)

OpenJDK 64-Bit Server VM GraalVM CE 22.3.0 (build 17.0.5+8-jvmci-22.3-b08, mixed mode, sharing)

```

gradle file:

```

import org.jetbrains.kotlin.gradle.tasks.KotlinCompile

plugins {

id("org.springframework.boot") version "3.0.2"

id("io.spring.dependency-management") version "1.1.0"

id("org.graalvm.buildtools.native") version "0.9.18"

kotlin("jvm") version "1.7.22"

kotlin("plugin.spring") version "1.7.22"

}

group = "me.alekseinovikov.goods"

version = "0.0.1-SNAPSHOT"

java.sourceCompatibility = JavaVersion.VERSION_17

repositories {

mavenCentral()

}

dependencies {

implementation("org.springframework.boot:spring-boot-starter-data-r2dbc")

implementation("org.springframework.boot:spring-boot-starter-webflux")

implementation("com.fasterxml.jackson.module:jackson-module-kotlin")

implementation("io.projectreactor.kotlin:reactor-kotlin-extensions")

implementation("org.jetbrains.kotlin:kotlin-reflect")

implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")

implementation("org.jetbrains.kotlinx:kotlinx-coroutines-reactor")

runtimeOnly("org.postgresql:postgresql")

runtimeOnly("org.postgresql:r2dbc-postgresql")

testImplementation("org.springframework.boot:spring-boot-starter-test")

testImplementation("io.projectreactor:reactor-test")

}

tasks.withType<KotlinCompile> {

kotlinOptions {

freeCompilerArgs = listOf("-Xjsr305=strict")

jvmTarget = "17"

}

}

tasks.withType<Test> {

useJUnitPlatform()

}

```

application.properties:

```

spring.r2dbc.url=r2dbc:postgresql://postgres:postgres@localhost:5432/goods

spring.r2dbc.pool.enabled=true

spring.r2dbc.pool.max-size=50

spring.r2dbc.pool.max-idle-time=30s

spring.r2dbc.pool.max-create-connection-time=30s

```

Application code:

```

@SpringBootApplication

class GoodsApplication

fun main(args: Array<String>) {

runApplication<GoodsApplication>(*args)

}

@Table("goods")

class Good(

@field:Id

val id: Int,

@field:Column("name")

val name: String,

@field:Column("description")

val description: String,

@field:Column("price")

val price: Int

) {

}

interface GoodsRepository: R2dbcRepository<Good, Int> {

}

@RestController

class GoodsController(private val goodsRepository: GoodsRepository) {

@GetMapping("/goods")

suspend fun getGoods(): Flow<Good> = goodsRepository.findAll().asFlow()

}

```

為 fat jar 建構:

```

gradlew clean build

```

為 GraalVM 本機映像建構:

```

gradlew clean nativeCompile

```

Rust + Rocket 實作

cargo.toml:

```

[package]

name = "rust-goods"

version = "0.1.0"

edition = "2021"

[dependencies]

rocket = { version = "0.5.0-rc.2", features = ["secrets", "tls", "json"] }

serde_json = "1.0"

refinery = { version = "0.8", features = ["tokio-postgres"]}

[dependencies.rocket_db_pools]

version = "0.1.0-rc.2"

features = ["sqlx_postgres"]

```

Rocket.toml:

```

[default]

secret_key = "6XrKhVEP3gFMqmfhUzDdSYDthOLU442TjSCnz7sPEYE="

port = 8080

[default.databases.goods]

url = "postgres://postgres:postgres@localhost/goods"

max_connections = 50

```

main.rs:

```

#[macro_use]

extern crate rocket;

use rocket::serde::Serialize;

use rocket::serde::json::Json;

use rocket::State;

use rocket_db_pools::{Connection, Database};

use rocket_db_pools::sqlx::{self};

use rocket_db_pools::sqlx::{Error, Postgres, Row};

use rocket_db_pools::sqlx::postgres::PgRow;

use sqlx::FromRow;

#[derive(Serialize, Debug, PartialOrd, PartialEq, Clone)]

#[serde(crate = "rocket::serde")]

pub struct Good {

pub id: usize,

pub name: String,

pub description: String,

pub price: usize,

}

struct Repository;

impl Repository {

pub(crate) fn new() -> Repository {

Repository

}

pub(crate) async fn list(&self, mut db: Connection<Goods>) -> Vec<Good> {

sqlx::query_as::<Postgres, Good>("SELECT id, name, description, price FROM goods")

.fetch_all(&mut *db)

.await

.unwrap()

}

}

impl<'r> FromRow<'r, PgRow> for Good {

fn from_row(row: &'r PgRow) -> Result<Self, Error> {

let id: i64 = row.try_get("id")?;

let name = row.try_get("name")?;

let description = row.try_get("description")?;

let price: i32 = row.try_get("price")?;

Ok(Good { id: id as usize, name, description, price: price as usize })

}

}

#[get("/goods")]

async fn list(repository: &State<Repository>,

db: Connection<Goods>) -> Json<Vec<Good>> {

Json(repository

.list(db)

.await)

}

#[derive(Database)]

#[database("goods")]

struct Goods(sqlx::PgPool);

#[launch]

async fn rocket() -> _ {

let rocket = rocket::build();

rocket.attach(Goods::init())

.manage(Repository::new())

.mount("/", routes![

list,

])

}

```

編譯:

```

cargo build --release

```

Rust + Actix Web 實作

Cargo.toml:

```

[package]

name = "rust-actix-goods"

version = "0.1.0"

edition = "2021"

[dependencies]

actix-web = "4"

derive_more = "0.99.17"

config = "0.13.3"

log = "0.4"

env_logger = "0.10.0"

deadpool-postgres = { version = "0.10.5", features = ["serde"] }

dotenv = "0.15.0"

serde = { version = "1.0.152", features = ["derive"] }

tokio-pg-mapper = "0.2.0"

tokio-pg-mapper-derive = "0.2.0"

tokio-postgres = "0.7.7"

```

.env:

```

RUST_LOG=error

SERVER_ADDR=0.0.0.0:8080

PG.USER=postgres

PG.PASSWORD=postgres

PG.HOST=localhost

PG.PORT=5432

PG.DBNAME=goods

PG.POOL.MAX_SIZE=50

PG.SSL_MODE=Disable

```

main.rs:

```

mod config {

use serde::Deserialize;

#[derive(Debug, Default, Deserialize)]

pub struct ExampleConfig {

pub server_addr: String,

pub pg: deadpool_postgres::Config,

}

}

mod models {

use serde::{Deserialize, Serialize};

use tokio_pg_mapper_derive::PostgresMapper;

#[derive(Deserialize, PostgresMapper, Serialize)]

#[pg_mapper(table = "goods")]

pub struct Good {

pub id: i64,

pub name: String,

pub description: String,

pub price: i32,

}

}

mod db {

use deadpool_postgres::Client;

use tokio_pg_mapper::FromTokioPostgresRow;

use crate::models::Good;

pub async fn select_goods(client: &Client) -> Vec<Good> {

let _stmt = "SELECT id, name, description, price FROM goods";

let stmt = client.prepare(&_stmt).await.unwrap();

client

.query(

&stmt,

&[],

)

.await

.unwrap()

.iter()

.map(|row| Good::from_row_ref(row).unwrap())

.collect::<Vec<Good>>()

}

}

mod handlers {

use actix_web::{web, Error, HttpResponse};

use deadpool_postgres::{Client, Pool};

use crate::db;

pub async fn get_goods(

db_pool: web::Data<Pool>,

) -> Result<HttpResponse, Error> {

let client: Client = db_pool.get().await.unwrap();

let goods = db::select_goods(&client).await;

Ok(HttpResponse::Ok().json(goods))

}

}

use ::config::Config;

use actix_web::{web, App, HttpServer, middleware::Logger};

use dotenv::dotenv;

use handlers::get_goods;

use tokio_postgres::NoTls;

use crate::config::ExampleConfig;

#[actix_web::main]

async fn main() -> std::io::Result<()> {

dotenv().ok();

env_logger::init();

let config_ = Config::builder()

.add_source(::config::Environment::default())

.build()

.unwrap();

let config: ExampleConfig = config_.try_deserialize().unwrap();

let pool = config.pg.create_pool(None, NoTls).unwrap();

let server = HttpServer::new(move || {

App::new()

.wrap(Logger::default())

.app_data(web::Data::new(pool.clone()))

.service(web::resource("/goods").route(web::get().to(get_goods)))

})

.bind(config.server_addr.clone())?

.run();

println!("Server running at http://{}/", config.server_addr);

server.await

}

```

編譯:

```

cargo build --release

```

Go + Echo 實作

go.mod:

```

module goods-go

go 1.20

require (

github.com/labstack/echo/v4 v4.10.0

github.com/lib/pq v1.10.7

)

require (

github.com/golang-jwt/jwt v3.2.2+incompatible // indirect

github.com/labstack/gommon v0.4.0 // indirect

github.com/mattn/go-colorable v0.1.13 // indirect

github.com/mattn/go-isatty v0.0.16 // indirect

github.com/valyala/bytebufferpool v1.0.0 // indirect

github.com/valyala/fasttemplate v1.2.2 // indirect

golang.org/x/crypto v0.2.0 // indirect

golang.org/x/net v0.4.0 // indirect

golang.org/x/sys v0.3.0 // indirect

golang.org/x/text v0.5.0 // indirect

golang.org/x/time v0.2.0 // indirect

)

```

main.go:

```

package main

import (

"database/sql"

"fmt"

"github.com/labstack/echo/v4"

_ "github.com/lib/pq"

"log"

"net/http"

)

const (

host = "localhost"

port = 5432

user = "postgres"

password = "postgres"

dbname = "goods"

)

var db *sql.DB

type Good struct {

ID int `json:"id"`

Name string `json:"name"`

Description string `json:"description"`

Price int `json:"price"`

}

func getAllGoods(c echo.Context) error {

rows, err := db.Query("SELECT id, name, description, price FROM goods")

if err != nil {

return c.JSON(http.StatusInternalServerError, err)

}

defer rows.Close()

goods := make([]Good, 0)

for rows.Next() {

var good Good

if err := rows.Scan(&good.ID, &good.Name, &good.Description, &good.Price); err != nil {

log.Fatal(err)

}

goods = append(goods, good)

}

return c.JSON(http.StatusOK, goods)

}

func main() {

psqlInfo := fmt.Sprintf("host=%s port=%d user=%s "+

"password=%s dbname=%s sslmode=disable",

host, port, user, password, dbname)

var err error

db, err = sql.Open("postgres", psqlInfo)

if err != nil {

log.Fatal(err)

}

db.SetMaxOpenConns(50)

e := echo.New()

// Routes

e.GET("/goods", getAllGoods)

// Start server

e.Logger.Fatal(e.Start(":8080"))

}

```

編譯:

```

go build -ldflags "-s -w"

```

基準測試

最後,在我們對環境和實作有了一定了解後,我們準備開始進行基準測試。

結果比較:

Name| Requests Per Second| Requests Total| Memory Usage

- | - | - | -

Node Js| 3233.377739| 97772| 105MB

Spring JVM| 4457.39441| 134162| 675MB

Spring Native Image| 3854.41882| 116267| 211MB

Rust Rocket| 5592.44295| 168573| 48MB

Rust Actix| 5312.356065| 160310| 33.5MB

Go Echo| 13545.859602| 407254| 72.1MB

哎呀!當我想到這個基準測試的想法時,我認為Rust會是勝利者。第二名将由JVM和Go獲得。但事實的發展有點出乎意料。

如果我在代碼實作上犯了任何錯誤,請寫下評論告訴我。我盡力遵循官方文檔中的示例。從我的角度來看,我的所有代碼都是異步和非阻塞的。我檢查了幾次。但我是人,如果有更好的方法可以提高特定技術的性能,請告訴我。

Go是最快的。似乎Echo庫是其中一個原因。

Rust的速度可疑地慢。我嘗試了幾次,檢查了2個架構,但未能使其更快。

傳統JVM相當快(至少比NodeJS快),但仍然消耗大量記憶體。

GraalVM Native Image在減少記憶體消耗但保留了JVM的成熟工具集方面很有價值。

NodeJS是最慢的,也許是因為它的單線程事件循環。這裡沒有什麼新鮮的。

結論

我不是說這個特定的用例展示了技術或工具的整體性能。我知道不同的工具有不同的用途。但是,所有這些語言和運作時都用于Web伺服器開發,并在雲伺服器中運作。是以,我決定進行這個基準測試,以了解使用不同技術堆棧開發簡單微服務時的速度和資源容忍程度。

對我來說,結果有些令人震驚,因為我預計Rust會獲勝。但Go向我展示了這門語言和Echo架構在編寫具有大量IO的簡單微服務方面非常出色。

遺憾的是,JVM似乎無法達到相同的性能/資源消耗,進而在開發雲Web服務方面變得不那麼吸引人。但GraalVM Native Image給了它第二次機會。它的速度不及Go或Rust,但減少了對記憶體的需求。

是以,如果你能雇傭很多Gopher來參與你的下一個項目,你可能能在基礎設施上節省一些錢。

如果你喜歡我的文章,點贊,關注,轉發!