网易首页 > 网易号 > 正文 申请入驻

Node.js扛住1万并发却几乎不耗CPU,内核藏了什么骚操作

0
分享至


你的代码里写了一行await fetch(url),Node.js服务器同时处理着一万个连接,CPU占用率却几乎没动。

这不是魔法。是内核在替你站岗。

JavaScript引擎把请求扔给libuv,libuv调用epoll_wait,让内核盯着这一万个文件描述符。内核40毫秒没吭声,然后突然说:"3个准备好了。"事件循环醒来,只处理这3个。剩下9997个连接在等,但你的CPU一滴汗都没出。

整个把戏就一次系统调用。内核负责等待,你负责干活。

你大概听过异步I/O效率高。可能信了,可能因为某个 benchmark 就信了。但不懂底下那层,你就是在开一架看不懂仪表的飞机。

那个迟早会撞上的bug

你写异步Python,代码看着都对,结果还是阻塞。服务器每次跑某个函数就卡死300毫秒。加worker,没用。问题藏在某个依赖里——一个time.sleep,一个阻塞的DNS查询,一个网络文件系统的open()。这调用不向事件循环交权,把线程当人质扣到完事。

只有搞懂机制,才能明白为什么这是灾难。修复方案不是"加个async",是"找出你其实没在搞非阻塞I/O的地方"。

先别急着找解药,看看病是什么。

1983年:一个read()卡死999个客户

1983年,你在写服务器。客户连上来,你read()套接字。没数据?进程阻塞——睡着,CPU去干别的,数据来了再叫醒你。这叫阻塞I/O,对一个客户完全没问题。

放大到一千个客户。每个read()都可能阻塞。单线程进程卡在第一个没话说的客户身上,另外999个有数据的干等着。 obvious fix 是线程——一个客户一个线程。但一千个线程就是一千个栈(默认通常8MB一个),一千个内核调度上下文,切来切去的开销。

1983年你玩不起。2024年,算术照样难看。现代Web服务器规模化后要处理几十万连接。你不可能有几十万个线程。

你想要的是:"这儿有十万个文件描述符,哪个有动静告诉我。"一次调用。内核阻塞到有活干。你醒来,处理刚好准备好的,回去睡。


这就是问题。下面是按好用程度排序的解法。

select:1983年的第一次尝试

select随4.2BSD在1983年到来——伯克利团队解决多路复用的初版方案。它让程序监视多个文件描述符,等其中一个准备好读、写或有异常条件。

但select有个硬伤:每次调用都要把整个文件描述符集合从用户空间拷进内核,有结果了再拷出来。监视几百个还行,几千个就开始喘了。更糟的是,返回后你得遍历整个集合才能知道谁准备好了——O(n)扫描,连接数上去就是灾难。

POSIX后来定义了poll,2001年Linux 2.1.23实现了epoll,BSD和macOS搞了kqueue。它们解决同一个问题:告诉内核"这些是我关心的描述符",之后每次只问"谁准备好了",不用反复搬运整个列表。

epoll的API设计透着Unix的糙劲。你先epoll_create弄个实例,epoll_ctl增删改要监视的描述符,然后epoll_wait阻塞到有事件。关键优化:描述符集合存在内核里,不用每次往返搬运。

但真正的狠活是边缘触发(edge-triggered)模式。默认的水平触发(level-triggered)里,只要描述符还有数据可读,epoll_wait就会一直报告它。边缘触发只在你从"没数据"变成"有数据"的那一刻通知一次——你得一次性读完,否则漏掉。

边缘触发像给服务器开了个玩笑:性能更好,因为内核少打扰你;但也更容易搞砸,因为读不干净就永远丢了。Nginx用边缘触发,但它是Nginx,写了二十年C的人肉GC。

io_uring:Linux的掀桌操作

2019年,Linux 5.1来了,带着io_uring。不是渐进改良,是重新设计。

之前所有方案——select、poll、epoll——都绕不开一个模式:你先发起系统调用,内核干完,再返回结果。io_uring说:别打电话了,放共享队列里,我批量处理。

它搞了两个环形缓冲区,用户空间和内核共享。提交队列(SQ)放你要干的I/O操作,完成队列(CQ)放干完的结果。你可以塞几十个读请求进去,做一次系统调用通知内核,然后内核可能用中断合并批量回报。

基准测试里,io_uring能把I/O吞吐量提30%以上,延迟降一半。不是因为它让磁盘转更快,是减少了用户态和内核态之间的往返次数——上下文切换是隐形税,io_uring在避税。

