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

2022-01-20 17:15:25 +08:00
 leebs

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

12007 次点击
所在节点    Node.js
43 条回复
pengtdyd
2022-01-21 01:20:34 +08:00
你考虑的太多了,你应该思考的是你们公司有这么大的业务量吗
kuangwinnie
2022-01-21 02:03:42 +08:00
所以 node js 不适合用来做 CPU-intensive 的操作啊;本来就是拿来做 IO intensive 的。
zeni123
2022-01-21 04:38:17 +08:00
@jorneyr CPU 计算密集型的任何语言都会卡住,需要 worker 来利用多个 CPU
georgema1982
2022-01-21 05:18:10 +08:00
一个请求卡住后,她会求助的:Step bro, I'm stuck
IvanLi127
2022-01-21 08:06:36 +08:00
你会卡住?那你就让这会卡住的部分放在这个线程外面,用其他线程跑就行了。
Biwood
2022-01-21 08:44:20 +08:00
event loop 怎么会卡住呢,甚至可以说处理高并发是优势,瓶颈在别的地方,几年前讨论的明明白白,现在这是退化了?
leebs
2022-01-21 09:30:00 +08:00
@Biwood 业务事件是程序员写的,这是有可能会卡住的。如果某个请求因为某个条件触发了耗时的 cpu 运算,其他请求只能等待,多线程处理的情况下,其他线程是不需要等待的。
byte10
2022-01-21 09:35:47 +08:00
@leebs 我靠。。。你。。你。你绝对需要看我的视频啦少年,首先 nodejs 是 nio 模型,还有基于事件轮训的设计,它不用等待 IO ,所有的 IO 也是一个事件。一个请求过来,接收完请求后,就生成一个事件,这个事件处理完后,就扔出一个 response 事件,事件池就会一直被轮训,一直被处理。nodejs 主要适合 IO 密集型,就是不要那么有很多计算性的代码,也就是你的业务代码不要太复杂就行了。

NIO 有一个绝对性优势,你可以看看我的视频,有讲解 nodejs 如何无视 IO 时间,做到吞吐量保持一致。https://www.bilibili.com/video/BV1FS4y1o7QB
yyfearth
2022-01-21 09:41:27 +08:00
@leebs node 单线程 如果做“CPU 密集的工作”或者“同步的 IO 操作”就会卡住
这个是一定要避免的 如果没法避免就必须用 worker https://nodejs.org/api/worker_threads.html

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

不够就上多个 node 进程 然后外面做负载均衡就是
尽量无状态 如果必须要 session 就用 redis
yyfearth
2022-01-21 09:48:06 +08:00
@leebs "某个条件触发了耗时的 cpu 运算"
耗时 CPU 运算目前两种方法,核心概念就是保证主线程不阻塞
1. 把这部分放到其他子线程或者进程 然后主线程异步处理
2. 如果上面太难 就把需要长时间 CPU 处理的运算打碎成多个短暂同步运算然后异步处理 保证主线程不要长时间卡住

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

当然 2 这种方法比较时候低并发或者低概率触发的情况
如果真的高并发情况大概率 还是要用 1 来处理
monkeyWie
2022-01-21 10:20:08 +08:00
我服了,CPU 密集和 IO 密集都不懂吗,在 JS 里一切 IO 都是异步的,不可能会卡,除非是 CPU 密集运算
juzisang
2022-01-21 10:44:25 +08:00
不知道现在前端 SSR 框架算不算 CPU 密集型计算,还有 node 里运行 marked md2html 都会卡一下,多了估计会影响
keepeye
2022-01-21 11:30:15 +08:00
cpu 密集型就多搞一些进程啊 这是问题吗
jguo
2022-01-21 11:31:05 +08:00
别拿 java 和浏览器 js 的概念去套 node
libook
2022-01-21 12:01:53 +08:00
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 技术栈。
13Sl
2022-01-21 12:51:09 +08:00
在设计业务逻辑时, 一般不会把需要 1s 处理时间得超重型任务的触发时机留给外界触发, 这种一般在设计上就会定时或使用其他方法手动触发.
node 在除非是在处理纯计算类的代码, 一般都会有时机切换到其他任务处理过程上(比较典型的切换代码标志就是 await), 所以一般来说很难产生像 1 楼所描述的处理请求按顺序等待的情况.
Austaras
2022-01-21 16:20:31 +08:00
我觉得最好还是诚实一点:应对不了。。。
zzlatan
2022-01-21 16:22:08 +08:00
@libook 赞!学习到了。
KouShuiYu
2022-01-21 17:43:36 +08:00
首先:一个请求卡住了,后续其他请求并不会卡住,触发是第一个卡住的原因是因为( 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}`);
});
coseylee
2022-03-03 14:25:56 +08:00
@KouShuiYu 如果将 await setTimeout(t) 换成 while(true) 的话,是无法接受并处理之后的请求。之所以 await setTimeout(t) 不会将主线程占用的原因是它属于定时器,是有特殊的调度处理的,不会占用主线程。

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/829521

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX