跳至主要內容

升级到 Spring Boot 3.5,我们的云成本减少了 45%

DD编辑部原创Spring BootSpring Boot大约 11 分钟

升级到 Spring Boot 3.5,我们的云成本减少了 45%

上个季度,我带着越来越焦虑的心情盯着我们公司的 AWS 账单。尽管服务的客户数量基本持平,但我们的云成本在过去一年里持续攀升。作为负责后端基础设施的技术负责人,我必须在下一个预算评审前找到解决方案。

我没想到的是,一次看似例行的 Spring Boot 升级,加上一些有针对性的配置调整,竟然让我们的 AWS 开支几乎减半。以下是我们如何发现问题、实施变更,并彻底提升应用资源效率的全过程。

云成本加剧的危机

我们公司运营着一个 SaaS 平台,帮助中型企业管理多渠道的库存。我们不是巨头(大约 5,000 个客户,每天处理 120,000 个订单—),但 AWS 账单已经涨到了令人头疼的每月 27,000 美元。_

CFO 在季度会议上说:"我们要么削减成本,要么涨价。"但在当前市场,涨价根本不现实。"

挑战很明确:在不影响应用性能和可靠性的前提下,找到显著的基础设施节省空间。但从哪里下手?

找出资源消耗大户

我们的应用架构对现代 Java 项目来说很常见:

  • Spring Boot 2.7 后端服务
  • PostgreSQL RDS 实例做持久化存储
  • Redis 做缓存
  • EC2 实例组成自动伸缩组,挂在负载均衡器后面

第一步是搞清楚钱都花在哪了。我设置了详细的成本分配标签,并用 AWS Cost Explorer 分析了我们的支出模式。

结果令人惊讶:

  • EC2 实例:占 58%
  • RDS PostgreSQL:占 25%
  • 数据传输:占 12%
  • 其他服务(Redis、S3 等):占 5%

EC2 成本成了首要目标,深入分析后发现更有意思的现象:我们运行的实例数量远超实际流量需求。自动伸缩经常被触发,启动的新实例大多处于低利用率。

资源利用率之谜

监控数据显示出一种奇怪的模式。每台 EC2 实例启动时各项指标都很健康,但随后会逐步出现:

  1. CPU 利用率逐步升高(最终达到 70–80%)
  2. JVM 堆内存持续增长
  3. 响应时间变慢
  4. 吞吐量下降

大约 12 小时后,指标恶化到自动伸缩被触发,启动新实例。但这些新实例并没有处理更多流量,只是在弥补已有实例性能下降的问题。

"看起来像是某种资源泄漏。"我对团队说,"但不是典型的内存泄漏,而是应用效率在逐步下降。"

数据库连接的真相

开启详细性能监控和日志分析后,我们发现了惊人的问题:应用创建了过多的数据库连接,且很多连接没有被正确关闭。

典型的 API 请求流程应该是:

Request → Controller → Service → Repository → Database

但实际连接使用却是:

初始请求 → 打开 5 个 DB 连接 → 关闭 3 个连接 → 泄漏 2 个连接

这些泄漏的连接会不断积累,直到连接池耗尽,导致性能下降,最终触发自动伸缩。

罪魁祸首?我们的应用用的是 Spring Data JPA,并有一些自定义的 repository 实现,没有正确管理事务边界和连接生命周期。

Spring Boot 3.5 的启示

就在这时,Spring Boot 3.5 发布了,带来了数据库连接管理和 ORM 性能的多项改进。发布说明中提到"资源利用率显著提升",这引起了我的注意。

进一步研究后,我发现 Spring Boot 3.5 包含:

  1. 更强的连接池集成
  2. 改进的事务管理
  3. 更智能的资源清理
  4. 更好地处理懒加载场景

升级能解决我们的问题吗?值得一试。

关键配置变更带来的转机

我们决定升级到 Spring Boot 3.5,并针对数据库连接管理做了几项关键配置调整。以下是最有影响力的具体变更:

1. 连接池优化

我们将默认的 HikariCP 配置调整为更适合我们负载的参数:

# 之前
spring.datasource.type=com.zaxxer.hikari.HikariDataSource
spring.datasource.hikari.maximum-pool-size=10
spring.datasource.hikari.minimum-idle=10
# 之后
spring.datasource.type=com.zaxxer.hikari.HikariDataSource
spring.datasource.hikari.maximum-pool-size=20
spring.datasource.hikari.minimum-idle=5
spring.datasource.hikari.idle-timeout=120000
spring.datasource.hikari.max-lifetime=1800000
spring.datasource.hikari.connection-timeout=30000
spring.datasource.hikari.leak-detection-threshold=60000

关键新增项是 leak-detection-threshold,帮助我们识别和记录潜在的连接泄漏。降低 minimum-idle 也避免了在低峰期保留过多空闲连接。

2. 事务管理改进

我们优化了事务管理配置:

