远程独狼的 Spring Boot 性能调优血泪史:从阿里云报警到深夜 Vim 敲出 3 倍吞吐量

代码收容所
2025-12-14 23:29
阅读 1081

Hi,我是阿哲。一个坐标杭州、在家办公的独立开发者,Vim 是我的 IDE(别笑,真的不用 IDEA),咖啡是我的燃料,而线上报警声——是我最怕听到的起床铃。

过去三年,我接了不少外包项目,客户多是阿里、网易生态里的中小团队。他们喜欢用 Spring Boot 搭快速原型,我也习惯了这套“开箱即用”的甜头。但去年双11前一周,我差点被一个看似普通的订单查询接口搞到连夜买站票回老家。

事情是这样的:客户是个做跨境电商的小公司,系统用 Spring Boot + MyBatis + MySQL 跑了快两年,平时流量不大,大家相安无事。但一到大促,那个 /api/order/detail 接口就开始疯狂超时。运维兄弟半夜三点在钉钉群里@我:“阿哲,CPU 又打满了!再这样老板要砍人了!”

我当时正窝在西湖边某小区的出租屋里,一边啃泡面一边看 Grafana 面板——QPS 不到 200,CPU 却飙到 95%。说实话,那一刻我真的想砸键盘。不是因为难,而是因为太典型了——典型的“开发爽了,运维哭了”的 Spring Boot 糟糕实践。

今天这篇,不讲理论,不灌鸡汤,就聊聊我这半年来在远程自由职业状态下,如何用“土法炼钢”把一个烂到发臭的 Spring Boot 应用,硬生生优化到扛住 600+ QPS 的实战总结。顺便也吐吐槽那些年我们踩过的坑。


别再信“Spring Boot 开箱即用”了!

先说句扎心的:Spring Boot 的默认配置,只适合写 Demo

很多团队(包括我早年)以为 spring-boot-starter-web 一加,application.yml 一配,跑起来就万事大吉。结果呢?线程池用的是 Tomcat 默认的 200 个最大线程;数据库连接池 HikariCP 默认 maxPoolSize 是 10;JVM 参数还是 -Xmx512m …… 这玩意儿不崩才怪。

我接手的那个项目,就是这种“样板房式开发”的受害者。代码倒是挺干净,Controller → Service → Mapper 三层分明,注解满天飞,可一压测就露馅。

第一步:定位瓶颈,别瞎猜

很多人一遇到性能问题就猛加机器,或者直接上 Redis 缓存。但在我这儿,先 profiling,再动手

我用 async-profiler 抓了个火焰图(没错,远程开发我也装了 Linux 虚拟机跑分析工具),结果吓一跳:

[Hot Methods]
org.springframework.cglib.proxy.MethodInterceptor.intercept (28.7%)
com.mysql.cj.jdbc.ClientPreparedStatement.executeInternal (21.3%)
java.util.HashMap.get (12.1%)
...

好家伙,近三成 CPU 耗在 CGLIB 动态代理上!一查代码,发现 Service 层所有方法都加了 @Transactional,连只读查询也不例外。更离谱的是,有些方法内部还调用了其他 Service 方法——导致事务嵌套,代理链层层叠加。

教训一:别滥用 @Transactional
只读操作加 @Transactional(readOnly = true),写操作才用默认事务。而且,尽量避免在同一个类里方法互相调用(Spring AOP 失效不说,还会触发额外代理开销)。


数据库:慢查询是万恶之源

火焰图里第二高的是 MySQL 驱动执行 SQL。打开慢日志一看,果然有条语句没走索引:

SELECT * FROM order_info 
WHERE user_id = ? AND status IN ('paid', 'shipped') 
ORDER BY create_time DESC 
LIMIT 10;

表有 500 万行,user_id 有索引,但 status 没联合。结果每次都要回表查几百行再排序。

解决方案:加复合索引 (user_id, status, create_time)。上线后,这条 SQL 从 300ms 降到 5ms。

但你以为这就完了?Too young。

测试同学第二天跑回归测试,发现另一个接口变慢了。一查,原来新索引虽然加速了查询,但插入订单时要维护三个字段的索引树,写入性能下降了 15%。

权衡来了:我们最终决定对高频查询接口走新索引,低频写入接口用旧方案,并通过 @QueryHints 强制指定索引:

