Node 中 AsyncLocalStorage 与异步资源状态共享

2023-06-18 0 815

他们好,我是山月。

在两个 Node 应用领域中,触发器天然资源窃听采用情景最少的地方性是:

全信道式笔记跟踪,结构设计每天允诺的服务项目器端服务项目、资料库、Redis随身携带完全一致的 traceId

极度抓取时可提供更多采用者重要信息,将能在极度控制系统及时处置哪一位采用者再次出现了难题

右图为 zipkin 依照 traceId 功能定位的全信道跟踪:

Node 中 AsyncLocalStorage 与异步资源状态共享zipkin 全信道跟踪

「产品目录」

1. 两个严重错误实例

2. async_hooks 与触发器天然资源

3. async_hooks.createHook

4. async_hooks 增容及试验

5. Continuation Local Storage 同时实现

6. cls-hooked 与 express/koa 中间件

7. node v13 后的 AsyncLocalStorage API

8. 展毛

1. 两个严重错误实例

他们上看两个在极度处置中实用性采用者重要信息的「严重错误实例」,下列为标识符

const session = new Map

()

// 开发工具 Aapp.use((ctx, next) =>

{

// 设置采用者重要信息 const

userId = getUserId()

session.set(userId

, userId)

await

next()

})

// 开发工具 Bapp.use((ctx, next) =>

{

try

{

await

next()

} catch

(e) {

const userId = session.get(userId

)

// 把 userId 上报给极度监控控制系统

}

})

「由于此时采用的 session 是触发器的,采用者重要信息极其容易被随后而来的允诺而覆盖。」

采用者山月进入开发工具 A,session 设置采用者为 山月

采用者松风进入开发工具 B,session 设置采用者为 松风

那如何解决这种难题?

2. async_hooks 与触发器天然资源

官方文档如此描述 async_hooks: 它被用来跟踪触发器天然资源,也就是窃听触发器天然资源的生命周期。

The async_hooks module provides an API to track asynchronous resources.

既然它被用来跟踪触发器天然资源,则在每个触发器天然资源中,都有两个 ID:

asyncId: 触发器天然资源当前生命周期的 ID

trigerAsyncId: 可理解为父级触发器天然资源的 ID,即 parentAsyncId

通过下列 API 调取

const async_hooks = require(async_hooks

);

const

asyncId = async_hooks.executionAsyncId();

const

trigerAsyncId = async_hooks.triggerAsyncId();

更多详情参考官方文档: async_hooks API

标题:async_hooks APINode 中 AsyncLocalStorage 与异步资源状态共享      

既然谈到了 async_hooks 用以窃听触发器天然资源,那会有那些触发器天然资源呢?他们日常项目中经常用到的也无非下列集中:

Promise

setTimeout

fs/net/process 等基于底层的API

然而,在官网中 async_hooks 列出的竟有如此之多。除了上述提到的几个,连 console.log 也属于触发器天然资源: TickObject。

FSEVENTWRAP,

FSREQCALLBACK, GETADDRINFOREQWRAP, GETNAMEINFOREQWRAP, HTTPINCOMINGMESSAGE,

HTTPCLIENTREQUEST,

JSSTREAM, PIPECONNECTWRAP, PIPEWRAP, PROCESSWRAP, QUERYWRAP,

SHUTDOWNWRAP,

SIGNALWRAP, STATWATCHER, TCPCONNECTWRAP, TCPSERVERWRAP, TCPWRAP,

TTYWRAP,

UDPSENDWRAP, UDPWRAP, WRITEWRAP, ZLIB, SSLCONNECTION, PBKDF2REQUEST,

RANDOMBYTESREQUEST,

TLSWRAP, Microtask, Timeout, Immediate, TickObject

3. async_hooks.createHook

他们可以通过 asyncId 来窃听某一触发器天然资源,那如何窃听到该触发器天然资源的创建及销毁呢?

答案是通过 async_hooks.createHook 创建两个钩子,API 及释义见标识符:

const

asyncHook = async_hooks.createHook({

// asyncId: 触发器天然资源Id // type: 触发器天然资源类型 // triggerAsyncId: 父级触发器天然资源 Id

init (asyncId, type, triggerAsyncId, resource) {},

before (asyncId) {},

after (asyncId) {},

destroy(asyncId)

{}

})

最重要的四个 API:

destory: 窃听触发器天然资源的销毁。要注意 setTimeout 可以销毁,而 Promise 无法销毁,如果通过 async_hooks 同时实现 CLS 可能会在这里造成内存泄漏!

before: 触发器天然资源回调函数开始执行前

after: 触发器天然资源回调函数执行后

4. async_hooks 增容及试验

增容大法最重要的是增容工具,并且不停地打断点与 Step In 吗?

不,增容大法是 console.log

但如果增容 async_hooks 时采用 console.log 就会再次出现难题,因为 console.log 也属于触发器天然资源: TickObject。

「那 console.log 有没有替代品呢?」

此时可利用 write 控制系统调用,用它向标准输出(STDOUT)中打印字符,而标准输出的文件描述符是 1。由此也可见,操作控制系统知识对于服务项目端开发的重要性不言而喻。

node 中调用 API 如下:

fs.writeSync(1, hello, world

)

什么是文件描述符 (file descriptor)

标题:什么是文件描述符 (file descriptor)Node 中 AsyncLocalStorage 与异步资源状态共享      

完整的增容标识符如下:

function log (…args)

{

fs.writeSync(1, args.join() + \n

)

}

准备工作就绪,下列他们通过 async_hooks 来窃听 setTimeout 这个触发器天然资源的生命周期。

