升级到 Spring Boot 3.5,我们的云成本减少了 45%
升级到 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 实例启动时各项指标都很健康,但随后会逐步出现:
- CPU 利用率逐步升高(最终达到 70–80%)
- JVM 堆内存持续增长
- 响应时间变慢
- 吞吐量下降
大约 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 包含:
- 更强的连接池集成
- 改进的事务管理
- 更智能的资源清理
- 更好地处理懒加载场景
升级能解决我们的问题吗?值得一试。
关键配置变更带来的转机
我们决定升级到 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() {
// 资源密集型操作
}
}
这样可以针对不同操作设置不同的事务和抓取行为,而不是一刀切。
实施与即时成效
这些变更需要仔细测试,因为数据库连接问题往往隐蔽且依赖环境。我们的做法:
- 搭建与生产一致的预发环境
- 升级到 Spring Boot 3.5 并应用新配置
- 进行大量压力测试验证变更
- 对比前后连接使用模式
初步结果令人振奋:
- 单实例平均数据库连接数从 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
保证:
- 只有在即将执行 SQL 时才获取连接
- 事务结束后立即释放连接
这样大大缩短了连接持有时间,让更小的连接池也能应对同样的负载。
查询优化引擎
Spring Boot 3.5 的新查询优化器解决了多种常见低效问题:
- N+1 查询预防:检测潜在 N+1 查询模式并自动转为高效批量查询
- 连接优化:分析实体关系,选择更优的连接策略
- 抓取大小调优:根据结果集大小自动调整 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 的改进是基础,而我们精细的配置调优则释放了这些提升的全部潜力。最终收获的不只是成本节省,还有更可靠、高效、环保的应用。
对于面临类似挑战的团队,我的建议是:
- 先彻底分析当前资源使用模式
- 理解框架的数据库连接行为
- 针对性地调整配置
- 持续监控并迭代优化
我们实现 45% AWS 账单削减,并不是靠大刀阔斧重构或彻底换架构,而是通过理解和优化已有系统——有时,最有效的提升就藏在你应用的配置文件里。
作者:DevDecoded,原文:https://medium.com/@vermatanisha666/the-spring-boot-3-5-configuration-that-cut-our-aws-bill-by-45-e32118706ea7