SpringBoot 物理線程、虛擬線程、Webflux 性能全面對比!

大量的文章評估了一系列技術(包括 Node.js、Deno、Bun、Rust、Go、Spring、Python 等)在簡單的 “hello world” 場景中的性能。雖然這些文章獲得了好評,但有一個共同點:忽略了現實場景開發中的複雜性。

本文旨在通過現實場景的視角剖析各種技術,在這種特殊情況下,我們深入研究以下常見用例:

  1. 從 authorization header 中提取一個 JWT。

  2. 驗證 JWT 並從聲明中提取用戶的電子郵件。

  3. 使用提取的電子郵件執行 MySQL 查詢。

  4. 最後,返回用戶的記錄。

雖然這個場景看起來似乎也很簡單,但它概括了 Web 開發領域中經常遇到的現實挑戰。

介紹

在本文中,我們將深入探討所有同級產品之間的友好比較,即具有**「物理線程、虛擬線程和 Webflux 的 SpringBoot」**,重點關注它們在特定用例場景中的性能。我們已經探索了標準 SpringBoot 應用程序如何與 webflux 相媲美,但現在,我們引入一個關鍵的區別:

帶有虛擬線程的 Spring Boot

我們熟悉 SpringBoot,但有一點不同——它在虛擬線程而不是傳統的物理線程上運行。虛擬線程是併發領域的遊戲規則改變者。這些輕量級線程簡化了開發、維護和調試高吞吐量併發應用程序的複雜任務。

雖然虛擬線程仍然在底層操作系統線程上運行,但它們帶來了顯着的效率改進。當虛擬線程遇到阻塞 I/O 操作時,Java 運行時會暫時掛起它,從而釋放關聯的操作系統線程來爲其他虛擬線程提供服務。這個優雅的解決方案優化了資源分配並增強了整體應用程序響應能力。

考慮到這些有趣的設置,讓我們更深入地研究我們的性能比較。撰寫本文是爲了解決最常見的請求之一,即查看物理、虛擬和 Webflux 在實際用例中的比較。

測試環境及軟件版本

我們的性能測試是在配備 16GB RAM 的 MacBook Pro M1 上進行的,確保了可靠的測試平臺。用於這些測試的軟件堆棧包括:

負載測試和 JWT

爲了評估我們的應用程序在不同負載下的性能,我們使用了開源負載測試工具 Bombardier。我們的測試場景涉及預先創建的 100000 個 JWT 列表。在測試過程中,Bombardier 從該池中隨機選擇 JWT,並將它們包含在 HTTP 請求的授權標頭中。

Bombardier 開源地址: https://github.com/codesenberg/bombardier/

MySQL 數據庫架構

用於這些性能測試的 MySQL 數據庫有一個名爲 users 的表。該表設計有 6 列,足以模擬我們應用程序中的真實數據交互,使我們能夠評估它們的響應能力和可擴展性。

mysql> desc users;
+--------+--------------+------+-----+---------+-------+
| Field  | Type         | Null | Key | Default | Extra |
+--------+--------------+------+-----+---------+-------+
| email  | varchar(255) | NO   | PRI | NULL    |       |
| first  | varchar(255) | YES  |     | NULL    |       |
| last   | varchar(255) | YES  |     | NULL    |       |
| city   | varchar(255) | YES  |     | NULL    |       |
| county | varchar(255) | YES  |     | NULL    |       |
| age    | int          | YES  |     | NULL    |       |
+--------+--------------+------+-----+---------+-------+
6 rows in set (0.00 sec)

用戶數據庫已準備好包含 100000 條用戶記錄的初始數據集。

mysql> select count(*) from users;
+----------+
| count(*) |
+----------+
|    99999 |
+----------+
1 row in set (0.01 sec)

在我們對 SpringBoot 物理線程、虛擬線程和 Webflux 進行友好性能評估的背景下,瞭解關鍵的數據關係至關重要。具體來說,在 JSON Web Token(JWT)有效負載中,每個電子郵件條目直接對應於存儲在 MySQL 數據庫中的一條用戶記錄。

代碼

SpringBoot(物理線程)

配置信息

server.port=3000
spring.datasource.url= jdbc:mysql://localhost:3306/testdb?useSSL=false&allowPublicKeyRetrieval=true
spring.datasource.username= dbuser
spring.datasource.password= dbpwd
spring.jpa.hibernate.ddl-auto= update
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQLDialect

實體類

package com.example.demo;

import jakarta.persistence.Entity;
import jakarta.persistence.Table;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;

@Entity
@Table(name = "users")
public class User {
  @Id
  private String email;

  private String first;

  private String last;

  private String city;

  private String county;

  private int age;

  public String getId() {
    return email;
  }

  public void setId(String email) {
    this.email = email;
  }

  public String getFirst() {
    return first;
  }

  public void setFirst(String name) {
    this.first = name;
  }

  public String getLast() {
    return last;
  }

  public void setLast(String name) {
    this.last = name;
  }

  public String getEmail() {
    return email;
  }

  public void setEmail(String email) {
    this.email = email;
  }

  public String getCity() {
    return city;
  }

  public void setCity(String city) {
    this.city = city;
  }

  public String getCounty() {
    return county;
  }

  public void setCounty(String county) {
    this.county = county;
  }

