Nodejs单线程是怎么应对高并发的场景的?

发布于 1周前 作者 nodeper 来自 nodejs/Nestjs

Nodejs单线程是怎么应对高并发的场景的?

node 单线程处理事件请求,一个请求卡住了,后续其他请求都会卡住,用 node 做业务处理,并发高的情况下,岂不是后面的请求可能会一直排队? node 不是单线程分发事件,多线程处理事件嘛?

44 回复

你不要卡住不就行了。。需要改改非要让代码卡住的问题。


node 不是单线程 JavaScript 才是单线程 一个请求卡住 暂时没有返回的话 并不影响后续请求的执行

卡住就下一个

楼主所说的卡住 应该是指 需要 cpu 计算的不可避免的那部分吧 类似 redis 中出现的慢查询。

不太了解 node 但是单线程利用多核可以走 multi process, 也可以搞 one event base per thread

一个请求处理 1s ,第十个请求岂不是要等个 10s 左右的时间,如果请求没有 IO 要处理的话。

node 实际上底层有个 libuv 线程池,看看 node 的时间循环就知道了,只要不是 cpu 密集型,把 cpu 卡住了,就没事。

主线程处理连接,返回数据,数据的处理交给工作线程去干,一个连接对应一个线程可能比较耗费资源,所以要用协程之内的东西

你可以看看我这个代码,对每个请求都会同步地调用 Youtube-dl ,但是不会卡住别的请求,因为有多线程呀

https://github.com/develon2015/Youtube-dl-REST/blob/node/index.js#L182

就担心 CPU 计算密集型业务。

CPU 计算密集型 放合适的地方啊, 为啥要指望一个语言打天下呢

node 适合 IO 密集,不适合 CPU 密集

复杂业务很难定义属于哪一类吧,总不能为了一个小的需求用其他语言做单独实现。

一般都是根据 cpu 线程数 单机多实例部署吧

node 全异步怎么会卡住?业务很少有 CPU 密集部分,CPU 密集部分也不会放在 web 请求中,在 web 请求中搞 CPU 密集放哪个语言多进程框架都不行毕竟线程也是很有限的。

[单线程分发事件,多线程处理事件] 多线程指的不是真正的系统层面上的线程,甚至你也可以把句话认为是错误的
node 那些常用的框架正常开发出来应该都是单核应用
正常的异步内部逻辑不会卡后续逻辑,如果你现在确实碰到了卡后续请求的问题,可能是使用了同步版本的 IO 调用(导致无法利用 JS 的异步能力),也可能是本身业务 cpu 运算量就大
简单点的利用多核的方法可以直接用 PM2

担心主线程卡住,worker_threads 了解一下: http://nodejs.cn/api/worker_threads.html

发给下游服务去处理。

你想一下,领导给你一个人的任务太多,卡住怎么办?

很简单,把工作给下面的人分下去,人多力量大。

nodejs 也是同理,当然有些数据库之类的写入,还是尽量用消息队列处理。

需要人灵活地使用工具

拿电钻去钉钉子是不对的 😂

如果你这 1s 内 cpu (单核)占满,那确实后面的请求就得排队了。但一个请求要 cpu 实实在在算 1s 的业务,估计也不会用 node 写了。不过 node 也有了 worker_threads ,甚至可以调用 c++模块,一定程度上有利于计算密集型任务。

你考虑的太多了,你应该思考的是你们公司有这么大的业务量吗

所以 node js 不适合用来做 CPU-intensive 的操作啊;本来就是拿来做 IO intensive 的。

CPU 计算密集型的任何语言都会卡住,需要 worker 来利用多个 CPU

一个请求卡住后,她会求助的:Step bro, I’m stuck

你会卡住?那你就让这会卡住的部分放在这个线程外面,用其他线程跑就行了。

event loop 怎么会卡住呢,甚至可以说处理高并发是优势,瓶颈在别的地方,几年前讨论的明明白白,现在这是退化了?

业务事件是程序员写的,这是有可能会卡住的。如果某个请求因为某个条件触发了耗时的 cpu 运算,其他请求只能等待,多线程处理的情况下,其他线程是不需要等待的。

我靠。。。你。。你。你绝对需要看我的视频啦少年,首先 nodejs 是 nio 模型,还有基于事件轮训的设计,它不用等待 IO ,所有的 IO 也是一个事件。一个请求过来,接收完请求后,就生成一个事件,这个事件处理完后,就扔出一个 response 事件,事件池就会一直被轮训,一直被处理。nodejs 主要适合 IO 密集型,就是不要那么有很多计算性的代码,也就是你的业务代码不要太复杂就行了。

