Node.js集群模式下请求未分发至各Worker进程的异常排查
Node.js 集群模式请求分发异常排查
测试场景与观测现象
原生cluster模块测试
测试使用的核心代码如下:
const http = require("http"); const cluster = require("cluster"); const CPUs = require("os").cpus(); const numCPUs = CPUs.length; if (cluster.isMaster) { console.log("This is the master process: ", process.pid); for (let i = 0; i < numCPUs; i++) { cluster.fork(); } cluster.on("exit", (worker) => { console.log(`worker process ${process.pid} has died`); console.log(`Only ${Object.keys(cluster.workers).length} remaining...`); }); } else { http .createServer((req, res) => { res.end(`process: ${process.pid}`); if (req.url === "/kill") { process.exit(); } console.log(`serving from ${process.pid}`); }) .listen(3000); }
基于上述代码开展压测观测到的异常:
- 压测全程所有请求日志均来自同一个固定pid的worker进程,无轮询分发效果
- 手动访问
/kill接口杀掉当前承接请求的worker后,短时间内请求依然关联旧的进程处理逻辑,直到存活worker数量持续减少才会切换到其他pid - 代码本身存在日志打印bug:worker退出回调中错误使用全局
process.pid(即master进程pid)打印退出信息,没有取实际退出的worker实例pid,容易造成日志误读
PM2集群模式对照测试
- 执行
pm2 start app.js -i 3启动3个集群实例,PM2进程列表显示3个实例全部为online状态 - 执行
loadtest -n 50000 http://localhost:3000压测期间,通过pm2 monit观测到:仅id为0的实例存在CPU、内存占用波动,其余两个online实例无任何请求命中,也没有输出应用访问日志 - 按照社区建议调整PM2配置重新测试时,出现服务完全无法启动的异常,不同配置的测试过程留存了操作录屏用于效果对比
核心问题成因
- TCP连接绑定特性:Node.js集群层的请求分发仅在新建TCP连接的节点完成——master进程收到新的连接请求时,才会按照内置调度策略将连接分配给对应worker。一旦客户端和服务端的长连接(默认开启keep-alive复用)建立完成,这个连接上的所有后续请求都会被一直绑定到最初分配的worker上,不会因累计请求数触发重新分配。loadtest工具默认复用已建立的长连接发送全部压测请求,自然所有请求都会落在同一个worker上。
- 跨平台调度策略差异:Linux环境下Node.js cluster默认采用轮询调度(Round-Robin),新建连接会均匀分发给各个worker;Windows环境下受系统网络接口实现限制,默认的连接绑定逻辑会优先把新连接分配给最近空闲的节点,在单客户端持续压测的场景下,很容易出现连接长时间绑定同一个worker的情况。
- Worker被杀后的延迟现象:当通过
/kill退出当前绑定的worker后,已有的长连接会进入半关闭状态,客户端TCP栈不会立刻感知到连接失效,会在超时重传阶段继续尝试发送请求,直到系统层判定连接断开、重新建立新连接时,master才会将新连接分配给其他存活worker,这就是杀掉worker后短时间仍命中旧pid的核心原因。
正确验证请求分发的调整方案
- 压测时关闭客户端的连接复用,使用loadtest的
-k false参数强制每个请求都新建独立TCP连接,此时master进程才会按照调度策略把每个新连接轮询分配给不同worker - 修正退出日志的打印逻辑,改为输出
worker.process.pid,避免混淆master进程和worker进程的pid - 排查本机是否存在本地端口转发、系统代理等拦截了localhost请求,导致所有请求只绑定到同一个socket节点
内容的提问来源于stack exchange,提问作者falamiw
相关产品推荐
相关产品推荐

