技术探索与实践优化:从 Spring Boot 面试题到线上性能翻倍

锁表受害者
2025-12-13 08:18
阅读 497

大家好,我是快手某核心中台服务的架构师,入行快6年了,从0到1搭过好几个系统——比如我们组现在扛着日活千万级用户的活动配置中心,还有去年双11期间临时被拉去救火的秒杀库存服务。说白了,就是那个“半夜三点还在回滚代码、第二天还要跟产品对需求”的人。

最近两年我基本扎根在我们这个小组,日常除了写业务代码、怼产品经理(不是),就是研究怎么让系统跑得更快、更稳。上周五晚上,我又熬到凌晨两点调一个 GC 问题,突然想到:很多同学在面试时被问“Spring Boot 怎么做性能优化”,答得头头是道,但真到了线上环境,连 Young GC 频率都看不懂。这不就脱节了吗?

所以今天这篇,不讲理论大饼,就说说我这两年在真实项目里踩过的坑、交过的学费,以及怎么把 Spring Boot 服务的吞吐量硬生生干翻了一倍。顺便聊聊 Rust——别急,后面会提,先让我把 Java 的锅甩完(笑)。


起因:一次差点被 P0 的线上慢请求

事情发生在去年 Q4。我们有个活动配置接口,平时 QPS 不高,也就几百,但一到大促(比如 11.11、618),流量能瞬间飙到 5k+。那天早上,监控告警炸了:P99 延迟从 50ms 突然干到 2s+,前端页面直接白屏。

我当时正在喝第三杯冰美式,看到告警第一反应是:“不会又是缓存穿透吧?”结果查日志发现,根本不是缓存的问题——数据库连接池被打满了,大量线程卡在 HikariPool-1 - Connection is not available

🤯 那一刻我真的想砸电脑。明明压测时跑得好好的,怎么上线就崩?

后来复盘发现,罪魁祸首是我们用的默认 Spring Boot 配置:

spring:
  datasource:
    hikari:
      maximum-pool-size: 10

你没看错,默认最大连接数只有10!而我们的服务部署了 20 个实例,每个实例同时处理 300+ 请求,数据库连接池直接成了瓶颈。这要是面试题,估计 HR 都要问:“你知道 HikariCP 默认连接数是多少吗?”


实战优化:不止改个配置那么简单

当然,把 maximum-pool-size 调到 50 是第一步。但光改这个远远不够。我们开始系统性地做性能优化,主要从三个层面入手:

1. JVM & GC 调优:别再用默认参数了!

Spring Boot 默认启动参数是 -Xms1g -Xmx1g,GC 用的是 ParallelGC。但在高并发场景下,Young GC 频繁(每 5 秒一次),每次停顿 30~50ms,累积起来就是灾难。

我们换成了 G1GC,并调整了关键参数:

-Xms4g -Xmx4g \
-XX:+UseG1GC \
-XX:MaxGCPauseMillis=100 \
-XX:G1HeapRegionSize=16m \
-XX:+ParallelRefProcEnabled \
-XX:MaxTenuringThreshold=1

效果立竿见影:GC 暂停时间从平均 40ms 降到 8ms 以内,P99 延迟直接砍掉 60%。

💡 小贴士:别迷信 ZGC 或 Shenandoah,除非你有 JDK17+ 的生产环境支持。我们试过,兼容性问题一堆,最后还是 G1 最稳。

2. 异步化 & 缓存:能不查 DB 就别查

我们重构了核心链路:

  • 所有非关键写操作(比如埋点日志、审计记录)扔进 @Async 线程池
  • 高频读接口加 本地缓存(Caffeine) + Redis 二级缓存
  • 用了 @Cacheable(sync = true) 防止缓存击穿

特别吐槽一下:以前为了“代码简洁”,我们把所有逻辑塞在一个 Service 方法里,结果一个慢 SQL 拖垮整个链路。现在?拆!能异步的全异步,能缓存的全缓存。

3. 线程池隔离:别让一个接口拖垮整个服务

