TT Lab
开始
学习 学习路径 课程

慢的不是一个请求,而是全部

函数名末尾的四个字母决定了事故

在 TT Lab 中继续学习

一句话总结

crypto.pbkdf2 与 crypto.pbkdf2Sync 做的是同样的计算,花的是同样的时间。不同的只是在哪里运行,而这个差别决定了是“只有这个请求慢”,还是“全都慢”。而且,异步的东西也并不全都在同一个地方。

为什么需要它

“改成异步就行了”这种说法太笼统了。实际上有三个位置,三者的性质各不相同。

第一,在事件循环上运行的事。JSON.parse、正则表达式、对大数组排序,以及所有名字以 Sync 结尾的函数,都在这里。这些事在运行期间,会让该进程的其他一切都停下来。

第二,libuv 线程池。读文件、压缩、异步加密运算会被送到这里。事件循环是自由的,但槽位是固定的。默认是四个槽位,所以如果同时送出五个繁重的任务,第五个要等到前面某一个结束才能开始。

第三,内核。套接字的读写,以及 dns.resolve*() 在这里。事件循环只是等着,直到操作系统告知“准备好了”。既不占用线程,也不占用 CPU。

如果分不清这三个位置,就会出现奇怪的事。比如把图片压缩改成异步,保住了事件循环,但从那以后读文件变慢了。因为两者共用同样的四个槽位。只看代码的话,这两个功能毫无关系。

工作原理

哪些 API 用线程池,不是要背的东西,文档里写着。Node 的命令行选项文档中 UV_THREADPOOL_SIZE 一节直接列出了清单——除文件监视和显式同步函数之外的所有 fs API,crypto.pbkdf2()、crypto.scrypt()、crypto.randomBytes()、crypto.randomFill()、crypto.generateKeyPair() 这样的异步加密 API,dns.lookup(),以及除显式同步版本之外的所有 zlib API。同一份文档写明默认大小是 4,libuv 一侧的文档写明绝对上限是 1024(libuv 线程池)。

在这张表里最容易混淆的,是 dns.lookup 与 dns.resolve4 的区别。两者都是把名字转成地址,前者在线程池里调用操作系统的 getaddrinfo(3),后者则直接通过网络查询。Node 的 DNS 文档对后者明确写道,“这种网络通信始终是异步进行的,不使用 libuv 的线程池”。所以在域名查询很多的服务中,读文件变慢的事确实会发生。

槽位有四个这一点,一测就能看出来。送出四个同样分量的异步任务,四个几乎在同一时刻结束;送出八个,就会分成两批。在实验镜像的 Node 22.11.0 上用 2 核 CPU 测量,结果如下。

4개:  298 301 302 307 ms                          — 함께 끝난다
8개:  315 317 321 325 | 700 703 703 704 ms        — 뒤 네 개는 한 바퀴를 기다린다

那么,增加槽位是不是就行了?如果用 UV_THREADPOOL_SIZE=8 重新测量,八个都在 710ms 前后结束。波浪消失了,但全部结束的时刻没变。因为增加的只是能同时开始的任务数,而不是替你计算的 CPU。同一份文档补充的提醒也很重要——在程序内部通过 process.env 修改这个值,是不被保证的。因为线程池是在用户代码运行之前很早就创建好的。

在现场相遇的样子

最常见的是根本没有异步版本的事。JSON.parse 就是代表。即使用 fs.readFile 异步读取文件,把那个字符串变成对象的事,也是在事件循环上发生的。在 Node 22.11.0 上解析一个 12.9MB 的 JSON,花了 76ms,在此期间事件循环停顿了 58ms。没有可以挪去的地方,剩下的选择只有限制请求体大小。请求体上限就是延迟预算,原因就在这里。

第二是名字末尾的四个字母。在同一个镜像上,用 zlib.gzipSync 压缩 8MB,事件循环停顿 183ms,用 zlib.gzip 压缩则是 9ms。工作本身分别是 178ms 和 219ms,差不多。在代码评审中找 Sync,是性能优化的第一行,原因就是这些数字。特别是,启动时读取配置文件的 readFileSync 没有问题,而请求处理路径上的 readFileSync 就是事故——同一个函数,在哪里调用会改变判断。

下一项实验要做什么

把官方文档的清单抄成一张表,并亲自测量来确认这张表是否正确。把同一个计算分别用同步和异步各运行一次,测量事件循环延迟,用没有异步版本的 JSON.parse 确认没有地方可以挪,再送出八个同样的任务,数一数线程池的槽位。提高 UV_THREADPOOL_SIZE,同时测量波浪消失与总时间不变这两点,最后把同样的方法用到 zlib 上。