# 之前
spring.jpa.properties.hibernate.connection.provider_disables_autocommit=true
# 之后
spring.jpa.properties.hibernate.connection.provider_disables_autocommit=true
spring.jpa.properties.hibernate.connection.handling_mode=DELAYED_ACQUISITION_AND_RELEASE_AFTER_TRANSACTION
spring.transaction.default-timeout=30

DELAYED_ACQUISITION_AND_RELEASE_AFTER_TRANSACTION 这个设置堪称"神器",它确保数据库连接在真正需要时才获取,并在事务结束后立即释放。

3. JPA 查询优化

我们做了多项 JPA 和 Hibernate 优化:

# 批量处理提升性能
spring.jpa.properties.hibernate.jdbc.batch_size=50
spring.jpa.properties.hibernate.order_inserts=true
spring.jpa.properties.hibernate.order_updates=true
# 查询优化
spring.jpa.properties.hibernate.query.in_clause_parameter_padding=true
spring.jpa.properties.hibernate.query.fail_on_pagination_over_collection_fetch=true
spring.jpa.properties.hibernate.default_batch_fetch_size=30
# Spring Boot 3.5 新增
spring.jpa.properties.hibernate.query.optimizer.enabled=true

Spring Boot 3.5 的新查询优化器,显著减少了常用操作所需的数据库查询次数。

4. 语句缓存

我们开启了预编译语句缓存,对数据库性能提升明显:

spring.datasource.hikari.data-source-properties.prepStmtCacheSize=250
spring.datasource.hikari.data-source-properties.prepStmtCacheSqlLimit=2048
spring.datasource.hikari.data-source-properties.cachePrepStmts=true
spring.datasource.hikari.data-source-properties.useServerPrepStmts=true

这些设置确保常用 SQL 语句被缓存,减少了语句准备的开销。

5. 针对场景的连接管理

对于最消耗资源的接口,我们实现了针对性的事务和连接设置:

@Service
public class InventorySyncService {
    
    @Transactional(timeout = 60)
    @QueryHints(@QueryHint(name = "org.hibernate.fetchSize", value = "100"))
    public void synchronizeInventory() {
        // 资源密集型操作
    }
}

这样可以针对不同操作设置不同的事务和抓取行为,而不是一刀切。

实施与即时成效

这些变更需要仔细测试,因为数据库连接问题往往隐蔽且依赖环境。我们的做法:

  1. 搭建与生产一致的预发环境
  2. 升级到 Spring Boot 3.5 并应用新配置
  3. 进行大量压力测试验证变更
  4. 对比前后连接使用模式

初步结果令人振奋:

  • 单实例平均数据库连接数从 7.8 降到 3.2
  • 连接获取时间降低 68%
  • 72 小时压力测试期间未检测到连接泄漏

但真正的考验在生产环境。

上线与 AWS 成本影响

我们在维护窗口将变更上线,并立即开始监控。实际效果比预发还要明显:

  • EC2 集群平均 CPU 利用率从 62% 降到 28%
  • JVM 垃圾回收暂停减少 76%
  • 单实例吞吐量提升 120%
  • 平均响应时间从 187ms 降到 74ms

最重要的是,自动伸缩事件几乎消失。原本高峰期需要 20–24 台 EC2,现在只需 9–10 台即可稳定运行。

AWS 成本立竿见影:

上线前月账单:$27,000
上线后月账单:$14,850
总节省:$12,150(45%)

节省明细:

  • EC2 成本降低 58%(实例数减少,CPU 利用率下降)
  • RDS 成本降低 32%(连接数减少,查询效率提升)
  • 数据传输成本降低 15%(API 响应更高效)

技术细节解析:为何如此有效

要真正理解这些变更为何如此有效,必须了解 Spring Boot 3.5 的技术改进,以及我们的配置如何充分利用了这些特性。

连接生命周期的改进

Spring Boot 3.5 从根本上改变了数据库连接的管理方式。旧版本往往过早获取连接且持有时间过长。新设置 DELAYED_ACQUISITION_AND_RELEASE_AFTER_TRANSACTION 保证:

  1. 只有在即将执行 SQL 时才获取连接
  2. 事务结束后立即释放连接

这样大大缩短了连接持有时间,让更小的连接池也能应对同样的负载。

查询优化引擎

Spring Boot 3.5 的新查询优化器解决了多种常见低效问题:

  1. N+1 查询预防:检测潜在 N+1 查询模式并自动转为高效批量查询
  2. 连接优化:分析实体关系,选择更优的连接策略
  3. 抓取大小调优:根据结果集大小自动调整 JDBC fetch size

我们的配置已启用并调优了该优化器:

spring.jpa.properties.hibernate.query.optimizer.enabled=true
spring.jpa.properties.hibernate.default_batch_fetch_size=30

语句缓存

数据库语句准备开销不容小觑。开启语句缓存后,常用查询可跳过准备阶段:

spring.datasource.hikari.data-source-properties.cachePrepStmts=true
spring.datasource.hikari.data-source-properties.prepStmtCacheSize=250

