请教下在高并发的情况下,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左右。不太明白这块的原理,也没搜到理想的结果。所以想请教下各位这块的原理以及怎么处理会比较好


6 回复

教会下在高并发的情况下,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 操作无法及时完成。这类似于高速公路上的收费站,当车流量过大时,会导致交通拥堵。

解决方案

  1. 增加线程池大小: 你可以通过修改 request 库的配置来增加最大连接数,从而缓解线程池的阻塞问题。例如,你可以将 pool.maxSockets 设置为更大的值:

    const request = require('request');
    
    // 设置最大连接数
    request.defaults({ pool: { maxSockets: 200 } });
    
  2. 使用 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();
    
  3. 优化底层接口性能: 如果可能,尝试优化底层接口的性能,减少其响应时间。可以通过优化算法、数据库查询、缓存策略等手段来提高响应速度。

  4. 使用集群模式: 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请求)时,如果这些任务阻塞了事件循环,那么整体性能就会受到影响。

原理解释

  1. 事件循环:Node.js使用事件循环来处理异步I/O操作。当一个请求到达时,Node.js会立即处理该请求,并将其异步挂起,直到I/O操作完成。这使得Node.js能够在等待I/O操作完成的同时处理其他请求。

  2. libeio线程池:Node.js内部使用了一个由libeio库管理的线程池来处理磁盘和网络I/O操作。默认情况下,这个线程池有4个线程,这意味着如果有超过4个I/O操作同时发生,剩余的操作会被放入队列中等待执行。

  3. 阻塞性操作:如果你的请求处理逻辑中有长时间运行的任务,比如大量的计算或者长时间的网络请求,这些任务可能会阻塞事件循环,导致新请求无法及时得到处理。

解决方案

  1. 优化请求处理

    • 尽量减少请求处理中的阻塞性操作,比如减少不必要的计算或优化算法。
    • 使用async/await来确保非阻塞操作不会意外地阻塞事件循环。
  2. 增加线程池大小

    • 可以通过修改process.env.UV_THREADPOOL_SIZE环境变量来增加libeio线程池的大小,例如设置为process.env.UV_THREADPOOL_SIZE = '100';。但请注意,增加线程池大小并不总是最佳解决方案,因为每个线程都会消耗资源。
  3. 使用Worker Threads

    • 对于长时间运行的任务,可以考虑使用Node.js的worker_threads模块,将这些任务放在单独的工作线程中处理,避免阻塞主事件循环。

示例代码

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); // 模拟一个耗时任务

通过这种方式,你可以将长时间运行的任务交给单独的工作线程处理,从而避免阻塞主事件循环。

回到顶部