编程教育资源分享平台

网站首页 > 后端开发 正文

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

luoriw 2024-02-01 14:31:11 后端开发 13 ℃ 0 评论

现在是2023年,是时候进行一次新的Web服务器基准测试了!
结果对我来说有些出乎意料!


一个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。

基准测试工具集

import http from 'k6/http';
export default function () {
http.get('http://localhost:8080/goods');
}

工具:k6(v0.42.0)

脚本:

每次运行测试的命令都是相同的:

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来参与你的下一个项目,你可能能在基础设施上节省一些钱。
如果你喜欢我的文章,点赞,关注,转发!

Tags:

本文暂时没有评论,来添加一个吧(●'◡'●)

欢迎 发表评论:

最近发表
标签列表
最新留言