起步
记录是因为异常的原因是 node 自身的问题。
问题说明
客户报错,报错日志:
2025-03-08 19:40:27.770: RangeError: Value undefined out of range for undefined options property undefined
at Map.set (<anonymous>)
at AsyncHook.init (/opt/bonree/apm/agent/nodejs/7.1.0/Bonree/lib/models/async_trace.js:18:18)
at PromiseWrap.emitInitNative (internal/async_hooks.js:127:43)
at Promise.then (<anonymous>)
at next (/data/node_modules/co/index.js:100:51)
at onFulfilled (/data/node_modules/co/index.js:69:7)
at runMicrotasks (<anonymous>)
at processTicksAndRejections (internal/process/task_queues.js:97:5)
客户node版本:12.16.2
排查
根据报错信息看到探针代码:
一开始以为变量 self.stack 是个 undefined ,但它其实在初始化的时候已经是 map 对象了。
后来发现是 map 体积太大引起的。
https://github.com/nodejs/node/issues/37320
本地复现map体积过大:
node 限制了 map 体积不能超过 2^24 。
分析
node探针代码通过 async_hooks 模块实现了对异步函数的 hook,其 api 为:异步钩子 | Node.js v12.22.12 文档 — Async hooks | Node.js v12.22.12 Documentation
探针代码有在异步实例销毁时,把元素从 map 中移除。但不知客户应用里的异步实例一直没销毁,我们探针就没收到 destroy 事件。
后来发现,是 node 版本的问题,客户是 12.16.2 会有这个问题,在 12.22.12 则修复了这个问题。
github issue: Memory leak on Promise.all with async/await · Issue #34328 · nodejs/node
测试脚本:
const { createHook } = require("async_hooks");
const m = new Map();
createHook({
init(id) {
m.set(id, "val");
},
destroy(id) {
m.delete(id);
}
}).enable();
(async function () {
const a = new Array(10).fill(0)
for (let i = 0; i < 1e8; i++) {
await Promise.all(a.map(() => Promise.resolve()))
if (i % 1e5 === 0) {
console.log(Math.round(process.memoryUsage().heapUsed / 1024 / 1024), 'MB')
}
}
})()
运行结果
IT极限技术分享汇