Node.js服务与Apache ab工具并发测试异常:请求计数不符
这是个典型的并发场景下的疑难问题,我来帮你一步步拆解排查方向和验证方法:
先明确核心现象
从你的测试数据能看出规律:
- 低并发(
-c=1/2)时,服务日志数和ab指定的请求数完全一致 - 并发越高,服务日志数超出请求数的差值越大
- 差值不稳定但持续存在
这说明问题和并发压力下的请求发送/处理逻辑直接相关,我们可以从「ab工具是否多发送了请求」和「服务端是否重复处理了请求」两个方向入手验证。
第一步:验证ab实际发送的请求数量
首先要搞清楚:是ab偷偷发了更多请求,还是服务端自己重复处理了相同请求?
用抓包工具确认真实请求数
在CentOS 6.9上可以用tcpdump抓本地回环接口的请求,统计实际发送的GET /test请求数量:
# 1. 准备日志文件 > server.log > request_count.txt # 2. 后台启动服务,把日志输出到文件 node ./server.js > server.log 2>&1 & SERVER_PID=$! # 3. 启动tcpdump抓包并统计GET请求数 tcpdump -i lo port 3000 -l 2>/dev/null | grep "GET /test" | wc -l > request_count.txt & TCPDUMP_PID=$! # 4. 执行ab压测 /usr/bin/ab -n 1000 -c 1000 localhost:3000/test # 5. 停止相关进程并查看结果 kill $TCPDUMP_PID kill $SERVER_PID echo "ab指定请求数: 1000" echo "抓包实际请求数: $(cat request_count.txt)" echo "服务日志处理次数: $(wc -l server.log)"
对比这三个数值就能定位问题:
- 如果抓包数=服务日志数>ab指定数:说明ab在高并发下发送了超出指定数量的请求
- 如果抓包数=ab指定数<服务日志数:说明服务端重复处理了同一批请求
第二步:针对不同结论的排查方向
情况1:ab发送了额外请求
CentOS 6.9默认的ab版本是2.2,在高并发场景下可能存在两个导致额外请求的原因:
系统资源限制导致连接失败,ab隐式重试:CentOS 6.9默认文件描述符限制是1024,当
-c=1000时,需要同时建立1000个TCP连接,很可能触发资源限制导致部分连接失败,部分旧版本ab会在这种情况下自动重试请求。- 验证方法:查看ab输出中的
Failed requests数值,如果大于0,说明有请求失败,可能伴随重试。 - 解决方法:临时提升系统文件描述符限制(
ulimit -n 2048),或者更换更可靠的压测工具(如wrk、k6)。
- 验证方法:查看ab输出中的
ab本身的并发逻辑bug:旧版本ab在高并发下可能存在计数不准确或重复发送请求的问题,你可以换用其他工具测试对比:
# 用wrk测试示例 wrk -t1 -c1000 -d1s http://localhost:3000/test
情况2:服务端重复处理请求
如果抓包数和ab指定数一致,但服务日志数更多,说明是Node.js或Hapi.js的请求处理逻辑存在问题:
Node.js http模块的旧版本bug:你使用的Node.js v10.14.1是比较老的版本,存在一些高并发下的请求解析bug,比如TCP连接异常时重复触发
request事件。- 解决方法:升级Node.js到LTS版本(如v16.x或v18.x),再测试是否复现问题。
Hapi.js的路由处理异常:虽然你的代码只注册了一次
/test路由,但Hapi 17.x的部分版本在高并发下可能存在路由匹配的重复触发问题。- 解决方法:升级Hapi.js到最新的17.x补丁版本,或直接升级到v18+版本。
额外提示:修复count变量的并发安全问题
虽然这不是导致日志数异常的直接原因,但你的全局count变量使用count++不是原子操作,高并发下会出现计数不准确的问题。可以改用Node.js的Atomic类来保证并发安全:
'use strict'; var Hapi = require("hapi"); const count = new require('atomic-sleep').AtomicInteger(0); // 或者用Atomics模块 const server = Hapi.server({ port: 3000, host: 'localhost' }); server.route({ method: 'GET', path: '/test', handler: (request, h) => { const currentCount = count.incrementAndGet(); // 原子操作 console.log(currentCount); return currentCount; } }); // 剩余代码不变...
内容的提问来源于stack exchange,提问作者zsimo

