node.js - 并非所有资源都调用 Node async_hooks destroy 生命周期事件

标签 node.js async-hooks

我一直在探索 async_hooks API 来跟踪异步事件的状态。我发现 destroy 回调并不总是为每个相应的 init 回调调用。

这是一个简单的重现:

const asyncHooks = require("async_hooks");
const fs = require("fs");
const https = require("https");

asyncHooks
  .createHook({
    init(asyncId, type, triggerId) {
      fs.writeSync(1, `init ${asyncId} ${triggerId} ${type}\n`);
    },
    destroy(asyncId) {
      fs.writeSync(1, `destroy ${asyncId}\n`);
    },
    promiseResolve(asyncId) {
      fs.writeSync(1, `promiseResolve ${asyncId}\n`);
    }
  })
  .enable();

https.get("https://www.google.com", res => {
  console.log("status code - " + res.statusCode);
});

上面记录了发出简单 HTTP 请求时的所有 initdestroy 回调。

这是输出:

$ node bug.js
* init 5 1 TCPWRAP
* init 6 1 TLSWRAP
init 7 1 TickObject
* init 8 1 DNSCHANNEL
init 9 6 GETADDRINFOREQWRAP
init 10 1 TickObject
* init 11 10 HTTPPARSER
* init 12 10 HTTPPARSER
init 13 10 TickObject
init 14 5 TCPCONNECTWRAP
destroy 7
destroy 10
destroy 13
destroy 9
init 15 6 WRITEWRAP
destroy 14
status code - 200
init 16 12 TickObject
init 17 6 TickObject
init 18 6 TickObject
init 19 6 TickObject
init 20 6 TickObject
destroy 15
destroy 16
destroy 17
destroy 18
destroy 19
destroy 20
init 21 6 TickObject
init 22 6 TickObject
init 23 6 TickObject
init 24 6 TickObject
init 25 6 TickObject
init 26 6 TickObject
destroy 21
destroy 22
destroy 23
destroy 24
destroy 25
destroy 26
init 27 6 TickObject
init 28 6 TickObject
init 29 6 TickObject
destroy 27
destroy 28
destroy 29
init 30 6 TickObject
init 31 6 TickObject
init 32 6 TickObject
init 33 6 TickObject
init 34 6 TickObject
init 35 6 TickObject
init 36 6 TickObject
destroy 30
destroy 31
destroy 32
destroy 33
destroy 34
destroy 35
destroy 36
init 37 6 TickObject
init 38 6 TickObject
init 39 6 TickObject
destroy 37
destroy 38
destroy 39
init 40 6 TickObject
init 41 6 TickObject
destroy 40
destroy 41
init 42 6 TickObject
init 43 6 TickObject
init 44 6 TickObject
init 45 6 TickObject
destroy 42
destroy 43
destroy 44
destroy 45

我对上面的日志进行了注释,为每个没有相应 destroy 回调的 init 回调添加了星号 (*)。正如您所看到的,TCPWRAPTLSWRAPDNSCHANNELHTTPPARSER 回调类型似乎是有问题的类型。

我担心这种不对称会导致使用这种方法进行“连续本地存储”的各个 Node 模块中的内存泄漏,例如https://github.com/Jeff-Lewis/cls-hooked

最佳答案

将异步跟踪集成到Data-Forge Notebook后,我有两条建议可以帮助解决这个问题。 .

首先,您应该将想要启用异步 Hook 的代码包装在其自己的父异步资源中。将此视为一个异步上下文,它将您想要跟踪的代码与您不想跟踪的代码分开。

通过这种方式,您可以将异步跟踪隔离到仅真正需要它的代码。如果您这样做,那么您将故意忽略您不关心的其他代码中的异步操作,并且很可能是那些导致问题的异步操作,因此这样做会将它们排除在考虑范围之外。

这里有一些伪代码来解释我的意思:

const async_hooks = require("async_hooks");

function initAsyncTracking(trackedAsyncId) {

    const asyncHook = async_hooks.createHook({ // Initialise the async hooks API.
        init: (asyncId, type, triggerAsyncId, resource) => {
            // ... Add the async operation to your tracking if triggerAsyncId is equal to trackedAsyncId (the ID of our tracked parent async operation).
            // ... You also need to track the operation if triggerAsyncId is a child async operation of trackedAsyncId (you need to store enough information in your record to make this check).
        },
        destroy: asyncId => {
            // ... Remove the async operation if it was tracked ...
        },
        promiseResolve: asyncId => {
            // ... Remove the async operation if it was tracked ...
        },
    });

    asyncHook.enable(); // Enable tracking of async operations.
}

// ... code executed here (eg outside the async resource) isn't tracked.

const asyncTrackingResource = new async_hooks.AsyncResource("MY-ASYNC-RESOURCE"); // Create an async resource to be a parent of any async operations we want to track.
asyncTrackingResource.runInAsyncScope(() => {
    const trackedAsyncId = async_hooks.executionAsyncId(); // Get the id of the async resource we created.
    initAsyncTracking(trackedAsyncId );

    // ... code executed here (eg inside the async resource) will be tracked.

});

// ... code executed here (eg outside the async resource) isn't tracked.

我的第二条建议与 DNSCHANNEL 异步资源有关。我发现这个异步资源是由 Node.js 运行时库延迟创建并存储在全局变量中的。我能够通过 Node.js request 模块触发它的创建。所以这是一个系统异步资源,由您的代码间接创建并全局缓存(可能是为了性能?)。

这有点 hacky,但我发现如果我在异步跟踪代码之外强制创建全局 DNSCHANNEL 资源,那么它就不再是问题了

以下是预创建 DNSCHANNEL 异步资源的代码:

const { Resolver } = require("dns");

const hackWorkaround = new Resolver();

这有点难看,但它强制在我的异步跟踪代码运行之前创建它,因此 Node.js 似乎从未清理过这个特定资源,这并不是问题。

也可能是 Node.js 具有其他全局异步资源(例如此资源),这可能会给您带来问题。如果您发现更多请告诉我!

关于node.js - 并非所有资源都调用 Node async_hooks destroy 生命周期事件,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/54578654/

相关文章:

node.js - 如何为所有 Node 模块自动安装@types

facebook - Heroku - 使用 Node.js 创建了一个 facebook 应用程序,它给出了类型错误

javascript - 如何在express(nodejs)中的路由之间共享/存储数据?

javascript - 为什么我使用异步钩子(Hook) API 会导致 Node.js 异常终止?

node.js - 如何将 AsyncLocalStorage 用于 Observable?

node.js - 使用 async_hooks 跟踪上下文

node.js - 从容器内服务网格的网站

javascript - Socket.io 为所有客户端分配相同的 ID