Spring Boot 默认用 Tomcat 的线程池处理请求,所有接口共用。一旦某个接口慢,其他接口也跟着遭殃。

我们引入了 Hystrix(虽然已停更,但内部 fork 了) + 自定义线程池,按业务域隔离:

@HystrixCommand(
    threadPoolKey = "activityConfigPool",
    threadPoolProperties = {
        @HystrixProperty(name = "coreSize", value = "20"),
        @HystrixProperty(name = "maximumSize", value = "50"),
        @HystrixProperty(name = "allowMaximumSizeToDivergeFromCoreSize", value = "true")
    }
)
public ActivityConfig getConfig(String id) { ... }

这样即使活动配置接口挂了,用户信息接口照样稳如老狗。


效果对比:数据不会骗人

优化前后,我们在相同压测环境(5k QPS,持续 10 分钟)下做了对比:

指标 优化前 优化后 提升
平均响应时间 180ms 65ms 64%↓
P99 延迟 2100ms 120ms 94%↓
GC 暂停频率 5次/分钟 0.8次/分钟 84%↓
错误率 8.2% 0.03% 近乎清零

最爽的是,服务器资源反而省了——原来要 20 台 8C16G 的机器,现在 12 台就够了。运维大哥请我喝了杯奶茶,说“你们组终于不乱扩容了”。


面试题 vs 真实战:差距在哪?

很多面试题问:“Spring Boot 如何优化性能?”
标准答案往往是:“加缓存、调线程池、用连接池、开异步。”

但现实是:

  • 缓存加了,但没设 TTL,结果内存 OOM
  • 线程池用了,但没监控队列长度,任务堆积到爆
  • 异步开了,但没处理异常,错误静默丢失
  • 连接池调大了,但数据库 CPU 打满,DBA 来骂人

真正的优化,是带着监控、带着兜底、带着灰度策略去做的。

我们现在的做法:

  1. 所有优化必须可回滚:通过 Apollo 动态配置开关
  2. 关键指标必须可观测:接入 Prometheus + Grafana,监控线程池、缓存命中率、GC 日志
  3. 压测常态化:每周五下午固定做“破坏性测试”,模拟慢 SQL、网络抖动、Redis 宕机

顺带聊聊 Rust:为什么我开始学它?

说到这,可能有人问:“你不是 Java 架构师吗,怎么最近总在搞 Rust?”

其实是因为——Java 在某些场景真的有点重

我们有个边缘计算节点,需要极低延迟(<1ms)和超低内存占用(<50MB)。用 Spring Boot 启动都要 500MB 内存,GC 停顿哪怕 1ms 都不可接受。

于是领导一句:“你不是喜欢折腾吗?试试 Rust。”
我:???

但真上手后,发现 Rust 的零成本抽象、无 GC、内存安全,简直是这类场景的天选之子。现在我们已经用 Rust 写了一个轻量级网关,QPS 10w+,内存占用 30MB,延迟稳定在 0.3ms。

不过别误会,Rust 不会取代 Java。Spring Boot 在业务系统、快速迭代、生态丰富度上依然无敌。只是作为架构师,你得知道什么时候该用什么工具。


最后一点心得

性能优化不是“一次性工程”,而是持续迭代的过程。我见过太多团队,上线前不做压测,出问题就加机器,最后成本爆炸。

记住三句话:

  • 不要相信默认配置(尤其是 Spring Boot)
  • 不要只看平均值(P99、P999 才是用户体验)
  • 不要闭门造车(多和 SRE、DBA、测试聊)

我现在每天下班前都会扫一眼 Grafana 面板,看到曲线平稳就安心。如果哪天又红了……嗯,那今晚咖啡续命,继续干。

毕竟,程序员的浪漫,就是让每一毫秒都物有所值


P.S. 如果你在准备面试,别光背八股文。试着在自己项目里做一次真实的性能优化,哪怕只是调个 JVM 参数、加个缓存。那种“从线上救火到稳如泰山”的成就感,比拿 offer 还爽。

我是那个还在快手写代码的架构师,下次见。

评论 0

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