cls-hook 内存泄漏或错误使用?

Dav*_*ard 3 node.js express cls-hooked

以下代码会增加内存使用量直至崩溃:

const httpContext = require('express-http-context');
async function t2() {
}

async function t1() {
  for (let i = 0; i < 100000000; i++) {
    httpContext.ns.run(t2);
  }
}
t1();
Run Code Online (Sandbox Code Playgroud)

运行它: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();
Run Code Online (Sandbox Code Playgroud)

作品。

代码:

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();
Run Code Online (Sandbox Code Playgroud)

又出现内存泄漏了。

cls-hook async_hook 方法 init() 将上下文添加到 _contexts 映射中。cls-hook async_hook 方法 destroy() 从 _contexts 映射中删除上下文。

问题是 destroy 永远不会被调用。

这是 cls-hooks 中的错误还是与当前 async_hooks 不兼容?

PKi*_*ong 5

正如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);
}
Run Code Online (Sandbox Code Playgroud)

此外,内部的实现cls-hooked确实表明上下文是通过异步钩子回调销毁的destroy(asyncId)

destroy(asyncID)在对应的资源被销毁后调用asyncId。它也可以从嵌入器 API emitDestroy() 异步调用。有些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

用法

  1. npm run start对于 CLS-hook 服务器 npm run asyncAsync-hook 服务器

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

  3. 点击inspect进入服务器的开发工具

开发工具

  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钩子

cls钩图

异步钩子

统计数据 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()消息来显示它如何store是如何被破坏的,每秒的性能将受到影响)

15.15 秒内 5k 请求,读取 1.47 MB

异步

异步图

编辑1

OP 抱怨_context每次设置的长度namespace.run()执行 a 时设置的长度。正如前面强调的,OP 测试的方式不正确,因为它是在循环上运行的。

OP 抱怨的情况仅在namespace.run()执行某些回调或包含async function.

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();
Run Code Online (Sandbox Code Playgroud)

那么为什么_context没有被清除呢?这是因为async function t3无法在 Node.js 中运行,event loop因为它synchronous for loop正在连续运行,因此几乎无限地将项目附加到_context.

因此,为了证明这是由于这种行为造成的,我更新了存储库以包含一个cls-gc.js可以使用 运行的文件npm run gc,该文件在其间显式运行垃圾收集,并且垃圾收集不会影响_context

_context执行期间的长度会很长t1()t2()因为两者都是同步的。然而, 的长度大约_context是在setTimeout调用回调之后。请使用调试器来检查这一点。

的长度_context可在session

// process.env.DEBUG_CLS_HOOKED = true;
('use strict');
let createNamespace = require('cls-hooked').createNamespace;
let session = createNamespace('benchmark');

async function t3() {}
function t2() {
  t3();
}
function t1() {
  for (let i = 0; i < 500; i++) {
    session.run(t2);
    try {
      if (global.gc) {
        global.gc();
        console.log('garbage collection ran');
      }
    } catch (e) {
      console.log('`node --expose-gc index.js`');
      process.exit();
    }
  }
}
t1();

function t5() {
  for (let i = 0; i < 1000; i++) {
    // Check the _context here, should have length of at least 500
    session.run(t2);
    try {
      if (global.gc) {
        global.gc();
        console.log('garbage collection ran');
      }
    } catch (e) {
      console.log('`node --expose-gc index.js`');
      process.exit();
    }
  }
}
t5();

setTimeout(() => {
  console.log('here');
  // Check the _context here, length should be 0
  session.run(t2);
}, 3000);
Run Code Online (Sandbox Code Playgroud)