物联网平台接口 P99 很高怎么办?Spring Boot 接口压测与压测客户端踩坑实录

发布时间:2026/10/13 10:16:00
物联网平台接口 P99 很高怎么办?Spring Boot 接口压测与压测客户端踩坑实录
真实运行截图一、压测要回答的不是快不快是三个数很多同学一提压测就说看看能不能扛住 1 万并发这个目标太虚。一次压测真正要带回来的是三个数吞吐量req/s——单位时间内处理完多少请求分位数延迟P50 / P95 / P99——99% 的请求在多少毫秒内返回错误率——压测中失败的请求占比。为什么必须看分位数而不能只看平均我项目里 9 月 22 日那次 JMeter 压测就是活例子数据来自D:\dev\iot-device-platform\loadtest\README.md2026-09-22 实测指标数值总请求数136,892吞吐量2,285 req/s平均响应36.5 msP507 msP99422 ms错误率0.00%平均 36.5ms 看着很健康但 P99 是 422ms——最慢的 1% 请求比一半以上的请求慢 60 倍。用户刷看板时偶尔卡一下的体感就藏在这 1% 里平均数根本照不出来。二、这次我先把项目跑起来重新压了一遍为了让文章有当场可复现的数据今天10-09我把项目用默认 H2 profile 重新起在 8080 端口不依赖 EMQXMQTT 连不上只是 warning不影响 HTTP 接口先注册 2 台压测设备确认接口活着见上面第三张截图然后对热点读接口GET /api/devices做并发压测。第一个版本的压测脚本我直接写在浏览器页面里跑结果翻车了——这正是本文最大的价值。三、坑一用浏览器当压测客户端64 并发只压出 247 req/s浏览器页面里用fetch发 64 个并发请求、持续 15 秒实测结果总请求数 3,766吞吐量只有 247 req/s平均响应 256msP50 210msP99 828ms同一台机器、同一个服务稍后换个客户端能压出4191 req/s相差17 倍。原因不是服务端抖了而是浏览器的连接管理Chromium 对同一个 HTTP/1.1 源最多只开6 条 TCP 连接64 个 fetch 排队抢 6 条通道测出来的响应时间大半是在排队不是服务端在处理。这就是压测的第一课数字难看时先怀疑压测客户端再怀疑服务端。项目里那次 JMeter 报告我特意写了句瓶颈在压测端并发模型而不是服务端loadtest\README.md原文这次算是用另一条路径又验证了一遍。顺带说一句浏览器压测不是不能用但它适合做单用户延迟采样不适合做并发吞吐压测。四、换 Node/undici 重压4191 req/s、P99 32msNode 的内置 fetchundici没有每源 6 连接的限制才是合格的压测客户端。核心代码真实脚本来源ai-content-studio\tools\bench-node-1009.cjs已删减注释asyncfunctiononeShot(){constt0performance.now();letstatus0;try{constrawaitfetch(URL,{cache:no-store});awaitr.arrayBuffer();statusr.status;}catch(e){status-1;}return{ms:performance.now()-t0,status};}// 预热 500 次丢弃 JIT 冷启动请求再 64 个 worker 并发跑满固定时长for(leti0;i500;i)awaitoneShot();constlat[];leterrors0;constt0performance.now();conststopt0DURATION;asyncfunctionworker(){while(performance.now()stop){constrawaitoneShot();if(r.status!200)errors;lat.push(r.ms);}}awaitPromise.all(Array.from({length:CONC},worker));lat.sort((a,b)a-b);constpctplat[Math.min(lat.length-1,Math.floor(lat.length*p))];// 输出total / rps / avg / p50 / p90 / p95 / p99 / max / errRate实测64 并发 × 15 秒指标数值总请求数62,884吞吐量4,191 req/s平均响应15.3 msP5014.5 msP9019.5 msP9522.7 msP9932 ms最大65.7 ms错误率0.00%注意两个细节预热必须做。我第一轮没预热时只有 2266 req/s、P99 57.8msJIT 冷启动把前几秒的慢请求全算进分位数了预热 500 次后才是稳定状态的真实水平。这次 P50 14.5ms 到 P99 32ms 只差 2 倍、最大 65.7ms长尾很短——因为空库查询真的没什么可慢的。五、4191 和 2285 哪个准都不准也都准有人会问9 月 JMeter 压出 2285 req/s今天 Node 压出 4191 req/s是不是服务端变快了一倍不是。这两组数字根本不可比因为压测环境三要素全变了数据集不同9 月那次打的是 MySQL profile、目标设备有 40 条遥测记录今天 H2 是空内存库连磁盘 IO 都没有客户端不同JMeter 100 线程 vs undici 64 并发客户端本身的开销和调度完全不同机器状态不同9 月那次 JMeter 和服务端同机抢核README 里写了这是 P99 422ms 的主要来源之一今天压测端是另一个进程模型。所以压测报告必须把环境写死在标题旁边客户端模型、数据集、机器部署、JVM 参数缺一个数字就没意义。这一点在面试里说出来比背出任何一个具体数字都加分。六、服务端为什么扛得住三道防打爆设计压测能扛不是 Tomcat 厉害是接口设计时就没留打爆自己的口子。三处真实代码① 参数硬上限来源src\main\java\com\iothub\data\DataPointController.javaprivatestaticfinalintDEFAULT_LIMIT100;privatestaticfinalintMAX_LIMIT1000;intsizelimitnull?DEFAULT_LIMIT:Math.min(limit,MAX_LIMIT);PageablepageablePageRequest.of(0,size,Sort.by(Sort.Direction.DESC,id));调用方传limit999999也会被钳到 1000——遥测数据是无界的不设上限等于把内存交给调用方管。② 查询走索引来源src\main\java\com\iothub\data\DataPoint.javaEntityTable(indexesIndex(nameidx_datapoint_device,columnListdeviceId))publicclassDataPoint{...}时序表是全平台数据量最大的表deviceId 上没有索引压测第一个死的就是它。③ 关掉 open-in-view来源src\main\resources\application.ymlspring:jpa:open-in-view:false不持有数据库连接到视图渲染结束连接池才能在高并发下快速周转——这也是很多压不上去案例里藏得很深的一条。七、P99 真的高了按这个顺序查回到标题的问题压测发现 P99 很高怎么办按我踩过的经验排查顺序应该是先看压测客户端连接数有没有上限、有没有和被测服务抢 CPU今天 247 req/s 的教训看 GC 日志长尾里最常见的常驻嘉宾P50 和 P99 差几十倍多半有它的份看慢 SQL索引命中了吗有没有深分页OFFSET 100000这种看连接池HikariCP 默认 10 个连接64 并发一来就在池上排队最后才是加机器。顺序反了就会出现调了一周 JVM 参数其实瓶颈在压测脚本的惨剧。写在最后我是软件工程在读正在从零搭一个物联网设备接入平台Spring Boot EMQX MySQL这个压测是平台性能这条支线的第一篇。下一篇计划写共享订阅shared subscription或者 Kafka 解耦。如果这篇对你有用评论区聊一句你压测踩过的坑比点赞更让我高兴。本文所有数据均为本机实测JMeter 数据来自项目loadtest\README.md2026-09-22Node 压测数据为 2026-10-09 当场实测代码均来自项目真实源码截图为真实运行画面。