cls 挂钩或错误使用中的内存泄漏? CLS 挂钩异步钩子

问题描述

以下代码增加内存使用直到崩溃:

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-hookedasync-hookNode.js 内传递上下文时如何利用内存。

用法

  1. npm run start 用于 CLS 挂钩服务器 npm run async 用于异步挂钩服务器

  2. 转到 Chrome 并粘贴 chrome://inspect

  3. 点击inspect访问服务器的开发工具

devtools

  1. 转到memory选项卡,您可以在用请求轰炸服务器之前、之中和之后拍摄快照并检查heap

  2. node benchmark.js 开始用请求轰炸服务器。这是由 autocannon 提供支持,您可能需要增加 connectionsduration 以查看差异。

结果

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 读取

cls-hook

cls-hooked graph

异步钩子

统计 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​​

async

async-graph

编辑 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