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

相關文章
相關標籤/搜索