Hello, I’m Shanyue.
The most common asynchronous resource listening scenarios in a Node application are:
- Full link log tracking, design each request for third-party services, databases, Redis to carry consistent traceId
- User information can be provided during exception capture, so that the abnormal system can find out which user has problems in a timely manner
The following figure shows zipkin’s full-link tracking based on traceId positioning:
directory
1. An incorrect example
2. Async_hooks and asynchronous resources
3. async_hooks.createHook
4. Debug and test async_hooks
Continuation Local Storage implementation
CLS - Hooked & Express/KOA middleware
7. Node V13 AsyncLocalStorage API
Nodule 8.
An error example
Let’s look at an example of an error in configuring user information in exception handling. Here’s the code
const session = new Map(a)// Middleware A
app.use((ctx, next) = > {
// Set user information
const userId = getUserId()
session.set('userId', userId)
await next()
})
// Middleware B
app.use((ctx, next) = > {
try {
await next()
} catch (e) {
const userId = session.get('userId')
// Report the userId to the exception monitoring system}})Copy the code
Because the session used at this time is asynchronous, user information is extremely easy to be overwritten by subsequent requests.
- User Shanyue enters middleware A. Session sets the user as Shanyue
- User Songfeng enters middleware B, and session sets user songfeng
- User Shanyue enters middleware B and obtains user Songfeng in session (something is wrong)
So how do you solve this problem?
Async_hooks and asynchronous resources
The official documentation describes async_hooks as follows: It is used to track asynchronous resources, that is, to listen for the lifetime of asynchronous resources.
The async_hooks module provides an API to track asynchronous resources.
Since it is used to track asynchronous resources, there are two ids in each asynchronous resource:
asyncId
: INDICATES the ID of the current life cycle of the asynchronous resourcetrigerAsyncId
: indicates the ID of the parent asynchronous resource, that isparentAsyncId
Call from the following API
const async_hooks = require('async_hooks');
const asyncId = async_hooks.executionAsyncId();
const trigerAsyncId = async_hooks.triggerAsyncId();
Copy the code
See the official documentation: Async_hooks API for more details
Now that we’re talking about async_hooks listening for asynchronous resources, what are those asynchronous resources? We often use the following in our daily projects:
Promise
setTimeout
fs
/net
/process
Etc based on the underlying API
However, async_hooks lists this much on the official website. In addition to the several mentioned above, console.log is also an asynchronous resource: 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
Copy the code
async_hooks.createHook
We can use asyncId to listen on an asynchronous resource. How can we listen on the creation and destruction of an asynchronous resource?
Create a hook using async_links. createHook.
const asyncHook = async_hooks.createHook({
// asyncId: indicates the Id of an asynchronous resource
// type: indicates the type of the asynchronous resource
// triggerAsyncId: parent asynchronous resource Id
init (asyncId, type, triggerAsyncId, resource) {},
before (asyncId) {},
after (asyncId) {},
destroy(asyncId){}})Copy the code
Let’s just focus on the four most important apis:
init
: Listens for the creation of an asynchronous resource. In this function, we can get the call chain of the asynchronous resource and also the type of the asynchronous resource, which are important.destory
: Listens for the destruction of asynchronous resources. Pay attention tosetTimeout
Can be destroyed, andPromise
Cannot destroy, CLS could leak here if implemented via async_hooks!before
: before the asynchronous resource callback startsafter
: after the asynchronous resource callback function is executed
Async_hooks debugging and testing
Is it important to debug tools and keep breaking points and Step In?
No, the debug method is console.log
However, if you debug async_hooks using console.log, problems arise because console.log is also an asynchronous resource: TickObject.
theconsole.log
Are there alternatives?
You can use the write system call to print characters to standard output (STDOUT), which has a file descriptor of 1. Thus, the importance of operating system knowledge for server-side development is self-evident.
Node calls the following API:
fs.writeSync(1.'hello, world')
Copy the code
What is a file descriptor?
The complete debugging code is as follows:
function log (. args) {
fs.writeSync(1, args.join(' ') + '\n')}Copy the code
Now that we are ready, let’s listen for the life cycle of setTimeout using asynC_hooks.
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(() = > {
// The after lifecycle begins with the callback function
log('Info'.'Async Before')
Promise.resolve(3).then(o= > log('Info', o))
// After lifecycle is at the end of the callback
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
Copy the code
Note: Promises have no destory life cycle, so beware of memory leaks. Also, if you use await promise, the promise will not have a before/after life cycle
From the above code, we can see the entire life cycle of setTimeout, and determine the call chain of asynchronous resources through asyncId and triterAsyncId.
setTimeout (2)
-> promise (3)
-> then (4)
Copy the code
Through the chain of the asynchronous resource, state data can be shared throughout the life cycle of the asynchronous resource. So this is the CLS.
Continuation Local Storage implementation
Continuation-local storage works like thread-local storage in threaded programming, but is based on chains of Node-style callbacks instead of threads.
CLS is a key-value pair store of shared data that exists throughout the life cycle of an asynchronous resource. One copy of data is maintained for the same asynchronous resource and will not be modified by other asynchronous resources.
Based on theasync_hooks
, you can design CLS for the server side. Currently in Node (>12.0.0),async_hooks
Can be used directly in production environment, I have almost all Node services connected basedasync_hooks
Implementation of CLS:cls-hooked
.
The two most popular implementations in the community are as follows:
- Node – continuation – local – storage: implementation of github.com/joyent/node…
- Cls-hooked: CLS using AsynWrap or async_hooks instead of async-listener for node 4.7+
Here is a minimalist example of reading and writing values for an asynchronous resource:
const createNamespace = require('cls-hooked').createNamespace
const session = createNamespace('shanyue case')
// will be applied to all asynchronous resource life cycles under this function
session.run(() = > {
session.set('a'.3)
setTimeout(() = > {
/ / get the value
session.get('a')},1000)})Copy the code
I myself have implemented a library similar to CLS functionality using async_hooks, see [CLS-session](github.com/shfshanyue/…
CLS – Hooked & Express/KOA Middleware
RequestId and userId are added to logger to make it easier to obtain Context information from Node asynchronous resources globally.
Here is an example of KOA middleware that stores userId using CLS-Hooked
function session (ctx, next) {
await session.runPromise(() = > {
/ / get requestId
const requestId = ctx.header['x-request-id'] || uuid()
const userId = await getUserIdByCtx()
ctx.res.setHeader('X-Request-ID', requestId)
// Set requestId/userId in CLS
session.set('requestId', requestId)
session.set('userId', userId)
return next()
})
}
Copy the code
Node V13 AsyncLocalStorage API
The ALS API was implemented in versions later than Node V13.10.0 and later migrated to the long-supported version V12.17.0. See document Asynchronous Context Tracking.
AsyncLocalStorage functions are similar to CLS, but the API is slightly different. Here is a minimalist example of reading and writing values:
const { AsyncLocalStorage } = require('async_hooks')
const asyncLocalStorage = new AsyncLocalStorage()
const store = { userId: 10086 }
// Set an asynchronous resource cycle Store
asyncLocalStorage.run(store, () = > {
/ / get the value
asyncLocalStorage.getStore()
})
Copy the code
The middleware for writing a KOA is shown below
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)
Copy the code
There’s a bigger problem facing ALS:
Can I use it in a production environment?
This is a tradeoff between performance and debugging, and if you can sacrifice a bit of performance for better performance monitoring and debugging in production, it’s definitely worth it.
Feat: Support asyncLocalStorage KoA plans to enable ALS.
Since Node V16.2, ALS performance has improved greatly thanks to the PromiseHook API in V8.
Whether it is enabled under the current Version of Node is a personal tradeoff.
summary
This article explains the usage scenario and implementation of asynchronous resource listening, which can be summarized as the following three points:
- CLS is asynchronous resource life-cycle based storage, implemented via asynC_hooks
- Promise no
destroy()
Life cycle, need to pay attention to memory leaks, if necessary withlru-cache
In combination with - open
async_hooks
After that, each asynchronous resource has an asyncId and trigerAsyncId, through which the asynchronous call relationship can be found - CLS Common Scenarios For exception monitoring and full-link log processing, based on
async_hooks
的cls-hooked
Implemented as CLS - in
node13.10
Then it was officially doneALS