Javascript与CLS(Continuation-local Storage)

前言

最近在作公司serverless相关需求的时候,须要封装调用链上报的组件,在传入traceId和userId等上下文信息时,须要从框架层逐层往下传递,好比打印一个log,须要这样:javascript

// 基于koa的某个工具包内部
log.info('这是一个log', req)
复制代码

全部须要上下文的地方都须要传入,致使代码严重耦合,咱们有什么办法能够优雅的解决这个问题呢?html

一个简单的http server

const http = require('http');

//create a server object:
http.createServer(function (req, res) {
    logicA(req, res)
}).listen(8080); //the server object listens on port 8080

function logicA(req, res) {
    logicA1(req, res)
}
// 无所不在req, res
function logicA1(req, res) {
    res.write('Hello World!'); //write a response to the client
    res.end(); //end the response 
}
复制代码

无所不在的req res的透传前端

其余语言是怎么处理的呢?java

以java为例,java中有一个功能叫局部线程存储(Thread-local Storage)例如在某些网络模型中好比当一个请求来的时候(本人对java了解很少,不详细展开),程序会在线程池里分配一个线程去处理这个请求,在这个线程中有局部变量是当前请求线程内共享的,线程内都能访问的。node

Continuation-local Storage与TLS相似,不过是基于Nodejs风格的回调调用。它得名于函数式编程中的Continuation-passing style,旨在链式函数调用过程当中维护一个持久的数据。git

从浏览器入手

直接讲Node可能有的同窗不理解,咱们能够从浏览器举例。Node web server的一次请求,其实也是一个事件,能够类比浏览器的一次点击事件。在浏览器端,咱们处理复杂逻辑的时候,可能会遇到如下的代码github

