5

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 请求时的所有init和回调。destroy

这是输出:

$ 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

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

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

4

1 回答 1

1

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

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

这样,您可以将异步跟踪隔离到真正需要它的代码。如果您这样做,那么您将故意忽略来自您不关心的其他代码的异步操作,并且很可能是那些导致您的问题的异步操作,因此这样做会将它们从考虑中删除。

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

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.jsrequest模块触发它的创建。所以这是一个系统异步资源,它由您的代码间接创建并全局缓存(也许是为了性能?)。

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

这是预先创建 DNSCHANNEL 异步资源的代码:

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

const hackWorkaround = new Resolver();

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

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

于 2019-11-10T09:18:07.560 回答