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.

  1. User Shanyue enters middleware A. Session sets the user as Shanyue
  2. User Songfeng enters middleware B, and session sets user songfeng
  3. 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 resource
  • trigerAsyncId: 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/processEtc 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 tosetTimeoutCan be destroyed, andPromiseCannot destroy, CLS could leak here if implemented via async_hooks!
  • before: before the asynchronous resource callback starts
  • after: 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.logAre 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_hooksCan be used directly in production environment, I have almost all Node services connected basedasync_hooksImplementation 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:

  1. CLS is asynchronous resource life-cycle based storage, implemented via asynC_hooks
  2. Promise nodestroy()Life cycle, need to pay attention to memory leaks, if necessary withlru-cacheIn combination with
  3. openasync_hooksAfter that, each asynchronous resource has an asyncId and trigerAsyncId, through which the asynchronous call relationship can be found
  4. CLS Common Scenarios For exception monitoring and full-link log processing, based onasync_hookscls-hookedImplemented as CLS
  5. innode13.10Then it was officially doneALS