NIO 有一个绝对性优势,你可以看看我的视频,有讲解 nodejs 如何无视 IO 时间,做到吞吐量保持一致。https://www.bilibili.com/video/BV1FS4y1o7QB

node 单线程 如果做“CPU 密集的工作”或者“同步的 IO 操作”就会卡住
这个是一定要避免的 如果没法避免就必须用 worker https://nodejs.org/api/worker_threads.html

然后保证主线程不卡住就没问题了
所以 node 做 IO 密集的工作 性能还是可以的

不够就上多个 node 进程 然后外面做负载均衡就是
尽量无状态 如果必须要 session 就用 redis

"某个条件触发了耗时的 cpu 运算"
耗时 CPU 运算目前两种方法,核心概念就是保证主线程不阻塞
1. 把这部分放到其他子线程或者进程 然后主线程异步处理
2. 如果上面太难 就把需要长时间 CPU 处理的运算打碎成多个短暂同步运算然后异步处理 保证主线程不要长时间卡住

对于 2 比如一个运算需要 10s 那么就把它打碎成 1000 个小运算 每个 0.1s 然后中间异步等待
这样主线程就不会因为一个 10s 的运算而阻塞 10s
虽热这样一来 原先 10s 的事情可能变成了 15s 或者更久 但是至少不会长时间阻塞其他的请求

当然 2 这种方法比较时候低并发或者低概率触发的情况
如果真的高并发情况大概率 还是要用 1 来处理

我服了,CPU 密集和 IO 密集都不懂吗,在 JS 里一切 IO 都是异步的,不可能会卡,除非是 CPU 密集运算

不知道现在前端 SSR 框架算不算 CPU 密集型计算,还有 node 里运行 marked md2html 都会卡一下,多了估计会影响

cpu 密集型就多搞一些进程啊 这是问题吗

别拿 java 和浏览器 js 的概念去套 node

Node 是如何应对高并发场景的?答:异步非阻塞。

JavaScript 的生态根基简单来讲就是语言+API 。
JavaScript 是一门脚本语言,一门语言要想有实际用途就得有能力调用各个系统,那么就需要各个系统面向 JavaScript 提供 API ,比如你计算了 1+2 ,能得出结果 3 ,但你要想看到这个结果就得让操作系统帮你显示出来,于是操作系统(中间省略很多环节)给 JS 提供了个 console API ,你可以使用 console.log 来(中间省略很多环节)调用操作系统把 3 显示出来。

所以 Node 不等于 JS ,JS 语言的执行能力只是 Node 的一项子功能而已。

原生 JavaScript 语言是单线程执行的,但 Node 不是单线程的,Node 为 JS 语言提供了一些 API ,其中大部分都是 IO 相关的 API ,比如网络访问、文件系统访问等。

Node 有一个假设,就是很多应用场景下 IO 操作的工作量要远远大于计算操作。比如大多 Web 应用服务都是响应网络请求( IO 操作),经过简单的逻辑计算,然后进行数据库请求( IO 操作),那么假设只要 CPU 不闲着,IO 负载很可能会比 CPU 负载先用满。

Node 如何做到让 CPU 不闲着?答:计算单线程执行,IO 多线程执行(异步),但计算可以不等着 IO 完成(异步非阻塞)。

不调用任何 API ,纯进行 JS 计算,比如算斐波那契数列,1+2=3,2+3=5……这个只能单线程执行,算 2+3=5 的时候必须等着 1+2 出结果,只不过此时 CPU 并没有闲着而已。
如果在计算出每一个数字的时候,把数字写到硬盘上,这个写硬盘的操作就是 IO 操作;
假设没有异步非阻塞机制,应该是这样的:计算 1+2 ,得出 3 ,执行将 3 写入硬盘,等待写入完成,写入完成后计算 2+3……CPU 在等待的时候是闲着的,时间基本浪费在等待将 3 写入硬盘。
现在 Node 给你了一个能力,就是你可以在向硬盘写入 3 的时候选择不等着它完成,直接继续算 2+3 ,这就相当于有 1 个线程在不停算斐波那契数列,额外还有多个线程帮你把每个结果存硬盘。

回到题主的场景描述,Node 接收到一个请求之后,如果进行简单逻辑计算后就直接操作数据库( IO 操作)或应答( IO 操作)的话,可以选择不等着 IO 操作完成,继续处理下一个请求,等某个 IO 操作完成了就会回来调用后续的 JS 程序。

