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 下不去,最终都会落到数据库查询、表结构、事务、锁或连接池上。

数据库调优可以先问三个问题:

  1. 为什么要调优数据库?
  2. 什么影响数据库性能?
  3. 数据库调优到底调什么?

影响数据库性能的因素包括:

  • SQL 写法是否低效。
  • 表结构是否合理。
  • 索引是否匹配查询条件。
  • 主键、外键、多表关系是否设计得当。
  • 连接数、超时时间、缓存等数据库配置是否合理。
  • 是否存在大事务、慢查询、锁等待。
  • 数据库部署架构和硬件资源是否匹配业务量。

索引是数据库优化的常见入口,但不是全部。一个查询能不能走索引、索引区分度怎么样、是否回表、是否排序、是否产生临时表,都可能影响最终性能。

OpenResty:把能力前移到网关层

OpenResty 是基于 Nginx 和 Lua 的高性能 Web 平台。它的价值在于可以把一部分动态逻辑放到 Nginx 层执行,比如鉴权、限流、缓存读取、简单聚合等。

在性能优化里,OpenResty 常用于:

  • 反向代理后端服务。
  • 在网关层读取 Redis 缓存。
  • 用 Lua 实现简单业务逻辑。
  • 减少请求进入 Java 应用的次数。
  • 降低数据库访问压力。

课程案例中,使用 OpenResty 反向代理后,TPS 在原有基础上有明显提升。这背后的关键不是“加了一个代理就一定更快”,而是 OpenResty 更靠近入口,能用更低成本处理一部分高频请求。

多级缓存:越靠近用户,价值越大

缓存调优的核心是减少重复计算和重复 IO。对于首页广告、轮播图、分类信息这类读多写少的数据,很适合做缓存。

一个常见的多级缓存链路是:

OpenResty 本地缓存 -> Redis -> MySQL

请求处理流程可以这样设计:

  1. 先查 OpenResty 进程内缓存。
  2. 本地缓存没有,再查 Redis。
  3. Redis 没有,再回源 MySQL。
  4. 回源成功后,把数据写回 Redis 和本地缓存。

缓存预热则是在系统启动或活动开始前,把 MySQL 中的热点数据提前加载到 Redis,避免高峰期大量请求同时打到数据库。

缓存调优的边界

缓存不是只要加上就好。实际项目里还要考虑:

  • 缓存过期时间。
  • 数据一致性要求。
  • 缓存击穿、穿透、雪崩。
  • 热点 key。
  • 本地缓存占用内存。
  • 多节点本地缓存更新问题。

课程里提到一个很重要的判断:用户越近的地方,缓存价值越大。OpenResty 本地缓存比 Redis 更靠近请求入口,命中后可以减少网络调用;Redis 又比 MySQL 更适合承接高频读取。

JVM 调优:不要把它当第一选择

JVM 调优的目标,是让应用用更少的硬件资源承载更高吞吐,同时减少 GC 频率和 Full GC 次数,降低停顿时间。

但 JVM 调优不是性能优化的第一步。优先级通常应该是:

  1. 架构调优。
  2. 代码调优。
  3. 数据库和缓存调优。
  4. 容器和网络调优。
  5. JVM 调优。

大多数 Java 应用不需要一开始就精细调 JVM。只有当监控显示内存、GC 或停顿时间成为瓶颈时,才值得深入。

什么时候需要 JVM 调优

遇到这些情况,可以考虑 JVM 调优:

  • 系统吞吐量下降。
  • 响应时间明显变差,尤其是 P99 抖动。
  • 老年代持续上涨,接近最大堆。
  • Full GC 频繁。
  • GC 停顿时间过长。
  • 出现 OutOfMemoryError
  • 本地缓存占用大量堆内存。

JVM 调优主要调两件事:

  • 内存分配是否合理。
  • 垃圾回收是否高效。

堆内存不是越大越好。内存太小会频繁 GC,内存太大则可能带来更长停顿和资源浪费。合理值要结合对象生命周期、流量规模、缓存策略和 GC 日志判断。

一套实战调优顺序

把课程中的内容串起来,可以形成一套相对稳的实战路径:

  1. 用单机 JMeter 做基础压测,确认接口、断言、监听器都正确。
  2. 当压力机成为瓶颈时,切到分布式压测。
  3. 建立 Grafana 监控,同时观察压测指标和服务器资源。
  4. 用梯度压测找到瓶颈出现的并发区间。
  5. 如果瓶颈在 Web 容器,调 Tomcat 线程、连接、队列或 IO 模型。
  6. 如果瓶颈在数据库,分析 SQL、索引、表结构和连接配置。
  7. 如果是高频读接口,优先引入 Redis 或 OpenResty 多级缓存。
  8. 如果 GC 和内存成为瓶颈,再进入 JVM 调优。
  9. 每次只改一个主要变量,压测前后对比。

这套顺序的关键是“证据驱动”。每次优化都要能回答:为什么改、改了什么、是否生效、指标变化如何。

小结

项目性能优化不是参数大全,也不是工具清单,而是一种工程判断能力。

分布式压测解决的是“压力能不能真实打上去”;Tomcat、NIO2、Undertow 解决的是“Web 容器能不能扛住”;数据库和缓存解决的是“高频数据访问能不能降本”;JVM 调优解决的是“内存和 GC 是否拖后腿”。

真正成熟的调优方式,是从压测数据出发,沿着调用链逐层排查。先找到最贵的瓶颈,再做最小必要改动,最后用同一套压测方式验证结果。