分布式压测和项目调优
本章介绍如何使用Docker 容器来搭建压力测试监控平台。
1. 分布式压测
在使用 JMeter 进行大并发压力测试时,单台机器往往受限于内存、CPU 和网络 I/O,导致服务器压力未达到预期,但压测机的压力已经过大并发生崩溃。
为了解决这一问题,JMeter 提供了分布式压测功能,从而显著提升其负载能力。
单机网络带宽有限,高延时场景下,单机可模拟最大线程数有限。
下图是分布式压测架构:

需要注意的是,JMeter 分布式压测中,Controller 节点负责协调,Slave 节点负责执行测试。当控制器节点配置 10 个线程,每个线程循环 100 次时,单个控制器会产生 1000 个请求样本。在 Master 启动压测后,每台 Slave 都会执行相同的测试配置,向被测服务发送 1000 次请求。因此,如果使用 3 台 Slave,总共会产生 3000 次请求样本。
搭建 JMeter 分布式压测环境注意事项:
- 三台 JMeter Slave 都是在 Linux( Ubuntu 22.04.1 LTS ) 服务器上搭建。
- 需要确保 Server 和 Salve 之间的时间是同步的。
- 需在内网配置 JMeter 主从通信端口【1个固定,1个随机】,简单的配置方式就是关闭防火墙,但存 在安全隐患。
1.1 在 Window 系统中搭建部署 JMeter Master
- 与Window中安装JMeter一样简单,略
1.2 Linux 部署 JMeter Salve
下载解压安装包命令
mkdir -p /work/JMeter-slave
wget https://archive.apache.org/dist/jmeter/binaries/apache-jmeter-5.6.3.tgz
tar -zxvf apache-jmeter-5.6.3.tgz
mv apache-jmeter-5.6.3 ./apache-jmeter-5.6.3-salve
修改rmi
配置文件和主机的 hostname
cd apache-jmeter-5.6.3-salve/bin
# 修改ip
vim jmeter-server
# RMI_HOST_DEF=-Djava.rmi.server.hostname=本机ip
# 改端口
vim jmeter.properties
# RMI port to be used by the server (must start rmiregistry with same port)
server_port=1099
# To change the default port (1099) used to access the server:
server.rmi.port=1098
配置关闭server.rmi.ssl
# Set this if you don't want to use SSL for RMI
server.rmi.ssl.disable=true
启动jmeter-server服务
nohup ./jmeter-server > ./jmeter.log 2>&1 &
注意:剩下两台 Slave 也是同样的操作。
1.3 分布式环境配置
确保 JMeter Controller 和 Salve 安装正确
Salve启动,并监听1099端口
在JMeter Controller 机器安装目录bin下,找到jmeter.properties文件,修改远程主机选项,添加3个 Salve服务器的地址
remote_hosts=192.168.1.18:1099,192.168.1.19:1099,192.168.1.20:1099
启动jmeter,如果是多网卡模式需要指定 IP 地址启动
jmeter -Djava.rmi.server.hostname=192.168.1.218
验证分布式环境是否搭建成功
image-20250117174511298
3. 容器服务优化
3.1 Tomcat容器调优
Spring Boot 应用性能优化中,嵌入式 Tomcat 的调优是关键环节。
当出现响应时间延迟时,通常是由于 Tomcat 内部的 IO 模型(多线程与网络编程)成为瓶颈,并导致系统异常率升高。为了更好地理解系统并发处理能力,并指导线程池配置,我们可以基于响应时间(RT)和吞吐量(TPS)进行服务端并发线程数的估算,公式为:TPS / (1000ms / RT均值)。
Spring Boot 应用依赖于嵌入式 Tomcat 服务器,默认配置可能存在性能瓶颈。通过对 Tomcat 配置进行适当优化,可以显著提升应用性能。
修改配置如下所示:可以使用外挂配置,也可以修改配置文件application.yml
注意,做了任何修改一定要确认配置生效,否则干的再久也是白搭!
server:
port: 48080 # 配置服务器监听的端口号为 48080
max-http-header-size: 10MB # 配置 HTTP 请求头的最大为 10MB
tomcat:
accept-count: 1000 # 配置当所有工作线程都被使用时,Tomcat 可以接受的最大排队请求数
max-connections: 20000 # 配置服务器允许的最大连接数
threads:
max: 500 # 配置最大工作线程数
min-spare: 100 # 配置最小空闲线程数
management:
endpoints:
web:
exposure:
include: '*' # 配置暴露所有管理端点
base-path: /actuator # 配置管理端点的基本路径为 /actuator
endpoint:
shutdown:
enabled: true # 允许通过 /actuator/shutdown 来关闭应用程序
accept-count:最大等待连接数
- 当调用HTTP请求数达到Tomcat的最大线程数时,还有新的请求进来,这时Tomcat会将该剩余请 求放到等待队列
- acceptCount就是指队列能够接受的最大的等待连接数
- 默认值是100,如果等待队列超了,新的请求会被拒绝(connection refused)
maxConnections:最大连接数
- 默认值是8192。这意味着Tomcat能够同时处理的最大TCP连接数量是8192个。这个设置包括了正在活跃的请求以及等待处理的请求。
maxThreads:最大线程数
- 默认值是200。这意味着Tomcat能够创建的最大工作线程数是200个,这些线程用来处理HTTP请求。当所有工作线程都在处理请求时,新的请求将会放入到等待队列中,如果等待队列也满了,则超出部分的请求会被拒绝。
在优化 Tomcat 性能时,你是否认为最大线程数越大越好?实际上,线程数只是影响吞吐量(TPS)的因素之一,并非关键因素。
增加线程是有成本的,过多的线程会导致线程上下文切换开销和内存资源消耗。
JVM 默认的线程堆栈大小为 1MB(通过
-Xss
参数进行配置)。关于线程上下文切换的详细机制,我们将在并发编程章节中深入探讨。
那么最大线程数的值应该设置多少合适呢?
- 这个需要基于业务系统的监控结果来定。RT均值很低,可以不用设置,RT均值很高,可以考虑加 线程数
- 当然,如果接口响应时间低于100毫秒,足以产生足够的TPS,系统瓶颈不在于此,则不建议设置最大线程数
个人经验:
1C2G,线程数200
4C8G,线程数800
8C16G,线程数1600
3.1.1 确认调优配置生效
访问:http://218.249.73.244:48080/actuator/configprops

