Gadzan

一次 Vite 打包报错后对 V8 内存管理的思考

heap out of memory

项目中使用了 TypeScript + Vite + Vue,一次手贱升级了 Vite 版本,部署到预发布环境时打包出错了:

Jenkins 输出日志:

11:07:27 <--- Last few GCs --->
11:07:27 
11:07:27 [15088:0x6437390]   103233 ms: Mark-sweep (reduce) 2046.0 (2082.6) -> 2045.1 (2082.9) MB, 3591.7 / 0.0 ms  (average mu = 0.155, current mu = 0.011) allocation failure scavenge might not succeed
11:07:27 [15088:0x6437390]   106853 ms: Mark-sweep (reduce) 2046.6 (2083.4) -> 2045.5 (2083.6) MB, 3610.4 / 0.0 ms  (average mu = 0.084, current mu = 0.003) allocation failure scavenge might not succeed
11:07:27 
11:07:27 
11:07:27 <--- JS stacktrace --->
11:07:27 
11:07:27 FATAL ERROR: Reached heap limit Allocation failed - JavaScript heap out of memory
11:07:27  1: 0xb00e10 node::Abort() [node]
11:07:27  2: 0xa1823b node::FatalError(char const*, char const*) [node]
11:07:27  3: 0xcee09e v8::Utils::ReportOOMFailure(v8::internal::Isolate*, char const*, bool) [node]
11:07:27  4: 0xcee417 v8::internal::V8::FatalProcessOutOfMemory(v8::internal::Isolate*, char const*, bool) [node]
11:07:27  5: 0xea65d5  [node]
11:07:27  6: 0xeb5cad v8::internal::Heap::CollectGarbage(v8::internal::AllocationSpace, v8::internal::GarbageCollectionReason, v8::GCCallbackFlags) [node]
11:07:27  7: 0xeb89ae v8::internal::Heap::AllocateRawWithRetryOrFailSlowPath(int, v8::internal::AllocationType, v8::internal::AllocationOrigin, v8::internal::AllocationAlignment) [node]
11:07:27  8: 0xe79dda v8::internal::Factory::NewFillerObject(int, bool, v8::internal::AllocationType, v8::internal::AllocationOrigin) [node]
11:07:27  9: 0x11f33d6 v8::internal::Runtime_AllocateInYoungGeneration(int, unsigned long*, v8::internal::Isolate*) [node]
11:07:27 10: 0x15e7cf9  [node]
11:07:27 sh: line 1: 15088 Aborted                 vue-tsc --noEmit

看起来是内存溢出了,于是在 vite 的 issue 里查一下发现了内存溢出的话题

vite build error: out of memory · Issue #2433 · vitejs/vite
PLEASE READ This issue has affected a number of people. Please do not comment with comments along the lines of “I’m also hitting this”, but add a thumbs up to the issue instead. We know this…

讨论的结果是,通过下面这个指令来放宽 linux 内存的使用限制:

cross-env NODE_OPTIONS=--max-old-space-size=4096 vite build

问题可能在于低版本的 Node 可能有 GC 上的问题

修改打包脚本,重新部署,发现还是有问题;

原以为是 vite build 阶段发生的问题,仔细看 output log 发现是在 ts 编译阶段 vue-tsc --noEmit 发生的错误;

所以放宽内存限制的指令得放到 vue-tsc --noEmit 前面

cross-env NODE_OPTIONS=--max-old-space-size=4096 vue-tsc --noEmit && vite build

在 vue-tsc 的 github 上也发现了这个话题:

Potential memory leak of `vue-tsc --noEmit -w` · Issue #1106 · vuejs/language-tools
Reproduction pnpm create vite bug-vue-tsc select vue and vue-ts pnpm i vue-tsc@latest -D NODE_OPTIONS=‘--max_old_space_size=700’ pnpm exec vue-tsc --noEmit -w Modify any code of App.vue and wait fo…

关于 Node 的 max-old-space-size

参数可以参考下面文章:

How do I determine the correct “max-old-space-size” for Node.js?
I’m having some trouble to understand how Node.js acts based on the parameter max-old-space-size. In my case, for example, I’m running two t2.small AWS instances (2GB of RAM). Not sure why, but I d…

按照我对原文的理解,我翻译一下:

“old space”是 V8 所管理的堆中最大和最可配置的部分,也是垃圾收集(GC)作用的地方,--max-old-space-size 标志控制其最大大小。随着内存消耗接近极限,V8 将花费更多时间在垃圾收集上,以释放未使用的内存。

如果堆内存消耗(即 GC 无法释放的活对象)超过了限制,V8 就会让你的进程崩溃(因为没有其他办法),所以你不要把它设得太低。当然,如果设置过高,那么 V8 允许的额外堆使用量可能会导致整个系统内存耗尽(因为没有其他选择,所以会 swap 或杀死随机进程)

总之,在拥有 2GB 内存的机器上,我可能会将 --max-old-space-size 设置为 1.5GB 左右,以便为其他用途留出一些内存,避免 swap。

关于 max-old-space-size 选项,在 Node 官方文档有记录。

Command-line API | Node.js v21.1.0 Documentation

max-old-space-size 的 官方文档

对于 2GB 的机器,您可能应该使用:

 NODE_OPTIONS=--max-old-space-size=1536

'new space' 与 'old space' 到底是什么?

这篇文章回答得很好

What are v8 old space and new space?
Node.js has two parameters to control memory allocations as I know of: --max_new_space_size and --max_old_space_size What exactly are those mentioned NEW SPACE and OLD SPACE things?

确定内存使用量

您可以使用 free -m 查看 Linux 机器上的可用内存。请注意,您可以考虑可用的 free 和 buffers/cache 内存的总和,因为 buffers/cache 可以立即丢弃这些缓冲区一种使用未使用内存的好方法)。

Node 文档中还提到:

On a machine with 2GB of memory, consider setting this to 1536 (1.5GB)

因此考虑到基本操作系统所需的内存容量不会有太大变化,因此您可以在 4GB 机器上愉快地定为 3.5GB 左右。

要注意默认值并查看更改的效果:
默认为 2GB:

 $ node

> v8.getHeapStatistics()
{
  ....
  heap_size_limit: 2197815296,
}

2197815296 是 2GB 字节。

当设置为8GB时,可以看到 heap_size_limit 变化:

 $ NODE_OPTIONS=--max_old_space_size=8192 node
Welcome to Node.js v14.17.4.
Type ".help" for more information.
> v8.getHeapStatistics()
{
  ...
  heap_size_limit: 8640266240,
  ...
}

关于 V8 的 old space 是什么,可以参考下面文章介绍:

🚀 Visualizing memory management in V8 Engine (JavaScript, NodeJS, Deno, WebAssembly)
Let us take a look at how the V8 engine for JavaScript & WebAssembly manages memory for Browsers and NodeJS.
🚀 Demystifying memory management in modern programming languages
Let us take a look at how modern programming languages manage memory.


打赏码

知识共享许可协议 本作品采用知识共享署名 4.0 国际许可协议进行许可。

评论