0
点赞
收藏
分享

微信扫一扫

2023年Web服务器基准测试:NodeJS vs Java vs Rust vs Go

文风起武 2023-05-29 阅读 82

现在是2023年,是时候进行一次新的Web服务器基准测试了!

结果对我来说有些出乎意料!

image.png

一个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服务器都将使用此数量以保持相同的条件。

数据库初始化:

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来参与你的下一个项目,你可能能在基础设施上节省一些钱。

如果你喜欢我的文章,点赞,关注,转发!

举报

相关推荐

0 条评论