  public int getAge() {
    return age;
  }

  public void setAge(int age) {
    this.age = age;
  }
}

啓動類

package com.example.demo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.web.embedded.tomcat.TomcatProtocolHandlerCustomizer;
import org.springframework.context.annotation.Bean;

@SpringBootApplication
public class UserApplication {

    public static void main(String[] args) {
        SpringApplication.run(UserApplication.class, args);
    }
}

Controller 層

package com.example.demo;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.http.ResponseEntity;
import org.springframework.http.HttpStatus;
import org.springframework.http.HttpHeaders;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.beans.factory.annotation.Autowired;
import java.util.Optional;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.Jws;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.security.Keys;
import java.security.Key;
import com.example.demo.UserRepository;
import com.example.demo.User;

@RestController
public class UserController {

    @Autowired
    UserRepository userRepository;

    private SignatureAlgorithm sa = SignatureAlgorithm.HS256;
    private String jwtSecret = System.getenv("JWT_SECRET");

    @GetMapping("/")
    public User handleRequest(@RequestHeader(HttpHeaders.AUTHORIZATION) String authHdr) {
        String jwtString = authHdr.replace("Bearer","");
        Claims claims = Jwts.parser()
            .setSigningKey(jwtSecret.getBytes())
            .parseClaimsJws(jwtString).getBody();

        Optional<User> user = userRepository.findById((String)claims.get("email"));
        return user.get();
    }
}

接口類

package com.example.demo;

import org.springframework.data.repository.CrudRepository;
import com.example.demo.User;

public interface UserRepository extends CrudRepository<User, String> {

}

Springboot(虛擬線程)

其餘代碼基本照搬上述 「物理線程」, 啓動類修改如下:

package com.example.demo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.web.embedded.tomcat.TomcatProtocolHandlerCustomizer;
import org.springframework.context.annotation.Bean;
import java.util.concurrent.Executors;

@SpringBootApplication
public class UserApplication {

    public static void main(String[] args) {
        SpringApplication.run(UserApplication.class, args);
    }

    @Bean
    public TomcatProtocolHandlerCustomizer<?> protocolHandlerVirtualThreadExecutorCustomizer() {
        return protocolHandler -> {
            protocolHandler.setExecutor(Executors.newVirtualThreadPerTaskExecutor());
        };
    }
}

SpringBoot(webflux)

server.port=3000
spring.r2dbc.url=r2dbc:mysql://localhost:3306/testdb?allowPublicKeyRetrieval=true&ssl=false
spring.r2dbc.username=dbuser
spring.r2dbc.password=dbpwd
spring.r2dbc.pool.initial-size=10
spring.r2dbc.pool.max-size=10

啓動類

package webfluxdemo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.core.io.ClassPathResource;
import org.springframework.r2dbc.connection.init.ConnectionFactoryInitializer;
import org.springframework.r2dbc.connection.init.ResourceDatabasePopulator;
import org.springframework.web.reactive.config.EnableWebFlux;

import io.r2dbc.spi.ConnectionFactory;

@EnableWebFlux
@SpringBootApplication
public class UserApplication {

  public static void main(String[] args) {
    SpringApplication.run(UserApplication.class, args);
  }

}

Controller 層代碼

package webfluxdemo;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.http.HttpHeaders;

import webfluxdemo.User;
import webfluxdemo.UserService;

import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.Jws;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.security.Keys;
import java.security.Key;

import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

@RestController
@RequestMapping("/")
public class UserController {
  @Autowired
  UserService userService;

  private SignatureAlgorithm sa = SignatureAlgorithm.HS256;
  private String jwtSecret = System.getenv("JWT_SECRET");

  @GetMapping("/")
  @ResponseStatus(HttpStatus.OK)
  public Mono<User> getUserById(@RequestHeader(HttpHeaders.AUTHORIZATION) String authHdr) {
    String jwtString = authHdr.replace("Bearer","");
    Claims claims = Jwts.parser()
        .setSigningKey(jwtSecret.getBytes())
        .parseClaimsJws(jwtString).getBody();
    return userService.findById((String)claims.get("email"));
  }
}

接口類

package webfluxdemo;

import org.springframework.data.r2dbc.repository.R2dbcRepository;
import org.springframework.stereotype.Repository;

import webfluxdemo.User;

public interface UserRepository extends R2dbcRepository<User, String> {

}

Service 層代碼

package webfluxdemo;

import java.util.Optional;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import webfluxdemo.User;
import webfluxdemo.UserRepository;

import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

@Service
public class UserService {

  @Autowired
  UserRepository userRepository;

  public Mono<User> findById(String id) {
    return userRepository.findById(id);
  }
}

結果

爲了評估性能,我們進行了一系列嚴格的測試。每個測試由 100 萬個請求組成,我們評估了它們在不同併發連接級別(50、100 和 300)下的性能。

現在,讓我們深入研究結果,以圖表形式呈現:

所用時間對比每秒請求數最小延遲 10% 延遲 25% 延遲平均延遲中位數延遲 75% 延遲 90% 延遲 99% 延遲最高延遲平均 CPU 使用率平均內存使用率

分析

在此設置中,即使用 MySQL 驅動程序時,虛擬線程提供的性能最低、Webflux 保持遙遙領先。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/uZaUVS-t5zjOtCI3yVF9Ug