近期,Bun 发布了 v1.1.25
版本,并高调宣布在最新的性能测试中,每秒可以处理 129 万个 HTTP 请求,我们一起来看看这个版本究竟做了哪些改进吧。
node:cluster 支持 Bun 现在支持 node:cluster
API。
通过使用这个 API,你可以在同一个端口上运行一组 Bun workers
,从而实现更高的吞吐量和利用率。对于拥有多个 CPU
核心的机器来说,这是在生产环境中进行负载均衡的最佳选择。
下面是一个工作原理的示例:
主要的 worker 会创建 n 个子 worker,一般数量与 CPU 核心数相同; 每个子 worker 都会监听相同的端口(使用 reusePort
); 传入的 HTTP 请求会在子 worker 之间进行负载均衡分配处理。 import cluster from "node:cluster" ;import http from "node:http" ;import { cpus } from "node:os" ;import process from "node:process" ;if (cluster.isPrimary) { console .log(`主进程 ${process.pid} 正在运行` ); // 根据 CPU 核心数创建 N 个子进程 for (let i = 0 ; i < cpus().length; i++) { cluster.fork(); } cluster.on("exit" , (worker, code, signal) => { console .log(`工作进程 ${worker.process.pid} 退出` ); }); } else { // 请求会由子进程池中的工作进程处理,而不是主进程 http .createServer((req, res ) => { res.writeHead(200 ); res.end("hello world\n" ); }) .listen(3000 ); console .log(`工作进程 ${process.pid} 已启动` ); }
这个示例代码可以与 node:http
API 以及 Bun.serve()
API 一起使用。
import cluster from "node:cluster" ;import { cpus } from "node:os" ;if (cluster.isPrimary) { // 主进程逻辑:根据 CPU 核心数创建子进程 for (let i = 0 ; i < cpus().length; i++) { cluster.fork(); } } else { // 工作进程逻辑:使用 Bun 框架提供的 API 运行服务器 Bun.serve({ port : 3000 , // 监听的端口号 fetch(request) { return new Response(`你好,来自工作进程 ${process.pid} ` ); }, }); }
注意,目前 reusePort
只在 Linux 系统上有效。在 Windows 和 macOS 上,操作系统无法像预期那样对 HTTP 连接进行负载均衡处理。
开始支持 V8 公开 C++ API Bun 现在支持了 V8 的公开 C++ API,这使得像 cpu-features
这样的软件包可以在 Bun 中正常工作。
这是一个值得注意的变化,因为 Bun 不是像 Node.js
一样构建在 V8
之上。相反,Bun 是构建在 JavaScriptCore
上的,JavaScriptCore
是 Safari
使用的 JavaScript
引擎。
这意味着官方需要实现一个自己的 C++
翻译层,将 V8 的 API 与 JavaScriptCore
进行对接。
#include "v8.h" #include "V8Primitive.h" #include "V8MaybeLocal.h" #include "V8Isolate.h" namespace v8 {enum class NewStringType { /* ... */ };class String : Primitive {public : enum WriteOptions { /* ... */ }; BUN_EXPORT static MaybeLocal NewFromUtf8 (Isolate* isolate, char const * data, NewStringType type, int length = -1 ) ; BUN_EXPORT int WriteUtf8 (Isolate* isolate, char * buffer, int length = -1 , int * nchars_ref = nullptr , int options = NO_OPTIONS) const ; BUN_EXPORT int Length () const ; // 将本地字符串对象转换为JavaScriptCore的JSString对象 JSC::JSString* localToJSString () { return localToObjectPointer<:jsstring>(); } }; }
这是一项非常艰巨的工程,JavaScriptCore
和 V8 以不同的方式表示 JavaScript
值。V8 使用了移动式、并发的垃圾收集器,并且有明确的句柄作用域,而 JavaScriptCore
使用了非移动式的并发垃圾收集器,它会扫描堆栈内存(类似于隐式句柄作用域)。
在此之前,如果你尝试导入一个使用这些API的软件包,你会得到如下的错误提示:
console .log(require ("cpu-features" )());
dyld[94465]: missing symbol called fish: Job 1, 'bun index.ts' terminated by signal SIGABRT (Abort)
现在,像 cpu-features
这样的包可以导入并在 Bun 中运行。
$ bun index.ts { arch : "aarch64" , flags : { fp : true , asimd : true , // ... }, }
为什么要支持 V8 的内部 API 么?
最初 Bun 并没有打算支持这些 API,但是在发现许多受欢迎的软件包依赖于这些 API 后,开始决定加以支持,这些软件包包括:
在这个版本中,只有cpu-features
从上述列表中受到支持,其他 API 正在努力支持中。
使用 @aws-sdk/client-s3 实现的 S3 上传速度提高了5倍 Bun 修复了 node:http
客户端实现中的一个 bug
,这使得上传到 S3
的速度提高了5倍。
独立可执行文件中的 Worker Bun 的单文件独立可执行文件现在支持绑定 Worker
和 node:worker_threads
。
// main.ts console .log("Hello from main thread!" );new Worker("./my-worker.ts" );
// my-worker.ts console .log("Hello from another thread!" );
要使用 worker 编译独立可执行文件,可以将文件入口传递给 bun build --compile
命令:
bun build --compile ./main.ts ./my-worker.ts
这将在生成的可执行文件中将 my-worker.ts
和 main.ts
作为独立的入口点进行打包。
使用 OMGJIT 在 Windows 上实现更快的 WebAssembly Windows
上的 WebAssembly
现在支持 JavaScriptCore
的优化即时编译器 (JIT
),称为 OMGJIT
。
Node.js兼容性改进 execa
现在可以正常工作了。
Bun 修复了一个不能正确支持 EventTarget的setMaxListeners
的 bug
。这个问题影响到了像 execa
这样的包,会导致错误报 undefined is not a function
。
import { execa } from "execa" ;const { stdout } = await execa`echo "test"`
如果你遇到类似的错误,现在已经得到修复:
91 | const controller = new AbortController(); 92 | setMaxListeners(Number.POSITIVE_INFINITY, controller.signal); ^ TypeError: undefined is not a function at node:events:101:30 at spawnSubprocessAsync (/node_modules/execa/lib/methods/main-async.js:92:2) at execaCoreAsync (/node_modules/execa/lib/methods/main-async.js:26:32) at index.mjs:3:26
问题已修复:在调用 destroy()
关闭 TCP
连接后,与 node:net
的连接出现挂起
也是一个 bug
修复,在调用 destroy()
关闭 TCP
连接后,进程没有始终正确退出,因为事件循环仍然处于活动状态。这有时会导致像 postgres
这样的包无限期地挂起。
import net from "node:net" ;const server = net.createServer((socket ) => { // 当socket连接成功时触发connect事件 socket.on("connect" , (data) => { socket.destroy(); // 这会销毁连接, // 但是事件循环仍然处于活动状态 }); }); server.listen(3000 );