<html>
<header></header>
<body>
<button id="button" />
<script> button.addEventListener('click', event => { logicA(event) // 其余处理逻辑 }) function logicA(event) { logicA1(event) // 其余处理逻辑 } // logicA1 maybe in other module function logicA1(event) { console.log(`x: ${event.x}, y: ${event.y}`) } 复制代码

效果以下web

demo
有同窗可能会想,既然 event无处不在,那我放在全局变量上不就行了?代码以下

const gloabContext = {}
button.addEventListener('click', event => {
    gloabContext.event = event
    logicA()
    // 其余处理逻辑
})
function logicA() {
    logicA1()
    // 其余处理逻辑
}
// logicA1 maybe in other module
function logicA1() {
    console.log(`x: ${gloabContext.event.x}, y: ${gloabContext.event.y}`)
}

复制代码

globalContext和咱们透传的event一致,看起来好像也没啥问题编程

其实问题很大segmentfault

这个例子之因此没问题,是由于咱们的逻辑函数是同步的,若是咱们加入异步逻辑会发生什么事呢?

const gloabContext = {}
button.addEventListener('click', event => {
    gloabContext.event = event
    logicA(event)
    // 其余处理逻辑
})
async function logicA(event) {
    await delay(3000)
    logicA1(event)
    // 其余处理逻辑
}
// logicA1 maybe in other module
function logicA1(event) {
    console.log(`gloabContext x: ${gloabContext.event.x}, y: ${gloabContext.event.y}`)
    console.log(`event x: ${event.x}, y: ${event.y}`)
    console.log('\n')
}
async function delay(timeout) {
    return new Promise(resolve => {
        setTimeout(() => {
            resolve(true)
        }, timeout);
    })
}
复制代码

效果以下:

demo3
能够发现 globalContext中的值已经和咱们指望保存的 event不一致了。 咱们须要的是一个能够关联异步操做的数据,任意的异步操做能够访问这些数据,却互不影响。

浏览器的解决方案Zone.js

Zone.js 是Angular团队在Angular2中引入的,google团队Zones能够帮助开发者作到如下的事情:

  • 把一些数据关联到 zone 中,相似于某些语言中的本地线程存储(thread local storage),这样在 zone 中的任意异步操做均可以访问这些数据。

  • 自动追踪指定 zone 还未执行完的异步任务,以便执行相似清理、渲染或者测试断言等。

  • 分析发生在当前 zone 中异步执行的总时间,用于分析工做。

  • 处理 zone 中全部未捕获的异常或者未处理的 promise reject,阻断他们往上层冒泡。

废话少说,先看用Zone.js如何解决咱们刚才的问题

button.addEventListener('click', event => {
    Zone.current.fork({
        name: 'clickZone',
        properties: {
            event
        }
    }).run(
        () => logicA(event)
    )
    // 其余处理逻辑
})
function logicA(event) {
    delay(3000).then(() => {
        logicA1(event)
    })
    // 其余处理逻辑 
}
// logicA1 maybe in other module
function logicA1(event) {
    console.log(Zone.current.name)
    console.log(`gloabContext x: ${Zone.current.get('event').x}, y: ${Zone.current.get('event').y}`)
    console.log(`event x: ${event.x}, y: ${event.y}`)
    console.log('\n')
}
function delay(timeout) {
    return new Promise(resolve => {
        setTimeout(() => {
            resolve(true)
        }, timeout);
    })
}
复制代码

效果以下:

Amazing!

Zone.current.get('event')与咱们传递的event一致了!并且Zone.js还封装了Nodejs相关的API,咱们在服务端也能使用。

Zone.js还提供大量的钩子,有更多强大的用法,好比用来追踪未完成的异步宏任务和微任务,能够参考这篇文章翻阅源码后,我终于理解了Zone.js

咱们的问题解决了吗?

不幸的是,没有。

细心的同窗已经发现,我在使用Zone.js的时候并无使用async函数,咱们试试改为async函数后会发生什么。代码以下

button.addEventListener('click', event => {
    Zone.current.fork({
        name: 'clickZone',
        properties: {
            event
        }}).run( () => logicA(event))
    // 其余处理逻辑
})
async function logicA(event) {
    await delay(3000)
    logicA1(event)
    // 其余处理逻辑
}
// logicA1 maybe in other module
function logicA1() {
    console.log(Zone.current.name)
    console.log(`gloabContext x: ${Zone.current.get('event').x}, y: ${Zone.current.get('event').y}`)
    console.log(`event x: ${event.x}, y: ${event.y}`)
    console.log('n')
}
async function delay(timeout) {
    return new Promise(resolve => {
        setTimeout(() => {
            resolve(true)
        }, timeout);
    })
}
复制代码

报错了,Zone.current.name也不是咱们的clickZone,变成了<root>了,怎么回事呢?

这和Zone.js的实现有关

Zone.js采用猴子补丁(Monkey-patched)的暴力方式将Javascript中的异步函数都包装了一层,在浏览器中,包括RequestAnimationFrame,addEventListener,XMLHttpRequest等一系列异步方法。在Nodejs里,全部的异步api也被封装了一层(google工程师真暴力 = =!;今天的主角不是Zone.js,也就不展开了,有兴趣的同窗能够去查看源码)。

固然,这里被改写的对象也包括咱们的Promise对象。

未引入Zone.js前,咱们打印Promise显示以下:

引入Zone.js后,打印以下:

能够看到window.Promise对象已经被修改了,系统原生Promise被放在了__zone_symbol__Promise

那为何咱们用Pormise.then的方式调用Zone.js是正常的,用async函数的形式不行呢?

这又扯到了v8对async函数的实现,在v8中 async函数返回的结果是一个promise,若是 return 的不是一个promise,也会封装成一个promise对象,效果以下:

那么,引入Zone.js后呢,发生了什么

async函数返回的promise包装对象不是全局Promise的实例,而是native的!

更确凿的证据在这里

能够这么理解v8的行为:

async 函数的返回值若是不是native的promise,则v8会将其封装成native的promise,而与js中全局的Promise对象无关。

这么一来,Zone.js中的Monkey-patched就失效了。(其实关于async await还有一些有意思的东西,若是后面有时间会写一篇文章讲讲这里)

这个Issues里面也提到了,因为v8的实现机制,致使zone.js没法支持async await语法,只有使用babel或者ts将async await编译成generator的形式。其实angular团队也早将Zones for JavasSript提到TC39 process,可是至今是stage 0的状态,感受但愿渺茫。

对于前端来讲用babel还能解释为兼容浏览器版本,但对于Node应用来讲,编译async函数增长了调试的复杂度,那还有什么解决办法吗?

Nodejs的解决方案

Domain模块

domain模块早在node v0.8版本的时候就发布了。这个模块最先是用于捕捉异步回调中出现的异常,在腾讯开源的TSW中使用了domain来实现保存请求上下文:

经过process.domain始终指向当前执行栈所在的domain以及Object.defineProperty,实现了全局变量保存执行上下文

domain如今已被node官方标识为Stability: 0 - Deprecated(废弃的)状态,如今咱们去看domain模块的源码能够发现,该模块已经用async_hooks重写了,意味着即便最后从node api中移除,咱们经过async_hooks也能本身实现domain。

Async_hooks模块

在node8.0版本以后引入了async_hooks模块,该模块的状态是Stability: 1 - Experimental(实验性的),而且在github上有对async_hooks使用的性能问题的讨论,在基于koa框架下,性能损失在10%左右。除了性能损失,还有部分使用者出现了cpu暴涨的状况,这里由于信息有限,没法得知是否和使用者自身的编码有关。

除了async_hooks模块有性能损失,domain模块在基于async_hooks重写前自身也存在大约15%的性能损失。

阿里的Nodejs应用管理器 Pandora.js 就是用的async_hooks来作链路追踪的,其源码里依赖的cls-hooked包就是基于async_hooks模块实现。

Pandora.js源码,能够看到使用了cls-hooked

这里不对async_hooks模块的使用作过多展开,感兴趣的同窗去看看api就知道了。

结论

Zone.js: 支持浏览器,Nodejs,没法直接使用async await语法,须要编译。

Domain模块:支持Nodejs,已废弃,已用async_hooks实现。

Async_hooks模块: 支持Nodejs,存在性能损耗,可能存在内存泄漏,cpu暴涨的问题。

为了性能安全,咱们能够增长一个开关,在必要时候关闭async_hooksdomain的功能,同时作到不影响业务主流程。


写完下班,最后祝你们多拿年终奖。

参考

NodeJS async_hooks API与CLS

NodeJS与ThreadLocal

Zone for NodeJS API

用正确的方式为NodeJS打日志

angular with tsconfig target ES2017 async/await will not work with zone.js

剑走偏锋!domain模块竟然还能这样用!

翻阅源码后,我终于理解了Zone.js

zone.js —— 暴力之美

Node.js 异常捕获的一些实践

JavaScript异步机制详解

V8中更快的异步函数和promise

相关文章
相关标签/搜索