2026年7月11日 · 9 分钟阅读
Java 项目性能调优实战:从分布式压测到 Tomcat、缓存和 JVM
基于项目性能优化课程笔记,梳理分布式压测、Spring Boot 容器调优、数据库优化、OpenResty、多级缓存和 JVM 调优思路。
性能优化进入实战阶段后,问题会变得更具体:单机 JMeter 压不上去怎么办?Tomcat 默认线程数够不够?数据库要从哪里调?缓存加在哪里最划算?JVM 参数是不是越大越好?
这些问题不能孤立看。性能调优的顺序应该是:先确认压测能力可靠,再定位系统瓶颈,最后选择成本最低、收益最大的优化点。
分布式压测:先保证压力能打上去
当并发压力很大时,单台 JMeter 压力机可能会先成为瓶颈。表现通常是被测服务还没有明显压力,但压力机 CPU、内存或网络已经吃满。
这时就需要分布式压测。
JMeter 分布式压测由两类节点组成:
- Master:控制节点,负责下发压测任务和收集结果。
- Slave:施压节点,真正向被测服务发起请求。
一个容易忽略的点是,总请求样本数会按 Slave 数量放大。比如 Master 中配置线程数 10、循环 100 次,单个 Slave 会产生 10 x 100 = 1000 次请求。如果有 3 台 Slave,总样本数就是 10 x 100 x 3 = 3000。
分布式压测注意事项
搭建 JMeter 分布式环境时,重点关注这些问题:
- Master 和 Slave 尽量在同一内网。
- 多网卡机器要指定正确 IP。
- 各机器时间要同步。
- Master 能访问 Slave 的 RMI 端口。
- Slave 的
server.rmi.ssl.disable配置要和 Master 保持一致。 - 防火墙不能粗暴长期关闭,生产环境要明确开放固定端口。
分布式压测的目的不是让架构更复杂,而是避免压力机瓶颈污染压测结论。
Tomcat 调优:线程、连接和队列
Spring Boot 默认使用嵌入式 Tomcat。默认配置适合通用场景,但在高并发压测下,Tomcat 线程、连接和等待队列可能成为瓶颈。
最常见的三个参数是:
| 参数 | 含义 | 常见风险 |
|---|---|---|
server.tomcat.threads.max | 最大工作线程数 | 太小会处理不过来,太大会增加上下文切换和内存消耗 |
server.tomcat.accept-count | 等待队列长度 | 队列满后新连接可能被拒绝 |
server.tomcat.max-connections | 最大连接数 | 连接过多会消耗内存和网络资源 |
示例配置:
server:
tomcat:
accept-count: 1000
max-connections: 20000
threads:
max: 800
min-spare: 100
这些值不能照搬。线程数越大,吞吐量不一定越高,因为线程上下文切换和栈内存都会带来成本。JVM 默认每个线程栈大小可能是 1MB,线程开太多会明显增加内存压力。
什么时候该调 Tomcat 线程数
可以用一个简单公式估算服务端并发处理线程数:
服务端并发线程数 ≈ TPS / (1000ms / 平均 RT)
如果接口 RT 很低,系统已经能产生足够 TPS,就不一定需要加线程。如果 RT 较高、请求在容器层排队明显,适当增加线程数和等待队列才有价值。
调优时要记住一条底线:任何配置修改后,都要确认它真的生效。JVM、Tomcat、OpenResty、数据库参数都一样,没生效的优化等于没做。
网络 IO 模型:从 NIO 到 NIO2
Tomcat 的性能不仅受线程数影响,也受 IO 模型影响。Java NIO 使用缓冲区和通道处理 IO,相比传统阻塞 IO 更适合高并发网络场景。JDK 1.7 后引入的 NIO2 进一步增强了异步 IO 能力。
在 Spring Boot 中,可以通过自定义 Connector 使用 NIO2 协议处理 HTTP 请求。课程案例里的结论是:切换 NIO2 后,响应时间毛刺明显减少,超时异常也更少。
但这类优化要结合实际场景。IO 模型调优适合已经确认瓶颈在网络 IO 或容器处理层的情况,不适合在没有证据时提前引入复杂配置。
是否要把 Tomcat 换成 Undertow
Spring Boot 除了 Tomcat,也支持 Undertow 和 Jetty。Undertow 是轻量级、高性能的 Web 服务器,支持阻塞和非阻塞 API,也支持 WebSocket、HTTP/2 等能力。
切换 Undertow 的方式很简单:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-undertow</artifactId>
</dependency>
常见配置包括 IO 线程、工作线程、buffer 大小和是否使用直接内存。
server:
undertow:
threads:
io: 800
worker: 8000
buffer-size: 1024
direct-buffers: true
不过“换容器”不是银弹。课程里也提到,在低延时场景下,Undertow 的吞吐量不一定总是优于调优后的 Tomcat。工程上更稳妥的做法是:保留同一压测脚本、同一机器环境,分别压测对比后再决定。
数据库调优:不要只问有没有索引
数据库往往是后端性能优化里最重要的一环。很多接口 RT 下不去,最终都会落到数据库查询、表结构、事务、锁或连接池上。
数据库调优可以先问三个问题:
- 为什么要调优数据库?
- 什么影响数据库性能?
- 数据库调优到底调什么?
影响数据库性能的因素包括:
- SQL 写法是否低效。
- 表结构是否合理。
- 索引是否匹配查询条件。
- 主键、外键、多表关系是否设计得当。
- 连接数、超时时间、缓存等数据库配置是否合理。
- 是否存在大事务、慢查询、锁等待。
- 数据库部署架构和硬件资源是否匹配业务量。
索引是数据库优化的常见入口,但不是全部。一个查询能不能走索引、索引区分度怎么样、是否回表、是否排序、是否产生临时表,都可能影响最终性能。
OpenResty:把能力前移到网关层
OpenResty 是基于 Nginx 和 Lua 的高性能 Web 平台。它的价值在于可以把一部分动态逻辑放到 Nginx 层执行,比如鉴权、限流、缓存读取、简单聚合等。
在性能优化里,OpenResty 常用于:
- 反向代理后端服务。
- 在网关层读取 Redis 缓存。
- 用 Lua 实现简单业务逻辑。
- 减少请求进入 Java 应用的次数。
- 降低数据库访问压力。
课程案例中,使用 OpenResty 反向代理后,TPS 在原有基础上有明显提升。这背后的关键不是“加了一个代理就一定更快”,而是 OpenResty 更靠近入口,能用更低成本处理一部分高频请求。
多级缓存:越靠近用户,价值越大
缓存调优的核心是减少重复计算和重复 IO。对于首页广告、轮播图、分类信息这类读多写少的数据,很适合做缓存。
一个常见的多级缓存链路是:
OpenResty 本地缓存 -> Redis -> MySQL
请求处理流程可以这样设计:
- 先查 OpenResty 进程内缓存。
- 本地缓存没有,再查 Redis。
- Redis 没有,再回源 MySQL。
- 回源成功后,把数据写回 Redis 和本地缓存。
缓存预热则是在系统启动或活动开始前,把 MySQL 中的热点数据提前加载到 Redis,避免高峰期大量请求同时打到数据库。
缓存调优的边界
缓存不是只要加上就好。实际项目里还要考虑:
- 缓存过期时间。
- 数据一致性要求。
- 缓存击穿、穿透、雪崩。
- 热点 key。
- 本地缓存占用内存。
- 多节点本地缓存更新问题。
课程里提到一个很重要的判断:用户越近的地方,缓存价值越大。OpenResty 本地缓存比 Redis 更靠近请求入口,命中后可以减少网络调用;Redis 又比 MySQL 更适合承接高频读取。
JVM 调优:不要把它当第一选择
JVM 调优的目标,是让应用用更少的硬件资源承载更高吞吐,同时减少 GC 频率和 Full GC 次数,降低停顿时间。
但 JVM 调优不是性能优化的第一步。优先级通常应该是:
- 架构调优。
- 代码调优。
- 数据库和缓存调优。
- 容器和网络调优。
- JVM 调优。
大多数 Java 应用不需要一开始就精细调 JVM。只有当监控显示内存、GC 或停顿时间成为瓶颈时,才值得深入。
什么时候需要 JVM 调优
遇到这些情况,可以考虑 JVM 调优:
- 系统吞吐量下降。
- 响应时间明显变差,尤其是 P99 抖动。
- 老年代持续上涨,接近最大堆。
- Full GC 频繁。
- GC 停顿时间过长。
- 出现
OutOfMemoryError。 - 本地缓存占用大量堆内存。
JVM 调优主要调两件事:
- 内存分配是否合理。
- 垃圾回收是否高效。
堆内存不是越大越好。内存太小会频繁 GC,内存太大则可能带来更长停顿和资源浪费。合理值要结合对象生命周期、流量规模、缓存策略和 GC 日志判断。
一套实战调优顺序
把课程中的内容串起来,可以形成一套相对稳的实战路径:
- 用单机 JMeter 做基础压测,确认接口、断言、监听器都正确。
- 当压力机成为瓶颈时,切到分布式压测。
- 建立 Grafana 监控,同时观察压测指标和服务器资源。
- 用梯度压测找到瓶颈出现的并发区间。
- 如果瓶颈在 Web 容器,调 Tomcat 线程、连接、队列或 IO 模型。
- 如果瓶颈在数据库,分析 SQL、索引、表结构和连接配置。
- 如果是高频读接口,优先引入 Redis 或 OpenResty 多级缓存。
- 如果 GC 和内存成为瓶颈,再进入 JVM 调优。
- 每次只改一个主要变量,压测前后对比。
这套顺序的关键是“证据驱动”。每次优化都要能回答:为什么改、改了什么、是否生效、指标变化如何。
小结
项目性能优化不是参数大全,也不是工具清单,而是一种工程判断能力。
分布式压测解决的是“压力能不能真实打上去”;Tomcat、NIO2、Undertow 解决的是“Web 容器能不能扛住”;数据库和缓存解决的是“高频数据访问能不能降本”;JVM 调优解决的是“内存和 GC 是否拖后腿”。
真正成熟的调优方式,是从压测数据出发,沿着调用链逐层排查。先找到最贵的瓶颈,再做最小必要改动,最后用同一套压测方式验证结果。