QPS压测是验证系统吞吐能力的核心手段,绝大多数压测不过的场景,根因并不在应用代码本身,而是集中在连接数耗尽、线程池阻塞、数据库慢查询和GC抖动四个环节。调优的顺序应当是先排查基础设施与中间件,再定位数据库与JVM层问题,最后才考虑业务代码优化。
压测结果的准确性高度依赖基线数据的完整性,没有基线对照的QPS数据不具备参考价值。建立基线需要明确三件事:当前环境的硬件配置(CPU核数、内存大小、磁盘类型)、依赖中间件的版本信息(MySQL版本、Redis版本、消息队列类型)、以及压测工具的线程模型(并发数、Ramp-Up时间、持续时长)。
基线数据至少要跑三轮,取中位数而不是平均值。平均值会被首轮冷启动和尾轮资源回收阶段的数据拉偏,中位数能更稳定地反映系统在稳态下的真实处理能力。记录基线时同步采集CPU使用率、内存占用、GC日志、线程池活跃度等指标,这些数据在后续定位瓶颈时缺一不可。
根据压测实战中的高频问题,我整理了四个最常导致QPS压测不达标的瓶颈点,每个都有明确的判定阈值与排查命令。
连接数耗尽通常发生在数据库连接池或HTTP客户端连接池层面,表现为压测中后期错误率突然上升,错误类型多为Connection timed out或Too many connections。show processlist能看到大量Sleep状态的连接堆积,此时应用线程池反而处于空闲状态。
# 查看MySQL当前连接数与最大连接数 SHOW VARIABLES LIKE 'max_connections'; SHOW STATUS LIKE 'Threads_connected'; # 查看应用侧连接池状态(以HikariCP为例) hikaricp_connections_active hikaricp_connections_pending
连接池大小的计算公式是连接数 = ((核心线程数 × 2) + 有效存储设备核数),这个公式来自PostgreSQL官方文档的推荐模型,按此配置后连接池通常不会成为瓶颈。如果压测时Threads_connected持续逼近max_connections,优先检查连接是否泄漏——在代码中搜索未关闭的Connection、Statement和ResultSet对象。
线程池饱和是QPS压测曲线出现断崖式下跌的直接原因,判定特征是执行器组件的RejectedExecutionException异常,或者是日志中频繁出现Task rejected from pool的告警。线程池核心线程数设置过小、队列容量设置过大,都会让请求在队列中等待过久,压测期间表现为平均响应时间线性上升,但QPS始终无法突破某个固定值。
// 线程池配置建议:核心线程数=CPU核数×2,队列容量=1000
ThreadPoolExecutor executor = new ThreadPoolExecutor(
Runtime.getRuntime().availableProcessors() * 2,
Runtime.getRuntime().availableProcessors() * 4,
60L, TimeUnit.SECONDS,
new ArrayBlockingQueue<>(1000),
new ThreadPoolExecutor.CallerRunsPolicy()
);CallerRunsPolicy在并发峰值时可以保证任务不丢失,但压测时如果持续触发该策略,说明系统已经达到真实处理上限,应当以这个上限作为容量规划依据,而不是盲目调大线程数。线程数超过CPU核数×4之后,上下文切换开销会吞噬掉新增线程带来的收益。
数据层的瓶颈往往以慢查询和锁等待的形式叠加出现,QPS曲线表现为锯齿状波动——每一次跌落的间隔恰好对应一条慢SQL的执行周期。开启慢查询日志后,抓取执行时间超过100毫秒的SQL,用EXPLAIN分析执行计划。
| 排查项 | 判定阈值 | 核心指令 |
|---|---|---|
| 慢SQL | 单条执行时间 > 100ms | SHOW VARIABLES LIKE 'slow_query_log' |
| 锁等待 | 等待次数 > 0 或等待时间 > 5s | SHOW ENGINE INNODB STATUS |
| 索引失效 | EXPLAIN中type=ALL或rows超预期 | EXPLAIN SELECT... |
排查顺序上先看锁等待再看慢查询:锁等待会把正常SQL拖慢到与慢SQL相同的量级,如果直接从慢查询入手会浪费大量时间。InnoDB锁等待的典型场景是批量更新操作与高频查询冲突,此时压测并发数越大,吞吐量下降得越明显。
GC抖动是Java服务压测中特有的瓶颈形态,特征是QPS均值正常但P99延迟远超P95,响应时间分布出现典型的长尾。压测期间通过jstat -gcutil观察FGC次数和耗时,如果Full GC平均耗时超过500毫秒且频率高于每5分钟一次,说明堆内存配置或对象分配速率存在问题。
多数GC抖动由大对象直接进入老年代或者内存分配速率过高触发。通过jmap -dump:live,format=b,file=heap.hprof获取堆转储,再用MAT分析大对象引用链。压测前的GC参数调优重点在于设置合理的-Xms与-Xmx一致以避免扩容抖动,以及调整新生代与老年代的比例为1:2。
压测瓶颈排查必须按照固定的顺序执行,跳步排查会导致定位结论错误。推荐顺序是:网络层(带宽、TCP队列)→ 接入层(Nginx配置、KeepAlive超时)→ 应用层(线程池、连接池)→ 数据层(慢查询、锁等待)→ JVM层(GC、堆内存)。按此顺序排查一轮通常能在30分钟内锁定根因。
网络层的排查经常被忽略,但带宽打满会导致所有下游指标失真。用sar -n DEV 1观察网卡吞吐量,当rxkB/s接近网卡理论带宽的80%时,压测结果反映的是网络瓶颈而非应用性能。接入层的Nginx配置中,worker_processes建议设置为CPU核数,worker_connections建议设置为10240并开启keepalive。
QPS压测的数据解读必须统一口径,否则不同轮次的对比失去意义。统一口径包含三个约定:只统计成功响应(HTTP状态码为2xx且业务返回码为0)、超时时间统一设置为3秒、压测时长不少于5分钟。按照这一口径统计的结果,才能作为容量规划与性能对比的可靠依据。
实践数据表明,按照上述排查顺序处理后的系统,QPS通常能有20%到80%的提升,具体提升幅度取决于瓶颈所在层级。网络层与数据层的瓶颈修复后提升最明显,JVM层调优更多是消除长尾延迟而不是拉高QPS峰值。
评论
加载中...