3.1.2 调优前后的性能对比
调优前:压力机Active,RT、TPS、系统进程运行状态【应用活动线程数】


在调整完Spring boot 配置后,使用 Jmeter 压测时可能出现:java.net.BindException:Address already in use. 错误。
出现错误原因:Windows提供给TCPIP连接的端口为1024-5000,并且要四分钟左右循环回收,这就导致我们短时间内频繁调用大量请求时,端口将被占满。
解决这个错误方案 :
- 打开注册表:在cmd(win+R)中输入regedit,打开注册表;
- 设置系统参数:最大端口连接数;
- 找到系统参数设置项:\HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\Tcpip\Parameters
- 右击parameters,添加一个新的DWORD,参数名为MaxUserPort;
- 双击MaxMaxUserPort,输入数值为65534,基数选择十进制;
- 重启电脑!重启电脑!重启电脑!
按照以上配置后压测项目还会出现 java.net.BindException:Address already in use. 错误。这时就需要调整 Ramp-up period 参数。

可以根据实际情况调整:
基础方案(温和启动):
- Ramp-up period: 60-120 秒
- 这意味着每秒会增加 3-6 个线程
- 给系统足够时间预热和适应负载增长
激进方案(快速启动):
- Ramp-up period: 20-30 秒
- 每秒增加 13-20 个线程
- 适用于系统性能较好或需要快速达到目标并发的场景
保守方案(缓慢启动):
- Ramp-up period: 180-240 秒
- 每秒增加 1-2 个线程
- 适用于需要特别关注系统稳定性的场景
我的建议是:
- 先用保守方案(180秒)测试一轮
- 观察系统响应情况(TPS、响应时间、错误率)
- 如果系统表现稳定,可以逐步降低到基础方案(60秒)
- 重点关注以下指标:
- 响应时间是否稳定
- 错误率是否在可接受范围内
- 系统资源使用情况
如果你的系统能够很好地处理基础方案,而且需要测试更极限的情况,才考虑使用激进方案。
记住:较短的 Ramp-up period 会产生更大的突发压力,可能导致更多的连接错误。建议从较大的值开始,然后根据实际情况逐步调整。
**推荐:**把 jmmeter 程序迁移到一台安装程序较少比较干净的系统内进行压力测试。这样也可以解决这个问题!
调优后:压力机Active,RT、TPS、系统进程运行状态【应用活动线程数】


