问题描述
const httpContext = require('express-http-context');
async function t2() {
}
async function t1() {
for (let i = 0; i < 100000000; i++) {
httpContext.ns.run(t2);
}
}
t1();
运行:node --inspect --max-old-space-size=300 ns
问题:命名空间 _contexts 映射永远不会被清理。
在 cls-hooked/context.js 中有一个 destroy(id) 函数,但它从未被调用过。
我也尝试过 ns.bind、ns.runPromise(执行 ns.exit())和 ns.bind
如何在运行完成后删除上下文?
代码:
const httpContext = require('express-http-context');
function t2() {
}
async function t1() {
for (let i = 0; i < 100000000; i++) {
httpContext.ns.run(t2);
}
}
t1();
有效。
代码:
const httpContext = require('express-http-context');
async function t3() {
}
function t2() {
t3();
}
async function t1() {
for (let i = 0; i < 100000000; i++) {
httpContext.ns.run(t2);
}
}
t1();
再次出现内存泄漏。
cls 挂钩的 async_hook 方法 init() 将上下文添加到 _contexts 映射。 cls 挂钩的 async_hook 方法 destroy() 从 _contexts 映射中删除上下文。
问题是从来没有调用destroy。
这是 cls-hooked 中的错误还是与当前的 async_hooks 不兼容?
解决方法
正如向 OP 指出的那样,用法肯定是不正确的。
OP 应该只执行一次 ns.run()
,并且 run
中的所有内容都将具有相同的上下文。
看看这个正确用法的例子:
var createNamespace = require('cls-hooked').createNamespace;
var writer = createNamespace('writer');
writer.run(function () {
writer.set('value',0);
requestHandler();
});
function requestHandler() {
writer.run(function(outer) {
// writer.get('value') returns 0
// outer.value is 0
writer.set('value',1);
// writer.get('value') returns 1
// outer.value is 1
process.nextTick(function() {
// writer.get('value') returns 1
// outer.value is 1
writer.run(function(inner) {
// writer.get('value') returns 1
// outer.value is 1
// inner.value is 1
writer.set('value',2);
// writer.get('value') returns 2
// outer.value is 1
// inner.value is 2
});
});
});
setTimeout(function() {
// runs with the default context,because nested contexts have ended
console.log(writer.get('value')); // prints 0
},1000);
}
此外,cls-hooked
中的实现确实表明上下文是通过异步钩子回调 destroy(asyncId)
destroy(asyncID)
在 asyncId
对应的资源销毁后调用。它也被嵌入器 API 异步调用。某些 resources
依赖垃圾收集进行清理,因此如果对传递给 init
的资源对象进行了引用,则可能永远不会调用 destroy
,从而导致内存泄漏应用。如果资源不依赖于垃圾收集,那么这将不是问题。
https://github.com/Jeff-Lewis/cls-hooked/blob/0ff594bf6b2edd6fb046b10b67363c3213e4726c/context.js#L416-L425
这是我的存储库,通过使用 autocannon
用大量请求轰炸服务器来比较和测试内存使用情况
https://github.com/Darkripper214/AsyncMemoryTest
根据测试,heap
的利用率几乎没有增加(正如预期的那样,因为我们正在处理 HTTP 请求)。
CLS-Hooked 和 Async-Hook 的内存使用
目的
存储库是一个微型测试,用于查看使用 cls-hooked
和 async-hook
在 Node.js
内传递上下文时如何利用内存。
用法
-
npm run start
用于 CLS 挂钩服务器或npm run async
用于异步挂钩服务器 -
转到 Chrome 并粘贴
chrome://inspect
-
点击
inspect
访问服务器的开发工具
-
转到
memory
选项卡,您可以在用请求轰炸服务器之前、之中和之后拍摄快照并检查heap
-
node benchmark.js
开始用请求轰炸服务器。这是由autocannon
提供支持,您可能需要增加connections
或duration
以查看差异。
结果
CLS 挂钩
统计 | 1% | 2.5% | 50% | 97.5% | 平均 | 标准 | 最大 |
---|---|---|---|---|---|---|---|
请求/秒 | 839 | 839 | 871 | 897 | 870.74 | 14.23 | 839 |
字节/秒 | 237kB | 237kB | 246kB | 253kB | 246kB | 4.01kB | 237kB |
每秒采样一次的请求/字节计数(请注意,这是在附加调试器的情况下运行的,每秒性能会受到影响)
15.05 秒内 13k 个请求,3.68 MB 读取
异步钩子
统计 | 1% | 2.5% | 50% | 97.5% | 平均 | 标准 | 最大 |
---|---|---|---|---|---|---|---|
请求/秒 | 300 | 300 | 347 | 400 | 346.4 | 31.35 | 300 |
字节/秒 | 84.6kB | 84.6kB | 97.9kB | 113kB | 97.7kB | 8.84kB | 84.6kB |
每秒采样一次的请求/字节计数(请注意,这是在附加调试器的情况下运行的,并且有大量 debug()
消息显示它是如何被破坏的,每秒性能会受到影响)
15.15 秒内 5k 个请求,读取 1.47 MB
编辑 1
OP 抱怨每次执行 store
时设置的 _context
的长度。如前所述,OP 的测试方式不正确,因为它在循环中运行。
OP 所抱怨的场景只会在 namespace.run()
执行一些包含或包含 namespace.run()
的回调时发生。
async function
那么为什么_context 没有被清除?这是因为 async function t3() {} // This async function will cause _context length to not be cleared
function t2() {
t3();
}
function t1() {
for (let i = 0; i < 500; i++) {
session.run(t2);
}
}
t1();
将无法在 node.js async function t3
中运行,因为 event loop
持续运行,因此项目几乎无限附加到 {{1} }.
为了证明这是由于这种行为,我更新了 repo 以包含一个文件 synchronous for loop
,该文件可以使用 _context
运行,该文件在两者之间显式运行垃圾收集,以及垃圾集合不会影响 cls-gc.js
的长度。
npm run gc
和 _context
的执行过程中 _context
的长度会很长,因为两者都是同步的。但是,在调用 t1()
回调之后,t2()
的长度大约是正确的。请使用调试器进行检查。
_context
的长度将在 setTimeout
_context