但别急着迁移。io_uring需要内核5.1+,某些发行版默认没开,而且API还在演变。2022年的补丁加了缓冲区注册、轮询模式,2023年有人发现某些配置下的安全漏洞。它很快,但也很新。


事件循环:不是你在用,是你在被用

回到JavaScript。你写await的时候,以为自己在指挥异步流程。实际上V8引擎把Promise回调塞进libuv的事件循环,libuv决定什么时候让出线程。

这个循环大概长这样:检查有没有到期的定时器,处理pending的I/O回调,运行idle handle,再检查有没有close的handle。一轮结束,计算下次该睡多久,调用epoll_wait或等价物。

关键洞察:你的"异步"代码其实跑在单线程里。await只是语法糖,把函数切成状态机,在Promise决议时恢复。真正的并行发生在内核——它在硬件层面同时盯着所有文件描述符。

这就是为什么那个阻塞调用如此致命。一个time.sleep(0.1)不会魔法般变成非阻塞,它会占住整个线程100毫秒,期间事件循环停摆,所有 pending 的Promise都在排队。

Python的asyncio更惨。它试图在保留同步生态的同时搞异步,结果就是asyncsync的接缝处全是地雷。你调了个看起来无害的库函数,它内部读了配置文件——阻塞磁盘I/O,整个事件 loop 冻结。

Node.js相对干净,因为生态从头就是异步优先。但照样有坑:fs模块的同步方法、某些加密操作、甚至console.log在特定版本的同步实现。这些不是bug,是设计取舍,但代价由你付。

诊断:找出那个内鬼

怎么发现阻塞调用?Linux有perf,可以挂到进程上看谁在烧CPU时间。Node.js有--prof生成V8日志,0x工具能可视化火焰图。Python的asynciodebug模式,会警告执行超过100ms的协程。

但最朴素的办法往往最有效:加日志,测延迟。在事件循环的tick前后打时间戳,如果某个间隔异常拉长,就是有人在搞阻塞。

修复路径通常不是重写整个代码库,是隔离。把阻塞操作扔进线程池(Node的worker_threads,Python的run_in_executor),让事件循环保持呼吸。代价是线程切换开销,但比卡死整个服务器强。

更彻底的方案是换运行时。Go的goroutine调度器在语言层面解决这问题,阻塞调用会自动让出,调度器把goroutine挂到别的线程。Rust的tokio用work-stealing调度,I/O和计算任务混跑。它们不是没代价,是把代价藏得更深。

那个40毫秒的秘密

回到开头那40毫秒。内核为什么等这么久?因为epoll_wait有个timeout参数,libuv通常设成几十毫秒,平衡响应速度和系统调用频率。设成0就是忙等,CPU100%;设成-1就是无限等,第一个事件来之前彻底睡着。

这几十毫秒是妥协的艺术。现代数据中心里,一次网络往返要几百微秒到几毫秒,40毫秒像永恒。但对事件循环来说,它意味着每秒最多25次唤醒——足够处理大多数I/O密集型负载,又不至于让CPU空转。

特别声明:以上内容(如有图片或视频亦包括在内)为自媒体平台“网易号”用户上传并发布,本平台仅提供信息存储服务。

Notice: The content above (including the pictures and videos if any) is uploaded and posted by a user of NetEase Hao, which is a social media platform and only provides information storage services.

相关推荐
热点推荐
他是学霸会享受生活,清华毕业20年不上班,每年靠专利就衣食无忧

他是学霸会享受生活,清华毕业20年不上班,每年靠专利就衣食无忧

乐天闲聊
2026-04-01 18:19:34
全新奥迪A6L上市,售价区间32.29万元-43.59万元

全新奥迪A6L上市,售价区间32.29万元-43.59万元

财经汽车
2026-04-02 08:57:30
张雪回应影视化,希望顶级团队操刀,网友喊韩寒尹正接拍飞驰人生

张雪回应影视化,希望顶级团队操刀,网友喊韩寒尹正接拍飞驰人生

四斤
2026-04-01 11:06:39
12家车企公布3月销量:比亚迪拿下第一,广汽丰田暂居前三

12家车企公布3月销量:比亚迪拿下第一,广汽丰田暂居前三

车市红点
2026-04-01 20:41:36
Claude Code 泄露的代码里,处处写着:这家公司人品不行

Claude Code 泄露的代码里,处处写着:这家公司人品不行