const asyncHooks = require(async_hooks

)

const fs = require(fs

)

function log(…args)

{

fs.writeSync(1, args.join() + \n

)

}

asyncHooks.createHook({

init(asyncId, type, triggerAsyncId, resource)

{

log(Init: , `${type}(asyncId=${asyncId}, parentAsyncId:${triggerAsyncId})`

)

},

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

注意: Promise 无 destory 的生命周期,要注意由此造成的内存泄漏。另外,如果采用 await promise,Promise 也不会有 before/after 的生命周期

从以上标识符,可以看出整个 setTimeout 的生命周期,「并通过 asyncId 与 triterAsyncId 确定触发器天然资源的调用链条」

setTimeout (2)

-> promise (3)

-> then

(4)

通过该触发器天然资源的链条,可以同时实现在整个触发器天然资源生命周期内的状况数据共享资源。也就是下列的 CLS。

5. Continuation Local Storage 同时实现

Continuation-local storage works like thread-local storage in threaded programming, but is based on chains of Node-style callbacks instead of threads.

CLS 是存是触发器资源生命周期共享资源数据的两个键值对存储,对于在同一触发器天然资源中将会维护一份数据,而不会被其它触发器天然资源所修改。

「基于 async_hooks,可以结构设计出适用于服务项目端的 CLS。目前 Node (>12.0.0) 中,async_hooks 可直接采用在生产环境,我已将几乎所有的 Node 服务项目接入了基于 async_hooks 同时实现的 CLS: cls-hooked。」

社区中最流行的两种同时实现如下:

node-continuation-local-storage: implementation of https://github.com/joyent/node/issues/5243

标题:node-continuation-local-storageNode 中 AsyncLocalStorage 与异步资源状态共享      

cls-hooked: CLS using AsynWrap or async_hooks instead of async-listener for node 4.7+

标题:cls-hookedNode 中 AsyncLocalStorage 与异步资源状态共享      

下列是关于触发器天然资源读写值的最简实例:

const createNamespace = require(cls-hooked

).createNamespace

const session = createNamespace(shanyue case

)

// 将作用于该函数下的所有触发器天然资源生命周期session.run(() =>

{

session.set(a, 3

)

setTimeout(() =>

{

session.get(a

)

}, 1000

)

})

我自己也采用 async_hooks 也同时实现了两个类似 CLS 功能的库,可参考 [cls-session](https://github.com/shfshanyue/cls-session]

6. cls-hooked 与 express/koa 开发工具

为了在 Node 中

下列是利用 cls-hooked 存储 userId 的 koa 开发工具实例

function session (ctx, next)

{

await session.runPromise(() =>

{

const requestId = ctx.header[x-request-id

] || uuid()

const userId = await

getUserIdByCtx()

ctx.res.setHeader(X-Request-ID

, requestId)

// CLS 中设置 requestId/userId session.set(requestId

, requestId)

session.set(userId

, userId)

return

next()

})

}

7.node v13 后的 AsyncLocalStorage API

由于 CLS 的呼声实在过高,呼吁官方同时实现类似 API,于是 ALS 就在 node v13.10.0 之后的版本同时实现了,并随后把该 API 迁移到了长期支持版本 v12.17.0,详见文档 Asynchronous Context Tracking

标题:详见文档 Asynchronous Context TrackingNode 中 AsyncLocalStorage 与异步资源状态共享      

AsyncLocalStorage 与 CLS 功能类似,但是 API 有微弱的差别。下列是关于读写值的最简实例:

const { AsyncLocalStorage } = require(async_hooks

)

const asyncLocalStorage = new

AsyncLocalStorage()

const store = { userId: 10086

}

// 设置两个触发器天然资源周期的 StoreasyncLocalStorage.run(store, () =>

{

asyncLocalStorage.getStore()

})

写两个 koa 的开发工具如下所示

const{ AsyncLocalStorage } =require(async_hooks

)

const asyncLocalStorage = new

AsyncLocalStorage()

async function session (ctx, next)

{

const requestId = ctx.header[x-request-id

] || uuid()

const userId = await

getUserId()

const

context = { requestId, userId }

await asyncLocalStorage.run(context, () =>

{

return

next()

})

}

app.use(session)

对于 ALS 而言,有两个更大的难题将要面对:

我可以在生产环境中采用它吗?

目前,koa 将计划支持开启 ALS 特性,feat: support asyncLocalStorage

标题:feat: support asyncLocalStorageNode 中 AsyncLocalStorage 与异步资源状态共享      

在 Node v16.2 之后,ALS 得益于 v8 中 PromiseHook API,性能已经得到了很大的改善。

至于,在当前的 Node 版本下是否开启,那要看个人权衡了。

8. 展毛

本篇文章讲解了触发器天然资源窃听的采用情景及同时实现方式,可总结为下列三点:

CLS 是基于触发器天然资源生命周期的存储,可通过 async_hooks 同时实现

Promise 无 destroy() 生命周期,需要注意内存泄漏,必要时可与 lru-cache 结合

开启 async_hooks 后,每两个触发器天然资源都有两个 asyncId 与 trigerAsyncId,通过二者可查知触发器调用关系

CLS 常用情景在极度监控及全信道式笔记处置中,目前可以采用基于 async_hooks 的 cls-hooked 作为 CLS 同时实现

在 node13.10 之后官方同时实现了 ALS

9.

欢迎扫码添加山月的微信,备注进群,加入山月的前端面试交流群。

Node 中 AsyncLocalStorage 与异步资源状态共享

相关文章

发表评论
暂无评论
官方客服团队

为您解决烦忧 - 24小时在线 专业服务