请教下在高并发的情况下,Nodejs网络IO的处理效率问题
请教下在高并发的情况下,Nodejs网络IO的处理效率问题
用nodejs封装了一层应用层接口做webservice,使用了request去请求底层的http接口,这些接口因为计算的原因,普遍都在100ms以上(同一机房,网络延时可以忽略),我使用apache的ab压测,底层接口本身大约是200的qps。我封的node服务却只有70多qps,把请求接口注销掉的话有1000+qps。而且我打印了下请求接口的时间,同样的压力,比直接压测底层接口的时间多了几倍(400ms+)。 搜了下,libeio只有4个线程?是因为请求时间太长,libeio的线程被占用吗,类似于高速路上的收费站?不过我将pool.maxSocket改成200+后,qps达到了190左右。不太明白这块的原理,也没搜到理想的结果。所以想请教下各位这块的原理以及怎么处理会比较好
教会下在高并发的情况下,Node.js 网络 I/O 的处理效率问题
背景描述
我在使用 Node.js 封装一层应用层接口来做 WebService,通过 request
库来调用底层的 HTTP 接口。这些底层接口由于计算原因,普遍响应时间超过 100ms(在同一机房内,网络延迟可以忽略)。我使用 Apache 的 ab 工具进行压测,发现底层接口本身的 QPS 可以达到约 200。然而,我封装的 Node.js 服务只能达到 70 多 QPS。如果我注释掉对底层接口的请求,则 Node.js 服务可以达到 1000+ QPS。
通过打印请求接口的时间,我发现相同的压测压力下,请求底层接口的时间比直接压测底层接口的时间多了几倍(超过 400ms)。
原因分析
这个问题主要与 Node.js 的事件驱动模型和内部 I/O 处理机制有关。Node.js 使用的是单线程事件循环模型,并且依赖于 libuv 库来处理异步 I/O 操作。libuv 中的 I/O 操作实际上是通过线程池来实现的,其中默认的线程池大小为 4 个线程。
当你在高并发情况下发起大量的网络请求时,这些请求可能会阻塞 I/O 线程池中的线程,导致其他 I/O 操作无法及时完成。这类似于高速公路上的收费站,当车流量过大时,会导致交通拥堵。
解决方案
-
增加线程池大小: 你可以通过修改
request
库的配置来增加最大连接数,从而缓解线程池的阻塞问题。例如,你可以将pool.maxSockets
设置为更大的值:const request = require('request'); // 设置最大连接数 request.defaults({ pool: { maxSockets: 200 } });
-
使用 Promise 和 async/await: 为了更好地管理异步操作,你可以使用 Promise 和 async/await 来简化代码逻辑,同时保持良好的错误处理能力。
const request = require('request-promise-native'); async function fetchUrl(url) { try { const response = await request(url); return JSON.parse(response); } catch (err) { console.error(err); throw err; } } async function handleRequests() { const urls = ['http://example.com/api/data1', 'http://example.com/api/data2']; const results = await Promise.all(urls.map(fetchUrl)); console.log(results); } handleRequests();
-
优化底层接口性能: 如果可能,尝试优化底层接口的性能,减少其响应时间。可以通过优化算法、数据库查询、缓存策略等手段来提高响应速度。
-
使用集群模式: Node.js 提供了集群模块(
cluster
),可以在多个进程中并行处理请求,从而提高整体处理能力。你可以在主进程中创建多个工作进程,每个进程负责处理一部分请求。const cluster = require('cluster'); const http = require('http'); const numCPUs = require('os').cpus().length; if (cluster.isMaster) { for (let i = 0; i < numCPUs; i++) { cluster.fork(); } } else { http.createServer((req, res) => { res.writeHead(200); res.end('Hello World\n'); }).listen(8000); }
通过上述方法,可以显著提升 Node.js 在高并发情况下的网络 I/O 处理效率。
多核服务器? 考虑一下用cluster ?
对,多核,但是cluster还是Stability 1阶段。我这个线上重要服务还是不冒险使用处于试验性的,而且本身是放在nginx后的,所以会选择开多个进程。 关键是如果libeio就4个线程,如果请求的http接口时间比较长的话,感觉怎么这块都会是一个很大的瓶颈
可以先看下是不是程序或者系统设置的问题, 如果都ok的话可以参考这边文章 Scaling Node.js Applications
4个线程是指文件io,网络io libuv压根地没有使用线程,在linux是走epoll,你的问题是node http client的max socket默认只有5,默认情况下对单host只有5个活跃socket连接,而且0.10.x及一下node的http agent都不是正在意义上的keepalive,你可以查看timewait有大量来观察。
优化已经好早有同学分享过了,将max socket设置大点,使用agentkeepalive 模块替换默认的http agent
在Node.js中,网络I/O的处理效率问题通常与事件驱动、非阻塞I/O模型有关。Node.js的事件循环机制能够高效地处理并发连接,但在面对长时间运行的任务(如HTTP请求)时,如果这些任务阻塞了事件循环,那么整体性能就会受到影响。
原理解释
-
事件循环:Node.js使用事件循环来处理异步I/O操作。当一个请求到达时,Node.js会立即处理该请求,并将其异步挂起,直到I/O操作完成。这使得Node.js能够在等待I/O操作完成的同时处理其他请求。
-
libeio线程池:Node.js内部使用了一个由libeio库管理的线程池来处理磁盘和网络I/O操作。默认情况下,这个线程池有4个线程,这意味着如果有超过4个I/O操作同时发生,剩余的操作会被放入队列中等待执行。
-
阻塞性操作:如果你的请求处理逻辑中有长时间运行的任务,比如大量的计算或者长时间的网络请求,这些任务可能会阻塞事件循环,导致新请求无法及时得到处理。
解决方案
-
优化请求处理:
- 尽量减少请求处理中的阻塞性操作,比如减少不必要的计算或优化算法。
- 使用
async/await
来确保非阻塞操作不会意外地阻塞事件循环。
-
增加线程池大小:
- 可以通过修改
process.env.UV_THREADPOOL_SIZE
环境变量来增加libeio线程池的大小,例如设置为process.env.UV_THREADPOOL_SIZE = '100';
。但请注意,增加线程池大小并不总是最佳解决方案,因为每个线程都会消耗资源。
- 可以通过修改
-
使用Worker Threads:
- 对于长时间运行的任务,可以考虑使用Node.js的
worker_threads
模块,将这些任务放在单独的工作线程中处理,避免阻塞主事件循环。
- 对于长时间运行的任务,可以考虑使用Node.js的
示例代码
const http = require('http');
const { Worker } = require('worker_threads');
const server = http.createServer(async (req, res) => {
if (req.url === '/long-running') {
const worker = new Worker('./worker.js');
worker.on('message', result => {
res.end(result);
});
} else {
res.end('Hello World');
}
});
server.listen(3000, () => {
console.log('Server running at http://localhost:3000/');
});
worker.js
文件内容:
setTimeout(() => {
parentPort.postMessage('Long running task completed');
}, 1000); // 模拟一个耗时任务
通过这种方式,你可以将长时间运行的任务交给单独的工作线程处理,从而避免阻塞主事件循环。