关于 Nodejs 服务器高并发的疑问
发布于 7 天前 作者 dkvirus 1225 次浏览 来自 问答

从学习 Nodejs 以来,听各路大神说 nodejs 的一大优势就是处理高并发。我是非科班出身,对于高并发的理解就是能够同时处理大量请求,第一印象就是每个请求单独起个线程处理,请求与请求之间互不干扰,然鹅…

用 http 原生代码起一个服务器,接收两个请求:

  • /a:该接口内部写了个斐波拉契递归函数,执行时间大约 10s 左右
  • /b

实际起服务后,先请求 /a 接口,再请求 /b 接口。由于 /a 接口被斐波拉契函数阻塞了 10s 左右,惊讶的发现 /b 接口竟然一直要等到 /a 接口跑完才能响应。这说明 http 处理接口是一个处理完再处理下一个的,那么问题来了,10个用户同时请求同一个查询接口,假设每次查询时间为 1s,那岂不是运气差的人拿到响应肯定是在 10 秒之后。

所谓高并发又到底怎么理解呢?

var http = require('http')

var app = http.createServer(function (req, res) {
    if (req.url === '/a') {
        // 斐波拉契函数
        function fib(n) {
            if (n === 0) return 0;
            else if (n === 1) return 1;
            else return fib(n - 1) + fib(n - 2)
        }
        fib(44) // 执行时间要 10s 左右
        res.end('a is ' + new Date())
    } else if (req.url === '/b') {
        res.end('b is ' + new Date())
    }
})

app.listen(3600, function () {
    console.log('服务已启动')
})
33 回复

node是异步非阻塞,node将异步的io操作交给底层的libuv处理(线程池) 而这段代码是同步阻塞的,大量的计算阻塞住了线程。

@tw234tw 异步非阻塞我能理解,比如从数据库取数据这是个异步过程,扔到底层线程池处理。

对从数据库取回的数据做编码,比如列表数据结构给整成树形数据结构,类似的操作是同步代码吧,这些同步代码是在主线程上跑的吧,假设处理时间为 1s,即 1s 之后一个 /a 请求才算结束,下一个 /a 请求才开始。

现在的问题是测试结果:接口与接口之间是顺序请求的,不是并行执行的。

@dkvirus node是单线程,本来就是顺序

因为你的fib是 主线程中运行的,你把fib用c++多线程实现,用多线程运行计算,就能达到并发效果了

@jiurihuahuo 那何谈高并发?是我一直以来获取了假消息?还是?

@yakczh 现在就想知道 nodejs 自个可支持高并发,就是同时接收很多个请求,不会延迟处理。

@dkvirus 这种情况只有console.log(‘hello world’) 不会延迟出现 因为console.log(‘hello worl’)足够快 网络上的各种高并发就是这么测试出来的,你只要把 console.log(‘helloworld’) 换成实际场景中的代码,比如读一个图片文件 ,更新数据库,从google请求一个数据 你就能测试出会不会延迟了

@jiurihuahuo node单线程,所以在node主线程里一定是顺序执行的,并行执行的是那些异步i/o操作的部分,至于setTimeout 这个问题,可以看一下 https://nodejs.org/en/docs/guides/event-loop-timers-and-nexttick/ 了解一下node的事件循环机制对异步的处理。

你这个是cpu密集型 不是io密集型应用, erlang或许会更擅长处理这种

@AsJoy 那么请问,什么是 io 密集型应用,能否举个例子,如果连 web 服务都不算的话。

@tw234tw

  1. node 主线程按照顺序执行。 同意
  2. 并行执行的是那些异步i/o操作。 同意

这些跟高并发什么关系?还是说接口就应该是一个处理完成后处理下一个,如果这样的话高并发怎么理解。这个帖子核心问题就是想搞明白 nodejs 的高并发到底是怎样的。

中午和老大讨论了一下,老大说高并发是指:以前 web 服务器同一时间比如说最多只能接收 100 个请求,多的就无法接收了。nodejs 所谓的高并发是指可以同时接收 1000、10000 个请求,只不过以排队的方式在等待。

不知道这种说法可准确。

主线程执行js,是单线程的,js代码做大量计算就是cpu密集了。主线程不空闲出来也没法处理 io 的事,所以就阻塞了。

@myy 兄弟,cpu 密集型我现在懂了。

问题是高并发如何理解?

请求一个接口,从数据库搂完数据,要简单组装下,这个业务很常见吧。组装数据假设需要花费 100ms,问题是:100个人同时访问这个接口, 第 100 个人是不是要等前面 99 个请求处理完才能拿到响应,即 100(人) * 100ms = 10s 之后才能拿到响应。