InfoQ
2026-04-02 10:01:24
江苏省拟推荐3人,正在公示

江苏省拟推荐3人,正在公示

南京择校
2026-04-02 20:23:43
WTI原油期货涨超13%

WTI原油期货涨超13%

财联社
2026-04-02 21:27:21
斯诺克战报:霍金斯7-5胜罗伯逊,希金斯3-1领先塞尔比

斯诺克战报:霍金斯7-5胜罗伯逊,希金斯3-1领先塞尔比

民哥台球解说
2026-04-02 23:12:50
特朗普讲话暗藏三大玄机,美军中东“开溜”倒计时?专家:仗是打不起了,但“赢学”必须要赢

特朗普讲话暗藏三大玄机,美军中东“开溜”倒计时?专家:仗是打不起了,但“赢学”必须要赢

大象新闻
2026-04-02 23:49:04
尼泊尔的“一妻多夫”有多尴尬?看完她们的生活后,满满的辛酸

尼泊尔的“一妻多夫”有多尴尬?看完她们的生活后,满满的辛酸

芳芳历史烩
2026-03-31 20:27:45
周杰伦10余年前求婚画面首次公开:周杰伦单膝跪地向昆凌送上戒指,随后两人相拥看浪漫烟花

周杰伦10余年前求婚画面首次公开:周杰伦单膝跪地向昆凌送上戒指,随后两人相拥看浪漫烟花

鲁中晨报
2026-04-02 14:21:04
新华社记者探访伊朗导弹袭击后的以色列迪莫纳

新华社记者探访伊朗导弹袭击后的以色列迪莫纳

新华社
2026-04-02 16:12:16
小杨阿姨见到马筱梅终于老实了,和筱梅妈妈出门买菜 两人互不搭

小杨阿姨见到马筱梅终于老实了,和筱梅妈妈出门买菜 两人互不搭

傲傲讲历史
2026-04-02 23:48:19
毛新宇罕见失态,看到一陌生人当场痛哭:太像我爷爷年轻时了

毛新宇罕见失态,看到一陌生人当场痛哭:太像我爷爷年轻时了

锅锅爱历史
2026-04-02 23:50:08
看看这是林志玲多大的时候?

看看这是林志玲多大的时候?

情感大头说说
2026-04-02 08:47:20
检查一下:卧室里有“这7样”东西,尽快拿走!别给自己找麻烦

检查一下:卧室里有“这7样”东西,尽快拿走!别给自己找麻烦

抠搜侠
2026-04-02 16:01:42
中方回应特朗普全国讲话

中方回应特朗普全国讲话

新浪财经
2026-04-02 15:40:25
上海电影院现场被捉奸,带情夫当老公面出轨,狗血女主角真容曝光

上海电影院现场被捉奸,带情夫当老公面出轨,狗血女主角真容曝光

静若梨花
2026-03-01 16:25:46
杭州市场监管部门对优思益推手立案调查

杭州市场监管部门对优思益推手立案调查

新京报
2026-04-02 11:08:59
退休人员也要缴费了!4月起执行,每月扣多少、谁能免,一次说清

退休人员也要缴费了!4月起执行,每月扣多少、谁能免,一次说清

观察者海风
2026-04-02 23:48:25
2026-04-03 00:31:00
硬核玩家2哈
硬核玩家2哈
沉淀中,勿扰
694文章数 3关注度
往期回顾 全部

科技要闻

三年亏20亿,最新估值58亿,Xreal冲刺港股

头条要闻

北京89岁奶奶困屋内从27层翻窗下爬 爬到21层吓坏邻居

头条要闻

北京89岁奶奶困屋内从27层翻窗下爬 爬到21层吓坏邻居

体育要闻

邵佳一的改革,从让每个人踢舒服开始

娱乐要闻

宋宁峰带女儿出轨,张婉婷找董璇哭诉

财经要闻

市场被特朗普一句话打醒 滞胀交易回归

汽车要闻

轴距2米7/后排能跷腿 试驾后驱小车QQ3 EV

态度原创

本地
亲子
家居
游戏
公开课

本地新闻

从学徒到世界冠军,为什么说张雪的底气在重庆?

亲子要闻

希望每个来自星星的孩子,都可以健康快乐成长

家居要闻

岁月静好 典雅新章

卡普空小萝莉新游发售倒计时!最早4月16日可玩

公开课

李玫瑾:为什么性格比能力更重要?

无障碍浏览 进入关怀版