优化结论:提升Tomcat最大线程数,在高负载场景下,TPS提升接近1倍多,同时RT大幅降低;
3.2 网络IO模型调优
3.2.1 IO 模型介绍
在Java 程序中文件读写性能是影响应用程序性能的关键因素之一,NIO与原来的IO有同样的作用和目的,但 是使用的方式完全不同,NIO支持面向缓冲区的、基于通道的IO操作,NIO以更加高效的方式进行文件 的读写操作。
在Java 中NIO是从Java 1.4版本开始引入的一套新的IO API用于替代标准的Java IO API。
JDK1.7之后,Java对NIO再次进行了极大的改进,增强了对文件处理和文件系统特性的支持。我们称之 为AIO,也可以叫NIO2。
优化 Spring Boot 2.7.x 内置 Tomcat 9.x 配置,使用NIO2的Http协议实现,对请求连接器进行改写!
调整后的配置:
@Configuration
public class TomcatConfig {
/**
* 启用 NIO2(Http11Nio2Protocol)协议
*
* @return
*/
@Bean
public WebServerFactoryCustomizer<TomcatServletWebServerFactory> tomcatCustomizer() {
return factory -> {
factory.setProtocol("org.apache.coyote.http11.Http11Nio2Protocol");
factory.addConnectorCustomizers(this::http11Nio2Connector);
};
}
private void http11Nio2Connector(Connector connector) {
Http11Nio2Protocol nio2Protocol = (Http11Nio2Protocol)
connector.getProtocolHandler();
//等待队列最多允许1000个线程在队列中等待
nio2Protocol.setAcceptCount(1000);
// 设置最大线程数
nio2Protocol.setMaxThreads(500);
// 设置最大连接数
nio2Protocol.setMaxConnections(10000);
//定制化keepalivetimeout,设置30秒内没有请求则服务端自动断开keepalive链接
nio2Protocol.setKeepAliveTimeout(30000);
//当客户端发送超过10000个请求则自动断开keepalive链接
nio2Protocol.setMaxKeepAliveRequests(10000);
// 请求方式
connector.setScheme("http");
connector.setPort(48080); //自定义的端口,与源端口9001
connector.setRedirectPort(8443);
}
}
3.2.2 调优前后的性能对比
没有使用NIO2的Http协议实现之前的 RT、TPS




使用NIO2的Http协议实现之后的 RT、TPS




从以上图片信息的对比来看,使用NIO和NIO2后,主要的提升体现在以下几个方面:
吞吐量 (ops/s):
- 在NIO2中,吞吐量为461.5 ops/s,相比NIO的480 ops/s略有下降。但是,NIO2的性能更稳定,波动较小。
响应时间 (ms):
- 在NIO2中,大部分线程请求的平均响应时间都有所下降,特别是在400线程请求方面(从519.23 ms降到517.33 ms)。其他较高线程的请求也显示出轻微的改善。
- 最大响应时间在NIO2中也有所下降。例如,400线程请求的最大响应时间在NIO2中为776 ms,而在NIO中为792.34 ms。
TPS、TPM 和 TPH:
- NIO2中的TPS(每秒事务数)在400线程请求及以上有所增加,表明NIO2的处理能力更强。
- 同样,TPM(每分钟事务数)和TPH(每小时事务数)在NIO2中也有所提高,显示出系统在更高速率下处理更多事务。
错误率:
- 在NIO和NIO2中,错误率均为0%,因此在这一点上没有差异。
其他指标(如90%、95%、99%的响应时间)在NIO2中也表现出一些小幅的提升,整体响应时间有所降低。
总的来说,NIO2在性能上表现出了更加稳定的响应时间和吞吐量,尤其是在高线程情况下,系统处理更多请求时的效率得到了明显提升。
3.3 内置Tomcat升级成Undertow
Tomcat是Apache开源的一个轻量级的Servlet容器。Tomcat具有Web服务器特有的功能,包括 Tomcat管理和控制平台、安全局管理和Tomcat阀等。Tomcat本身包含了HTTP服务器,因此也可以视作单独的Web服务器。
但是,Tomcat和ApacheHTTP服务器不是一个东西,ApacheHTTP服务器是用C语言实现的HTTP Web服务器。Tomcat是完全免费的,深受开发者的喜爱。
Undertow是Red Hat公司的开源产品, 它完全采用Java语言开发,是一款灵活的高性能Web服务器,支持阻塞IO和非阻塞IO
。由于Undertow采用Java语言开发,可以直接嵌入到Java项目中使用。同时, Undertow完全支持Servlet和Web Socket,在高并发情况下表现非常出色。
3.3.1 具体配置过程
排除tomcat 在spring-boot-starter-web中
<!-- 排除 Spring Boot 默认的 Tomcat --> <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>
导入 starter-undertow
<!-- 添加 Undertow 作为 Web 服务器 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-undertow</artifactId> </dependency>
配置 application.yml
server: port: 48080 max-http-header-size: 10MB # 最大 HTTP 头大小,默认 8KB undertow: threads: io: 500 # IO线程数量,这里设置为800。这些线程专门用于处理网络I/O操作,如读取和写入数据,提高了并发处理能力 worker: 5000 # 默认值是IO线程数*8 buffer-size: 1024 # 设置Undertow使用的缓冲区大小,这里是1024字节(1KB)。缓冲区大小影响数据传输的效率,较大的缓冲区可以减少I/O操作的次数,但也会增加内存使用 direct-buffers: true # 是否使用直接缓冲区,这里设置为true。直接缓冲区可以减少内存拷贝操作,提高性能,但需要更多的内存管理,因为它们不受垃圾回收的影响
3.3.2 升级优化前后的性能对比
优化前:RT、TPS ,注意下图是没有配置NIO2优化的Tomcat。



调优后:RT、TPS



根据报告总结:
- 升级服务器后,RT更为平稳,TPS的增长趋势也趋于稳定。
- 在低负载情况下,接口通吐量不及Tomcat。
- 系统稳定性提升,如果只使用JSON接口且接口请求稳定性高,可考虑使用Undertow。
此外,还有一些优化点:
- ARP-IO模型
- Jetty容器
- KeepAliveTimeout(默认)
- MaxKeepAliveRequests
这些优化方式可以根据实际情况进行选择,例如我们通过分析应用性能瓶颈,找到关键的优化点,以达到最佳的性能表现。虽然无法保证完全优化,但可以显著提升性能。
4. 数据库调优
4.1 为什么要数据库调优?
因为程序的响应时间,是受数据库读写操作影响的。
提升网站整体通吐量,优化用户体验数据库是关键性能之一。
总的来说,数据库调优是为了提高查询速度、节省资源、提升用户体验、降低成本、支持业务增长和解决性能瓶颈,确保数据库系统高效运行。
4.2 什么影响数据库性能?
硬件配置
- CPU:处理器的速度和核心数影响并发处理能力。
- 内存(RAM):更多的内存可以减少磁盘I/O,提升缓存命中率。
- 存储设备:硬盘或SSD的性能(如IOPS、吞吐量)直接影响数据读取和写入速度。
- 网络带宽:在分布式系统或客户端-服务器架构中,网络速度影响数据传输。
MySQL服务
- 数据库表结构【对性能影响最大】
- 低下效率的SQL语句
- 超大的表
- 大事务
- 数据库配置
- 数据库整体架构
- .......
4.3 数据库调优到底调什么?
- 硬件优化
- 增加CPU
- 增加内存
- .....
- 数据库设计优化
- 表结构优化
- 主键索引优化
- 外键
- 多表关系
- .....
- 查询优化
- 避免全表扫描,通过合适的索引或查询重写减少全表扫描。
- 简化复杂查询,拆分复杂查询,减少子查询使用,避免笛卡尔积。
- 使用适当的连接方法,如INNER JOIN替代WHERE条件的连接。
- ....
- 数据库配置调整
- 最大连接数
- 连接超时
- 线程缓存
- 查询缓存
- 排序缓存
- 连接查询缓存
- .......
为什么使用索引就能加快查询速度呢?
- 索引就是事先排好顺序,从而在查询的时候可以使用二分法等高效率的算法查找
- 除了索引查找,就是一般顺序查找,这两者的差异是数量级的差异。
- 二分法查找的时间复杂度是O(logn),而一般顺序查找的时间复杂度是O(n)。
- 索引的数据结构是B+树,是一种比二分法查找时间复杂度更好的数据结构。
列举出来算法的时间复杂度有部分,可能感知不到差距。
假设表中有100w条数据,如果是根据id查找,一般顺序查找平均需要找50万条数据,对比50万次才能找到。如果采用索引二分法查找,最多不超过20次对比即可找到。
可以的看出,两种方式的执行效率相差2.5万倍.
5. OpenResty 调优
OpenResty 简单介绍
官网:https://openresty.org/en/installation.html
OpenResty 是一个基于 Nginx 的 Web 平台,用于构建高效的 Web 应用程序,API 和网关。其内部集成了大量精良的 Lua 库、第三方 模块以及大多数的依赖项。
OpenResty 的目标是让你的Web服务直接跑在 Nginx 服务内部,充分利用 Nginx 的非阻 塞 I/O 模型,不仅仅对 HTTP 客户端请求,甚至于对远程后端诸MySQL、PostgreSQL、Memcached 以及 Redis 等都进行一致的高性能响应。
关于Nginx 是什么?
Nginx 是一个开源的高性能HTTP服务器和反向代理服务器,同时也能够作为IMAP/POP3代理服务器。其特点是占有内存少,并发能力强,事实上nginx的并发能力在同类型的网页服务器中 表现较好。
5.1 使用 Docker 安装 OpenResty
拉取最新镜像:
docker pull openresty/openresty:latest
要特定的版本,比如固定版本1.19.9.1,可以这样拉取:
docker pull openresty/openresty:1.19.9.1-4-alpine
alpine 标签表示基于Alpine Linux的镜像,通常体积较小。
创建openresty需要挂载的目录文件:
mkdir -p /sg-work/openresty/nginx/
mkdir -p /sg-work/openresty/conf/
mkdir -p /sg-work/openresty/luascript/
启动容器并挂载目录:
docker run -d --name my-openresty -p 8456:80 openresty/openresty:latest
docker cp my-openresty:/usr/local/openresty/nginx/conf/nginx.conf /sg-work/openresty/nginx/conf/
docker cp my-openresty:/etc/nginx/conf.d /sg-work/openresty/nginx/conf/
docker cp my-openresty:/usr/local/openresty/nginx/logs /sg-work/openresty/nginx/
docker cp my-openresty:/usr/local/openresty/nginx/html /sg-work/openresty/nginx/
docker run -d --name my-openresty \
-p 8456:80 \
-v /sg-work/openresty/nginx/conf:/usr/local/openresty/nginx/conf/nginx.conf \
-v /sg-work/openresty/nginx/conf/conf.d:/etc/nginx/conf.d \
-v /sg-work/openresty/nginx/logs:/usr/local/openresty/nginx/logs \
-v /sg-work/openresty/nginx/html:/usr/local/openresty/nginx/html \
openresty/openresty:latest
docker run -d --name my-openresty \
-p 80:80 \
-p 443:443 \
-p 8786:8786 \
-p 8987:8987 \
-v /sg-work/openresty/nginx/conf:/usr/local/openresty/nginx/conf \
-v /sg-work/openresty/nginx/html:/usr/local/openresty/nginx/html \
-v /sg-work/openresty/nginx/logs:/usr/local/openresty/nginx/logs \
-v /sg-work/openresty/conf:/etc/nginx/conf.d \
-v /sg-work/openresty/luascript:/usr/local/openresty/luascript \
openresty/openresty:latest