Skip to content

https://nodejs.org/api/async_hooks.html

js
{
  AsyncLocalStorage: [class AsyncLocalStorage],
  createHook: [Function: createHook],
  executionAsyncId: [Function: executionAsyncId],
  triggerAsyncId: [Function: triggerAsyncId],
  executionAsyncResource: [Function: executionAsyncResource],
  asyncWrapProviders: [Object: null prototype] {
  },
  AsyncResource: [class AsyncResource]
}

上一章,简单介绍了 AsyncLocalStorage,本章一次介绍 createHookexecutionAsyncIdtriggerAsyncIdexecutionAsyncResource

1.createHook

createHook 方法,创建一个 AsyncHook 对象,用于在异步函数调用开始和结束之间,插入自定义的代码。

js
const asyncHooks = require('async_hooks')
const fs = require('fs')

function log(...args) {
  fs.writeSync(1, args.join(' ') + '\n')
}
log(asyncHooks.executionAsyncId())

asyncHooks.createHook({
  init(asyncId, type, triggerAsyncId, resource) {
    log('Init: ', `${type}(asyncId=${asyncId}, parentAsyncId: ${triggerAsyncId}), resource: ${resource}`)
  },
  before(asyncId) {
    log('Before: ', asyncId)
  },
  after(asyncId) {
    log('After: ', asyncId)
  },
  destroy(asyncId) {
    log('Destory: ', asyncId)
  }
}).enable()

setTimeout(() => {
  // after 生命周期在回调函数最前边
  log('Info', 'Async Before')
  Promise.resolve(3).then(o => log('Info', o))
  // after 生命周期在回调函数最后边
  log('Info', 'Async After')
})
//=> Output
// Init:  Timeout(asyncId=2, parentAsyncId: 1)
// Before:  2
// Info:  Async Before
// Init:  PROMISE(asyncId=3, parentAsyncId: 2)
// Init:  PROMISE(asyncId=4, parentAsyncId: 3)
// Info:  Async After
// After:  2
// Before:  4
// Info 3
// After:  4
// Destory:  2

2.executionAsyncId

executionAsyncId 方法,返回当前异步函数调用的 asyncId

js
const async_hooks = require('node:async_hooks')
const fs = require('node:fs')

function log(...args) {
  fs.writeSync(1, args.join(' ') + '\n')
}

log(async_hooks.executionAsyncId())  // 1

const path = '.'
fs.open(path, 'r', (err, fd) => {
  console.log(async_hooks.executionAsyncId())  // 2
})

3.triggerAsyncId

triggerAsyncId 方法,返回当前异步函数调用的父异步函数的 asyncId

js
const async_hooks = require('node:async_hooks')
const fs = require('node:fs')

function log(...args) {
  fs.writeSync(1, args.join(' ') + '\n')
}

log(async_hooks.triggerAsyncId())  // 0

const path = '.'
fs.open(path, 'r', (err, fd) => {
  console.log(async_hooks.triggerAsyncId())  // 1
})

4.executionAsyncResource

executionAsyncResource 方法,返回当前异步函数调用的父异步函数的 AsyncResource 对象。

js
const async_hooks = require('node:async_hooks')
const fs = require('node:fs')

function log(...args) {
  fs.writeSync(1, args.join(' ') + '\n')
}

log(async_hooks.executionAsyncResource())  // [object Object]

const timer = setTimeout(() => {
  log('timer', timer) // 2
  log(async_hooks.executionAsyncResource())  // 2
}, 0)

const path = '.'
fs.open(path, 'r', (err, fd) => {
  console.log(async_hooks.executionAsyncResource())  // FSReqCallback { oncomplete: [Function (anonymous)] }
})

5.CLS Issue

CLS 全称为 Continuation-local storage,即持久化本地存储

如下例中的同一个全局 session,如果 /a/b 两次请求存在时间差,那么第二次请求的 session 值会覆盖第一次请求的 session 值。

但实际上,我们想要 /a 请求中获取到的是 /a,而 /b 请求中获取到的是 /b

js
const http = require('node:http')
const asyncHooks = require('node:async_hooks')
const session = new Map()

const server = http.createServer(async (req, res) => {
  console.log(req.url, asyncHooks.executionAsyncId())
  const { url } = req
  session.set('url', url)
  if (url === '/a') {
    await new Promise((resolve) => {
      setTimeout(() => {
        resolve()
      }, 3000)
    })
    res.end(session.get('url'))
    return
  }
  res.end(session.get('url'))
})

server.listen(3000, () => {
  console.log('Server listening on port 3000')
})

创建一个 sh 脚本,用于发送请求 /a/b,并等待所有请求完成。来测试上面的脚本结果。

sh
#!/bin/bash

# 发送请求 A
curl http://127.0.0.1:3000/a &

# 等待 500 毫秒
sleep 0.5

# 发送请求 B
curl http://127.0.0.1:3000/b &

# 等待所有后台进程结束
wait

为了解决上面的问题,我们可以使用 AsyncLocalStorage

因为请求 ab 对应的 executionAsyncId 是不相同的。每个请求的 executionAsyncId 都是唯一的,原因如下:

  1. 异步上下文独立性 每个 HTTP 请求在 Node.js 中都会被处理为一个独立的异步操作。当请求 ab 到达时,Node.js 会为每个请求创建一个新的异步上下文。 executionAsyncId 是用于标识这些异步上下文的唯一 ID,因此每个请求的异步上下文都会有一个独立的 executionAsyncId
  2. 事件驱动模型 Node.js 依赖事件循环来处理 I/O 操作,包括 HTTP 请求。每当一个新的 HTTP 请求到达时,事件循环会将其放入队列并处理。在处理每个请求时,Node.js 会创建一个新的异步操作,赋予其一个新的 executionAsyncId。 由于 ab 是两个独立的 HTTP 请求,它们在事件循环中是两个独立的事件。因此,Node.js 为每个事件分配不同的 executionAsyncId
  3. 异步资源的唯一标识 executionAsyncId 主要用于跟踪和区分不同的异步操作。在 HTTP 服务器的上下文中,每个客户端请求(如 ab)都是一个独立的异步操作,并且每个操作有自己的资源(如套接字、回调函数)。 为了确保这些异步操作在跟踪和调试时可以被正确识别和区分,Node.js 使用 executionAsyncId 作为它们的唯一标识。
js
const asyncHooks = require('node:async_hooks')
const session = new Map()
const fs = require('node:fs')

function log(...args) {
  fs.writeSync(1, args.join(' ') + '\n')
}
function timeout (id) {
  session.set('a', id)
  setTimeout(() => {
    log('timeout', asyncHooks.executionAsyncId())
    const a = session.get('a')
    console.log(a)
  })
}
 
timeout(1)
timeout(2)
timeout(3)

另外也可以参考第三方开源项目:

cls-sessionnode-continuation-local-storage

js
const Session = require('cls-session')
const session = new Session()
 
function timeout (id) {
  session.scope(() => {
    session.set('a', id)
    setTimeout(() => {
      const a = session.get('a')
      console.log(a)
    })
  })
}
 
timeout(1)
timeout(2)
timeout(3)