前言
最近在做公司serverless相关需求的时候,需要封装调用链上报的组件,在传入traceId和userId等上下文信息时,需要从框架层逐层往下传递,比如打印一个log,需要这样:
// 基于koa的某个工具包内部
log.info('这是一个log', req)
所有需要上下文的地方都需要传入,导致代码严重耦合,我们有什么办法可以优雅的解决这个问题呢?
一个简单的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中有一个功能叫局部线程存储(Thread-local Storage)例如在某些网络模型中比如当一个请求来的时候(本人对java了解不多,不详细展开),程序会在线程池里分配一个线程去处理这个请求,在这个线程中有局部变量是当前请求线程内共享的,线程内都能访问的。
Continuation-local Storage与TLS类似,不过是基于Nodejs风格的回调调用。它得名于函数式编程中的Continuation-passing style,旨在链式函数调用过程中维护一个持久的数据。
从浏览器入手
直接讲Node可能有的同学不理解,我们可以从浏览器举例。Node web server的一次请求,其实也是一个事件,可以类比浏览器的一次点击事件。在浏览器端,我们处理复杂逻辑的时候,可能会遇到以下的代码
<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}`)
}
效果如下
有同学可能会想,既然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一致,看起来好像也没啥问题
其实问题很大
这个例子之所以没问题,是因为我们的逻辑函数是同步的,如果我们加入异步逻辑会发生什么事呢?
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);
})
}
效果如下:
可以发现 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_hooks
或 domain
的功能,同时做到不影响业务主流程。
写完下班,最后祝大家多拿年终奖。
参考
angular with tsconfig target ES2017 async/await will not work with zone.js
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。