@Query(value = "SELECT * FROM order_info WHERE user_id = ?1 ...", 
       hints = @QueryHint(name = "org.hibernate.readOnly", value = "true"))
List<Order> findOrdersByUserWithIndex(@Param("userId") Long userId);

线程池与异步:别让 Web 容器背锅

回到最初的问题:为什么 QPS 200 就 CPU 打满?

Tomcat 默认最大线程数是 200。当每个请求平均处理 50ms(大部分卡在 DB 和网络 IO),理论上最大 QPS 也就 200 / 0.05 = 4000?不对啊,怎么 200 就崩了?

继续深挖,发现业务逻辑里有同步调用第三方物流接口,超时设成 10 秒!一旦物流服务抖一抖,Tomcat 线程全被 block 住,新请求进不来,CPU 其实不高,但线程耗尽,响应时间爆炸。

解决方案:拆!把非核心链路异步化。

我用 Spring 的 @Async + 自定义线程池:

@Configuration
@EnableAsync
public class AsyncConfig {

    @Bean("logisticsTaskExecutor")
    public Executor logisticsTaskExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(10);
        executor.setMaxPoolSize(30);
        executor.setQueueCapacity(100);
        executor.setThreadNamePrefix("logistics-");
        executor.setRejectedExecutionHandler(new ThreadPoolTaskExecutor.CallerRunsPolicy());
        executor.initialize();
        return executor;
    }
}

然后在 Service 里:

@Async("logisticsTaskExecutor")
public void asyncUpdateLogistics(Order order) {
    // 调用第三方,失败也不影响主流程
    logisticsClient.update(order.getTrackingNo());
}

注意@Async 方法必须是 public,且不能和调用方在同一个类(否则代理失效)——又是 Spring AOP 的坑!


JVM 与 GC:别再用默认参数了

远程开发有个好处:服务器资源你说了算(反正客户付钱)。但很多人连 JVM 参数都不调,就敢上生产。

我用 jstat -gcutil 监控 GC,发现老年代增长极快,每 5 分钟 Full GC 一次。原因?对象生命周期太长。

比如,Service 层返回的 DTO 包含大量嵌套对象,而前端其实只需要几个字段。结果每次请求都创建上千字节的临时对象,堆内存吃紧。

优化手段

  1. 精简返回体:用 MapStruct 或手写转换器,只传必要字段。
  2. 调整 JVM 参数(基于 G1 GC):
-Xms2g -Xmx2g
-XX:+UseG1GC
-XX:MaxGCPauseMillis=200
-XX:G1HeapRegionSize=16m
-XX:+ParallelRefProcEnabled

配合 -XX:+PrintGCApplicationStoppedTime 日志,确认 STW 时间稳定在 50ms 以内。


最终成果:从 200 QPS 到 650+

我把所有优化点整合后,用 JMeter 压测对比:

优化阶段 平均响应时间 (ms) 99分位 (ms) 最大 QPS CPU 使用率
初始状态 480 1200 ~180 95%
仅 DB 优化 120 300 ~350 70%
加异步 + 线程池 85 200 ~500 60%
全量优化 45 90 650+ 45%

最爽的是,双11当天,客户 PM 在群里发红包:“感谢阿哲!系统稳如老狗!”


写在最后:自由职业者的“技术洁癖”

作为远程独立开发者,我没有团队 Code Review,没有 DevOps 支持,甚至没有同事可以甩锅(笑)。所以,我对代码质量和性能有种近乎偏执的要求——因为线上崩了,只有我能救

Spring Boot 确实香,但它不是银弹。它的“约定优于配置”哲学,很容易让人忽略底层细节。而性能问题,往往就藏在那些“默认就好”的角落里。

如果你也在杭州,也在接外包,或者正准备跳槽去阿里网易系公司,我想说:别只盯着八股文,多压测、多看火焰图、多问“为什么这么慢”。这些实战经验,比背一百道面试题都管用。

对了,上周五晚上,我又接到一个新需求:“能不能把接口再快点?老板说竞品能做到 20ms。”
我笑了笑,打开 Vim,敲下第一行代码:@RestController……

毕竟,独狼的浪漫,就是在寂静深夜,用一行行代码,把不可能变成可能。

(完)

注:本文所有配置和代码均来自真实项目脱敏,如有雷同,恭喜你——你也踩过同样的坑。

评论 0

最热最新
暂无评论
匿名用户Lv.1
0
影响力
0
文章
0
粉丝