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

从学习 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('服务已启动')
})
49 回复

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. 然后服务员就可以回到前台,接待新的客人,重复上面的流程(只要你的座位足够)。

至于你的 斐波拉契函数,你非要让服务员来做,那第二步自然要很久很久,然后他才能去接待其他客人。 在 Node 里面,这一步不应该亲力亲为,应该额外叫一个小弟来做。(譬如点餐你可以自助手机点啊,摔~)

而隔壁老板开的 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 感觉说得相当到位了,手动点赞,官方文档一开始就有说道这块。

@zengming00 go是每个请求启用一个线程? 你说的是java吧?

@pzzcn 赞同。

补充点个人看法。 大家在谈及多线程时往往忽视了其本质:减小因为 IO 延迟等待导致的 CPU 空闲时间,提高资源整体利用效能,而非提高 CPU 计算能力。 假定服务器为 10 核心(不考虑HT),且忽略 CPU 与高速缓存、内存交换时间开销,需求为计算斐波拉契数列。 若单进程,此时 1000 线程执行速度并不会比 10 线程快,反而因为线程切换可能会比 10 线程并发执行性能更低。 这个 CPU 密集计算场景,(java)单进程 10 线程, (nodejs)10 进程,执行性能取决于语言本身设计及实现。就 java 和 nodejs 相比应该差不多。 而实际工作环境不大可能是如此极端依赖 CPU 计算能力,更多的开销在 IO 上(包括外部存储本身性能高低,外部存储延迟,总线延迟等)。而 node.js 的设计模型在并没有提高计算能力的前提下可以 hold 住更多的请求而不至于耗尽系统资源,这就是我们常说的高并发能力。

若以服务行业来说:

  • java 更像一对一的 VIP 服务模式:(线程)专属服侍,待遇高。但服务提供方不可能配备太多的服务员(供线程消耗),于是当生意火爆时需要预订情况(不接受更多客户了);
  • node.js 更像师傅领进门,吃饱看大厨心情的小餐馆队列模式: 点菜(客户端发起请求)后就等着,上菜速度取决于后堂(整体资源)能力,但只要店面位置够多、食客有足够耐心,那么(队列)可以容纳更多的客户端(大家排队傻等中……) 而排队需要的资源很小,所以就会出现川渝店面门外长串板凳特色 ೭(˵¯̴͒ꇴ¯̴͒˵)౨

@dkvirus 你把node.js的 事件队列(Event Queue)和事件循环(Event Loop)看看就明白了。因为js主线程是单线程,那为了处理耗时的操作(ajax,文件读写等),才有了异步和回调函数callbck概念。将这些耗时的操作放到另外的线程中去处理,等处理完了,就会把耗时操作指定的回调函数callback放到事件队列中去。js主线程继续往下执行,不用等待耗时操作完成后再执行下面的代码。js主线程里面的代码都执行完了后,会轮询这个事件队列,发现了耗时操作所执行的callback,就会把callback放到主线程中去执行。你那个例子中,当’/a’请求过来的时候,由于此时主线程是空的,就会立刻执行斐波函数,而这段代码是同步代码啊,当然会一直在主线程中执行,’/b’的请求过来的时候,主线程正忙着处理斐波拉契函数的计算呢,会把’/b’请求的callback会被放到事件队列中去了,当斐波拉契函数处理完了之后,主线程有空了,就过来处理’/b’请求的回调函数。 看看下图吧,每次来了请求,都会触发net.server 的 request 事件 eventloop.png

很赞同@pzzcn @atian25 的解释,你的斐波拉契函数是CPU密集操作,nodejs没有多线程,肯定会阻塞的,而网络应用,是不会出现这种情况的,个人认为,基本的网络无非就是网络传输以及数据库的处理,这些都可以称做io,而所有io都是通过一个线程去异步实现,这样效率会很高,从而达到接收更多的请求,单线程也决定了程序是按照顺序去运行,所以在nodejs里面没有锁,然而,碰到如同楼主困惑的密集CPU操作,因为顺序执行,就卡住了。剩余的一些程序固定的消耗是非常小的。在做网络应用有这些功能的就不适合使用nodejs去完成,而因该用支持线程或者类似GO这种去做,而且,像这种功能,个人觉得就应该实现为异步的接口,用户也需要异步使用。

@Gitforxuyang 他想表达的应该是 Go 每个请求启用一个 Goroutine 呢。

@BigKongfuPanda 你说的都能理解,但是在很多日常业务中即便把很多耗时操作交到异步里面去处理了,但我们需要的结果恰恰也都在回调函数 Callback 里面,所以还是要等待异步耗时操作的完成才能拿到结果,所以Node只适合做一些业务的中转,并不适合处理业务本身?

@beyond5959 协程跟线程虽一字之差,但完全不一样。 可以说协程就是go的最大亮点之一。

@pzzcn 说得透彻,赞!

@beyond5959 为啥不考虑这些计算工作,用c++ addons 写呢。。。如果要同步计算。

@cnlile 所以我说了 Node 应该拿来当业务中转用,并不适合处理业务本身。

@LuckyHH 赞同这个说话。

@beyond5959 我觉得还是可以处理的,只要看应用场景啊。。。业务都是微服务化的今天,你怎么处理是综合考虑的结果,不是说一定不适合处理业务,只是拆分后,那个比较好。主要还是人员构成有关和某些特点场合,显然这些计算是属于通用场合的。。处理业务本身也很正常,我几百万用户的服务系统,照样用nodejs处理业务。。。更大场景我就不敢说了。。我也做过nodejs和硬件设备结合的处理业务场景,不要一棍子就把nodejs打死了,说不能处理业务本身。另外,c++addons 本身就是nodejs的组成部分。

@cnlile 微服务化其实就是把业务中转了,对 Node 来说就是把业务放在网络 I/O 里去了。

@cnlile 如果api网关用 java 实现,对于 json 格式数据处理汇聚,面对多端场景的字段过滤, 估计会很酸爽。 不知道为啥 java 一直不原生支持 json ?

@waitingsong Java 有jar包支持json的,在spring 下,简单的注释加入@RestController@RequestMapping 之类的东西,也可以处理api的,也都是比较简单的。一般来说,简单的api 我倾向于用OpenResty 服务处理,其他用nodejs,实在没办法的用Java。。。。特殊的用python… 多掌握一些技能没坏处的

回到顶部