如果是这样子,处理高并发如何理解? 如果不是这样子,上面那段 demo 验证的结果是一个接口处理完才开始处理下一个接口。是 demo 写的哪里不对?

js的单线程就是:同步阻塞,异步非阻塞。你这里的斐波拉契函数运行执行的是一个同步操作,只有这个函数运行结束之后才会继续执行后面的代码,因此别个的请求就只有等待了,因为是单线程,单线程,单线程(不知可否理解这里)。而异步操作不是在单线程上执行的,当遇到一个异步操作都会由底层的调用一个线程去做这件事,不影响主线程处理别的请求。 以上就是我片面的理解吧。

实际的生产中,从数据库读取数据这部 消耗的时间远比那些组装数据的时间要久, 实际上是一个接口接收到请求后,处理到异步的时候,下一个请求就开始进行处理了

你可以试一下把demo中的同步方法,改为异步耗时的操作,看看效果。

@dkvirus 没办法,nodejs的运行原理就是这样的。

高并发,我认为指的是用少量线程、异步I/O、事件驱动方式来处理IO,这种方式与传统的开大量线程的多线程模式相比较而言,可以以较少的资源消耗,应付很高的网络并发访问,解决的是网络接入和IO读写的瓶颈问题。

至于后面业务处理方面,该怎样还是怎样,主线程只能干IO的活,,CPU密集的还是要想办法分离出去,起独立线程也好,起独立进程也好,就是不能放在主线程中。

正常的情况下,启动服务还需要用到cluster 或者 pm2 这种东西

你的第一映像适用于GO语言,node相对其它语言来说已经没什么并发优势了

@cheerego cluster 跟 cpu 个数有关吧,我的云主机是 1 核的,等于没啥用。

var http = require('http')
var cluster = require("cluster");
var numCPUs = require("os").cpus().length;

console.log('numCPUs is %o', numCPUs)

if (cluster.isMaster) {
  // Fork workers.
  for (var i = 0; i < numCPUs; i++) {
    cluster.fork();
  }

  cluster.on("exit", function(worker, code, signal) {
    console.log("worker " + worker.process.pid + " died");
  });
} else {
    // Workers can share aclearny TCP connection
    // In this case its a HTTP server
    http.createServer(function(req, res) {
        if (req.url === '/a') {
            console.time('/a')        
            function fib(n) {
                if (n === 0) return 0;
                else if (n === 1) return 1;
                else return fib(n - 1) + fib(n - 2)
            }

            fib(42)
            console.timeEnd('/a')

            res.end('a is ' + new Date())
            
        } else if (req.url === '/b') {
            res.end('b is ' + new Date())
        }
  
      })
      .listen(3600, function () {
          console.log('服务已启动')
      });
}

我觉得这个老哥@tw234tw说的可能比较对。你的这段同步代码的确是会卡住主线程,我试了一种情况,或许能让你明白是怎么回事。就是在这段同步代码的后面又放了一段比较耗时的异步代码,然后我发现同时去请求/a和/b,/a的响应时间为同步代码time+异步代码time,而/b的响应时间几乎等于那段同步代码的执行时间。你可以自己试一下:单独请求包含同步代码的/a,单同时请求/b;单独请求含同步代码+异步代码的/a,同时请求/b;单独请求包含异步代码的/a,同时请求/b。不知道你有没有懂我的意思。

当然我放的那段异步代码耗时的部分是I/O,而不是计算,我相信,如果你把一段耗时的计算外层包一个异步的壳子,结果还是会卡住主线程的。我的理解是,快慢还是和nodejs处理的事情有关,一段计算耗时的代码,放在什么地方运行,都会卡住nodejs,让它无法处理其它的请求。个人理解。

cpu 1核

10个请求一起来 一个人10秒 第二个人20秒 第10个人100秒 好呢 还是每个人都100秒好

说白了就是两点:

  • 避免了CPU空转
  • 消除了线程切换上下文成本

关于CPU空转,一看代码就懂了:

function jsSleep(time) {
    return new Promise((resolve,reject)=>{
        setTimeout(() => {
            resolve(true)
        }, time);
    });
}

function otherSleep(time) {
    let oldTime = new Date().getTime();
    while(new Date().getTime()<oldTime+time) {}
    return true;
}

(async()=>{
    // Nodejs的 sleep或其他Node.js需要等待操作的实现方式
    await jsSleep(3000);
    // 传统语言的 sleep 或其他传统语言需要等待操作的实现方式
    otherSleep(3000)
})();

但成也萧何,败也萧何

话外音:斐波那契通项公式秒解

所说的“高并发”通常指的是带有后端 IO 请求的场景,比如需要到后端服务读取数据库,或者发起网络请求。 在这种场景下,js 的异步非阻塞模型效率要比同步阻塞高许多。