但如果执行的是异常复杂的计算,比如视频转码,如果是在处理请求的线程里做的话,一定会抢占预期用于处理请求的 CPU 时间,导致请求“卡住”。不过你猜怎么着,Node 其实是提供了多线程 API ( Worker threads )和多进程 API ( Child process ),你完全可以像其他语言那样使用多线程和多进程来进行优化。除此之外 Node 还提供了面向 C/C++的 N-API 以及面向很多语言的 WebAssembly ,在需要极端计算性能的场景下不至于完全放弃 JS 技术栈。

在设计业务逻辑时, 一般不会把需要 1s 处理时间得超重型任务的触发时机留给外界触发, 这种一般在设计上就会定时或使用其他方法手动触发.
node 在除非是在处理纯计算类的代码, 一般都会有时机切换到其他任务处理过程上(比较典型的切换代码标志就是 await), 所以一般来说很难产生像 1 楼所描述的处理请求按顺序等待的情况.

我觉得最好还是诚实一点:应对不了。。。

赞!学习到了。

首先:一个请求卡住了,后续其他请求并不会卡住,触发是第一个卡住的原因是因为( CPU 密集型计算)

你可以试试
先访问 http://127.0.0.1:3000/?t=10000 再访问 http://127.0.0.1:3000/?t=0 第二次结果是秒回的

const http = require(‘http’);
const { URL } = require(‘url’);
const { setTimeout } = require(‘timers/promises’);

const hostname = ‘127.0.0.1’;
const port = 3000;

const server = http.createServer(async (req, res) => {
const t = new URL(http://${hostname}:${port}${req.url}).searchParams.get(‘t’);
// 模拟耗时操作
await setTimeout(t);

res.end(Hello World:${t});
});

server.listen(port, hostname, () => {
console.log(Server running at http://${hostname}:${port});
});

如果将 await setTimeout(t) 换成 while(true) 的话,是无法接受并处理之后的请求。之所以 await setTimeout(t) 不会将主线程占用的原因是它属于定时器,是有特殊的调度处理的,不会占用主线程。

只有不是 CPU 密集型计算(比如从 1 循环加到一亿 )都不会卡着,你可以试试比如说请求、读文件等等

const http = require(‘http’);
const {
URL
} = require(‘url’);
const {
setTimeout
} = require(‘timers/promises’);

const hostname = ‘127.0.0.1’;
const port = 3001;

function fetch(url) {
return new Promise((resolve) => {
http.get(url, (res) => resolve(res));
})
}


const server = http.createServer(async (req, res) => {
const t = new URL(http://${hostname}:${port}${req.url}).searchParams.get(‘t’);
// 模拟耗时操作
await fetch(http://127.0.0.1:3000/?t=${t});

res.end(Hello World:${t});
});

server.listen(port, hostname, () => {
console.log(Server running at http://${hostname}:${port});
});

是的,这些都是异步操作,会在线程池处理。针对题主所问,如果长时间阻塞主线程,的确无法再接受请求进来。

如果你没有计算耗时的操作,Node 会在相当短的时间把控制权交给下一个请求,直到你的数据库任务、或是网络请求完成时才会继续阻塞程序

Node.js虽然是单线程运行JavaScript代码,但它通过一系列机制有效地应对高并发场景。以下是Node.js单线程应对高并发的关键方式:

  1. 非阻塞I/O操作:Node.js的I/O操作(如文件读取、网络请求)是非阻塞的。它将I/O任务交给底层操作系统处理,并立即返回继续执行后续代码。例如:
const http = require('http');
const server = http.createServer((req, res) => {
  setTimeout(() => { // 模拟异步I/O操作
    res.writeHead(200);
    res.end('Hello World\n');
  }, 100);
});
server.listen(8000, () => {
  console.log('Server is running at http://localhost:8000/');
});
  1. 事件循环:Node.js通过事件循环机制不断地检查是否有事件发生,并执行相应的回调函数。异步操作完成时,会触发事件并添加到事件队列中,事件循环依次处理这些事件。
  2. 异步编程:Node.js支持异步函数、回调函数、Promise和async/await语法,允许开发者以更清晰和可控的方式编写并发逻辑。
  3. 集群模块:Node.js提供cluster模块,可在多核服务器上创建多个工作进程,实现负载均衡,从而处理更多的并发连接和请求。

综上所述,Node.js通过非阻塞I/O、事件循环、异步编程和集群模块等机制,在单线程模型下实现了高效的高并发处理。

回到顶部