性能優(yōu)化!七個(gè)策略,讓Spring Boot 處理每秒百萬請求
環(huán)境:SpringBoot3.4.2
1. 簡介
在實(shí)施任何優(yōu)化前,我首先明確了性能基準(zhǔn)。這一步至關(guān)重要——若不清楚起點(diǎn),便無法衡量進(jìn)展,也無法定位最關(guān)鍵的改進(jìn)方向。以下是我們的初始性能指標(biāo)概況:
最大吞吐量:50,000 次請求/秒
平均響應(yīng)時(shí)間:350 毫秒
95 分位響應(yīng)時(shí)間:850 毫秒
峰值時(shí)段 CPU 使用率:85%-95%
內(nèi)存占用:堆內(nèi)存使用達(dá)可用空間的 75%
數(shù)據(jù)庫連接:頻繁達(dá)到連接池上限(100 )
線程池飽和:線程池資源經(jīng)常耗盡
以上指標(biāo)通過如下的工具進(jìn)行收集所得:
- JMeter用于負(fù)載測試,確定基礎(chǔ)吞吐量數(shù)值
- Micrometer + Prometheus + Grafana實(shí)現(xiàn)實(shí)時(shí)監(jiān)控與可視化
- JProfiler深入分析代碼中的性能熱點(diǎn)
- 火焰圖(Flame graphs)定位 CPU 密集型方法
根據(jù)上面的指標(biāo)總結(jié)如下性能瓶頸:
- 線程池飽和默認(rèn)的 Tomcat 連接器已達(dá)到性能上限
- 數(shù)據(jù)庫連接爭用HikariCP 連接池配置未針對實(shí)際負(fù)載優(yōu)化
- 序列化效率低下Jackson 在請求/響應(yīng)處理中消耗大量 CPU 資源
- 阻塞式 I/O 操作尤其在調(diào)用外部服務(wù)時(shí)表現(xiàn)明顯
- 內(nèi)存壓力過度對象創(chuàng)建導(dǎo)致頻繁的 GC 停頓
接下來,我們將逐一的解決上面的問題。
2. 性能優(yōu)化
2.1 使用響應(yīng)式編程
阻塞方式:
@Service
public class ProductService {
private final ProductRepository productRepository ;
public ProductService(ProductRepository productRepository) {
this.productRepository = productRepository ;
}
public Product getProductById(Long id) {
return repository.findById(id)
.orElseThrow(() -> new ProductNotFoundException(id)) ;
}
}
基于響應(yīng)式改造:
@Service
public class ProductService {
private final ProductRepository productRepository ;
public ProductService(ProductRepository productRepository) {
this.productRepository = productRepository ;
}
public Product getProductById(Long id) {
public Mono<Product> getProductById(Long id) {
return productRepository.findById(id)
.switchIfEmpty(Mono.error(new ProductNotFoundException(id)));
}
}
}
同時(shí)Controller層也需要改造
@RestController
@RequestMapping("/products")
public class ProductController {
private final ProductService productService;
public ProductController(ProductService productService) {
this.productService = productService ;
}
@GetMapping("/{id}")
public Mono<ResponseEntity<Product>> getProduct(@PathVariable Long id) {
return service.getProductById(id)
.map(ResponseEntity::ok)
.defaultIfEmpty(ResponseEntity.notFound().build());
}
}
注意,對應(yīng)依賴方面你需要引入如下相關(guān)的依賴:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-r2dbc</artifactId>
</dependency>
<!--基于響應(yīng)式的mysql驅(qū)動(dòng)包-->
<dependency>
<groupId>com.github.jasync-sql</groupId>
<artifactId>jasync-r2dbc-mysql</artifactId>
<version>2.1.24</version>
</dependency>
總結(jié):僅這一項(xiàng)改動(dòng)便使吞吐量翻倍,其核心在于更高效地利用線程資源。WebFlux 不再為每個(gè)請求分配獨(dú)立線程,而是通過少量線程處理海量并發(fā)請求。
有關(guān)響應(yīng)式編程,請查看下面文章:
新一代WebFlux框架核心技術(shù)Reactor響應(yīng)式編程基本用法
響應(yīng)式編程引領(lǐng)未來:WebFlux與R2DBC的完美結(jié)合實(shí)現(xiàn)數(shù)據(jù)庫編程
SpringBoot3虛擬線程 & 反應(yīng)式(WebFlux) & 傳統(tǒng)Tomcat線程池 性能對比
新一代web框架WebFlux到底要不要學(xué)?
2.2 數(shù)據(jù)庫優(yōu)化
數(shù)據(jù)庫交互成為下一個(gè)關(guān)鍵性能瓶頸。我采用了三管齊下的優(yōu)化策略:
- 查詢優(yōu)化
我使用 Spring Data 的 @Query 注解取代了低效的自動(dòng)生成查詢:
優(yōu)化前:
List<Order> findByUserIdAndStatusAndCreateTimeBetween(
Long userId, OrderStatus status,
LocalDate start, LocalDate end) ;
優(yōu)化后:
@Query("SELECT o FROM Order o WHERE o.userId = :userId " +
"AND o.status = :status " +
"AND o.createdDate BETWEEN :start AND :end " +
"ORDER BY o.createdDate DESC")
List<Order> findUserOrdersInDateRange(
@Param("userId") Long userId,
@Param("status") OrderStatus status,
@Param("start") LocalDate start,
@Param("end") LocalDate end) ;
使用 Hibernate 的 @BatchSize 優(yōu)化 N+1 查詢:
@Entity
@Table(name = "t_order")
public class Order {
// ...
@OneToMany(mappedBy = "order", fetch = FetchType.EAGER)
// 批量抓取數(shù)據(jù)
@BatchSize(size = 30)
private Set<OrderItem> items ;
}
- 連接池優(yōu)化
HikariCP 的默認(rèn)設(shè)置造成了連接爭用。經(jīng)過大量測試,我得出了這樣的配置(實(shí)際要根據(jù)自己的環(huán)境):
spring:
datasource:
hikari:
maximum-pool-size: 100
minimum-idle: 50
idle-timeout: 30000
connection-timeout: 2000
max-lifetime: 1800000
關(guān)鍵的一點(diǎn)是,連接數(shù)并不總是越多越好;這里的hikari可不支持響應(yīng)式。所以,我們應(yīng)該吧響應(yīng)式與阻塞式2種方式進(jìn)行分開處理。
基于響應(yīng)式數(shù)據(jù)庫的配置如下:
spring:
r2dbc:
pool:
initialSize: 30
maxSize: 10
max-acquire-time: 30s
max-idle-time: 30m
- 使用緩存
對于頻繁訪問的數(shù)據(jù)添加了 Redis 緩存。
// 開啟
@Configuration
@EnableCaching
public class CacheConfig {
}
// 使用緩存
@Service
public class ProductService {
@Cacheable(value = "products", key = "#id")
public Mono<Product> getProductById(Long id) {
return repository.findById(id)
.switchIfEmpty(Mono.error(new ProductNotFoundException(id)));
}
@CacheEvict(value = "products", key = "#product.id")
public Mono<Product> updateProduct(Product product) {
return repository.save(product) ;
}
}
配置緩存:
spring:
cache:
type: redis
redis:
cache-null-values: false
time-to-live: 120m
需要更多個(gè)性化配置,可以自定義RedisCacheManager。
2.3 序列化優(yōu)化
通過優(yōu)化jackson序列化,可以明顯減少CPU的占用。
@Configuration
public class JacksonConfig {
@Bean
public ObjectMapper objectMapper() {
ObjectMapper mapper = new ObjectMapper() ;
// 啟用 Afterburner 模塊以加速序列化
mapper.registerModule(new AfterburnerModule()) ;
// 僅僅序列化不為空的字段
mapper.setSerializationInclusion(Include.NON_NULL) ;
// 禁用不需要的功能
mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) ;
mapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES) ;
return mapper ;
}
}
如果對部分接口要求非常高,那么可以采用Protocol Buffers。
關(guān)于Protocol Buffers的使用,請查看下面文章:
接口優(yōu)化!Spring Boot 集成 Protobuf 并使用 RestTemplate 實(shí)現(xiàn)微服務(wù)間通信
基于 Spring Boot 實(shí)現(xiàn)自定義二進(jìn)制數(shù)據(jù)傳輸協(xié)議
2.4 線程池&連接優(yōu)化
有了 WebFlux,我們需要調(diào)整 Netty 的事件循環(huán)設(shè)置:
spring:
reactor:
netty:
worker:
count: 32 #工作線程數(shù)(2 x CPU cores)
connection:
provider:
pool:
max-connections: 10000
acquire-timeout: 5000
對于使用 Spring MVC 的,調(diào)整 Tomcat 連接器:
server:
tomcat:
threads:
max: 200
min-spare: 50
max-connections: 8192
accept-count: 100
connection-timeout: 5000
這些設(shè)置使我們能夠以較少的資源處理更多的并發(fā)連接。
2.5 基于 Kubernetes 的橫向擴(kuò)展:終極解決方案
通過橫向擴(kuò)展提升系統(tǒng)容量。將應(yīng)用容器化后部署至 Kubernetes 集群。
FROM openjdk:17-slim
COPY target/product-app.jar app.jar
ENV JAVA_OPTS="-XX:+UseG1GC -XX:MaxGCPauseMillis=100 -XX:+ParallelRefProcEnabled"
ENTRYPOINT exec java $JAVA_OPTS -jar /app.jar
然后根據(jù) CPU 利用率配置自動(dòng)縮放:
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: product-hpa
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
#目標(biāo) Deployment 的名稱(即需要被擴(kuò)縮容的應(yīng)用)
name: product-app
#副本數(shù)范圍限制5~20
minReplicas: 5
maxReplicas: 20
#定義觸發(fā)擴(kuò)縮容的指標(biāo)規(guī)則
metrics:
- type: Resource #使用資源指標(biāo)(如 CPU、內(nèi)存)
resource:
name: cpu #監(jiān)控 CPU 資源使用率
target:
type: Utilization #指標(biāo)類型為“利用率百分比”
#當(dāng)持續(xù)超過 70% 時(shí)觸發(fā)擴(kuò)縮容
averageUtilization: 70
利用 Istio 實(shí)施服務(wù)網(wǎng)格功能,以實(shí)現(xiàn)更好的流量管理:
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
name: product-vs
spec:
hosts:
- product-service # 目標(biāo)服務(wù)名(需與 Istio 服務(wù)網(wǎng)格中注冊的名稱一致)
http: # 定義 HTTP 協(xié)議相關(guān)的流量規(guī)則(支持路由、重試、超時(shí)等策略)
- route: # 配置流量路由規(guī)則
- destination: # 指定流量的實(shí)際目的地
host: product-service # 目標(biāo)服務(wù)名
retries: # 設(shè)置請求失敗時(shí)的重試策略
attempts: 3 # 最大重試次數(shù)(首次請求 + 3次重試 = 最多4次嘗試)
perTryTimeout: 2s # 單次請求(含重試)的超時(shí)時(shí)間(2秒無響應(yīng)則中斷)
timeout: 5s # 整個(gè)請求(所有重試?yán)塾?jì))的全局超時(shí)時(shí)間(超過5秒直接失?。?/code>
這使我們能夠高效處理流量高峰,同時(shí)保持彈性。