如果场景是 CPU 计算密集(比如楼上的计算斐波拉值),那么 js/Node.js 不具有优势。

@gyj1278 js 的异步非阻塞模型并不是提高了计算能力(或者执行性能)而是提高了执行效率-- 在 IO 密集场景,同步阻塞代码中 CPU 处于空转等待浪费了计算能力。

Node.js其实就是一个路由器

@xcstream 根据我最上面的 demo 显示应该是 一个人 10s,第二个人 20s(第一个人10s之后处理第二个人,第二个人请求 10s,加一起 20s)

到目前为止,我比较认同的高并发观点: 相对于 apache 以多线程的方式接收请求,一下子来成千上万(1000个请求)的请求,线程数(100个线程)就那么多,超过的请求(900个请求)就自动挂了; nodejs 起的web服务器,不管你来多少请求,都在事件队列里慢慢排队,高并发 指能接收请求的数目,而不是指同一时间内能处理多少个请求(猜的~)。

  • 以下面代码为例,是 express 中简单的一个处理接口
  • 浏览器访问 /home,这是个网络请求,本身应该算是异步io吧,
  • nodejs 接收之后将异步操作交给底层 libio 线程池处理,
  • 处理完成之后将回调函数 function (req, res) {} 扔到事件队列里,
  • js主线程同步代码执行完之后就会去事件队列中拿回调函数,回调函数最终是在主线程里执行的,
  • so,前面我在回调里写了个斐波拉契阻塞函数,会阻碍其它请求的返回。
app.get('/home', function (req, res) {
  // ....
})

感觉这个说法暂时可以解释我的疑惑。

讲个故事吧,不一定确切。

你开了一个 Node 餐馆:

  1. 客人来了,服务员(worker)会带领客人入座
  2. 入座过程中,因为你是高级宾馆,所以需要客人沐浴更新、走红地毯、点餐等固定流程(cpu 操作)
  3. 点餐完成后,服务员把订单通过对讲机(网络)丢给后堂厨师(数据库自己的 handler)
  4. 客人在座位上等待后堂小弟上菜。
  5. 然后服务员就可以回到前台,接待新的客人,重复上面的流程(只要你的座位足够)。

而隔壁老板开的 Java 餐馆:

  • 财大气粗,我每一个座位,都配备一个专门的服务员。
  • 在第三步,服务员亲自通知后堂,然后等着上菜给客人。
  • 不过他们的服务员待遇好,他们随时可以召唤很多小弟(fork 线程)来帮他们做事。
  • 不同的是,服务员是一人一席的,他是全程服务客人的,即使很多时候是空等着。

nodejs的高并发是hello world式的高并发,hello world的benchmark 蒙蔽了很多人的双眼

所以 一个一个来处理平均50秒 一起处理平均100秒

还是看你的业务需求,CPU密集型本身就不适合nodejs,但非常适合大量io操作的情况。 假设一个请求流程是10ms的简单运算,生成sql语句,900ms的数据库查询并返回。90ms的数据数据并返回。完整时间是1s。 如果是java处理 如果是1000个请求,不在考虑数据库压力的情况下,理论上java会并发产生1000个线程来处理,理论上也就可以在1s内处理完1000个请求并全部返回数据。 单位为了这1000个线程,服务器可能产生了百分之80的资源消耗。也就是说你这台服务器能支撑1000个并发已经很好了。

如果是nodejs处理 如果1000个请求,从第一个请求开始,10ms处理完成,然后就交给数据去处理了,这个时候就可以处理下一个10ms的请求。也就是我需要10s才可以接收完所有的请求, 但是第一个请求返回时间是1s,第二个请求返回时间是1s+10ms,以此类推,但是这1000个并发nodejs会在11s左右全部处理完。

你可能觉得java是1s,nodejs是11s,差距很大,但是需要注意的是服务器消耗。 java是1000个进程可能已经是服务器的极限看了,但是nodejs服务器确一点事情都没有。 换而言之, 我一台服务器打开10个nodejs进程,我一台服务器就可以处理的并发是10s,1W的,但是java确实1s,1000 我需要用10台服务器才完成。

实际工作中,我们会发现。在相同的业务需求下,io密集型的需求,使用nodejs同一台服务器可以处理的并发量更大,当然,牺牲了一部分的时间。但是这个完全可以用多开进程的方式来弥补来达到一个均衡。 同样的服务器,仅io密集型的需求而言,nodejs更加合适。

但是如果是cpu密集型的 你java开1000个线程,是并发计算的,一个计算1秒,1000的线程也1秒返回了,但是使用nodejs,你需要1000秒才可以返回。

所以,还是要看你的业务场景

@pzzcn 感觉说得相当到位了,手动点赞,官方文档一开始就有说道这块。

回到顶部