对于我们这种查询模式较为固定的应用,显著降低了数据库 CPU 占用并提升响应速度。

泄漏检测与预防

泄漏检测配置对定位剩余问题至关重要:

spring.datasource.hikari.leak-detection-threshold=60000

该设置会在连接持有超时后记录详细堆栈,帮助我们定位和修复代码中的连接管理问题。

代码优化

虽然配置变更带来了主要提升,我们也做了几项代码优化作为补充:

1. 简化 Repository 方法

我们重构了复杂的 repository 方法,充分利用 Spring Data JPA 的查询派生能力:

// 之前
@Query("SELECT p FROM Product p LEFT JOIN FETCH p.variants v WHERE p.sku = :sku")
Product findBySku(@Param("sku") String sku);
// 之后
Product findBySku(String sku);

在 Spring Boot 3.5 的新查询优化器下,简化后的方法性能更好,框架能做出更优的抓取决策。

2. 显式事务边界

我们让事务边界更明确,尤其是只读操作:

@Transactional(readOnly = true)
public ProductDTO getProduct(String sku) {
    Product product = productRepository.findBySku(sku);
    return mapper.toDTO(product);
}

readOnly = true 提示 Spring 和数据库进一步优化查询执行。

3. 批量操作异步处理

对于资源密集型操作,我们采用异步处理并控制资源消耗:

@Async("taskExecutor")
public CompletableFuture<Void> processInventoryUpdates(List<InventoryUpdate> updates) {
    // 处理逻辑
    return CompletableFuture.completedFuture(null);
}
@Configuration
public class AsyncConfig {
    @Bean
    public Executor taskExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(5);
        executor.setMaxPoolSize(10);
        executor.setQueueCapacity(25);
        return executor;
    }
}

这样保证批量操作不会占用过多数据库连接或 CPU。

总结与最佳实践

在这个过程中,我们总结出几条对其他团队也有参考价值的最佳实践:

1. 连接池大小要与数据库连接数匹配

最大连接池大小应按如下公式计算:

(最大 DB 连接数 - 预留连接数) / 应用实例数

比如 RDS 最大连接 100,5 个应用实例:

(100 - 5 预留) / 5 实例 = 每实例 19 个连接

我们取整到 20,留有余地。

2. 监控并记录连接使用

开启详细连接监控:

spring.datasource.hikari.metrics.registry-type=log
logging.level.com.zaxxer.hikari=DEBUG

这样能清晰看到连接使用模式,便于排查问题。

3. 区分环境配置

不同环境需求不同。我们为不同环境设置了专属 profile:

# 开发环境
spring.datasource.hikari.maximum-pool-size=5
spring.datasource.hikari.minimum-idle=1
# 生产环境
spring.datasource.hikari.maximum-pool-size=20
spring.datasource.hikari.minimum-idle=5

这样开发环境不会占用多余资源。

4. 定期审查查询性能

我们实现了 SQL 性能监控,记录慢查询及其执行计划:

spring.jpa.properties.hibernate.generate_statistics=true
spring.jpa.properties.hibernate.session.events.log.LOG_QUERIES_SLOWER_THAN_MS=250

帮助我们及时发现和优化最耗资源的查询。

意外收获:超越成本的好处

虽然降低 AWS 成本是首要目标,但我们还发现了不少额外好处:

1. 开发体验提升

连接管理更好,错误信息更清晰,开发过程中遇到的超时和连接问题大幅减少。

2. 压测更准确

我们的压力测试结果更可预测、更贴近生产,有助于容量规划。

3. 运维事件减少

变更后六个月内,我们得到了这样的改变:

  • 连接相关告警减少 87%
  • 自动伸缩事件减少 92%
  • 数据库连接相关的生产事故为零

4. 环保效益

服务器数量从 24 台降到 10 台,不仅省钱,也大幅减少了能耗和碳排放。

结语:把配置当作一等优化手段

作为软件工程师,面对性能挑战时我们常常关注代码优化、算法改进和架构调整。我们的经验表明,配置(尤其是数据库连接相关配置)同样值得作为一等优化手段。

Spring Boot 3.5 的改进是基础,而我们精细的配置调优则释放了这些提升的全部潜力。最终收获的不只是成本节省,还有更可靠、高效、环保的应用。

对于面临类似挑战的团队,我的建议是:

  1. 先彻底分析当前资源使用模式
  2. 理解框架的数据库连接行为
  3. 针对性地调整配置
  4. 持续监控并迭代优化

我们实现 45% AWS 账单削减,并不是靠大刀阔斧重构或彻底换架构,而是通过理解和优化已有系统——有时,最有效的提升就藏在你应用的配置文件里。

作者:DevDecoded,原文:https://medium.com/@vermatanisha666/the-spring-boot-3-5-configuration-that-cut-our-aws-bill-by-45-e32118706ea7

上次编辑于:
贡献者: didi