當用戶報問題:線上某個功能使用報錯時,如何快速且準確地定位?當某個請求介面返回資料緩慢時,如何有效追蹤優化?
眾所周知,當一個請求到來時,大概會有以下日誌產生:
1、AceesLog:使用者存取日誌
2、Exception:程式碼異常日誌
3、SQL:sql查詢日誌
4、ThirdParty:第三方服務日誌
該如何追蹤一條請求產生的所有日誌?
一般做法是使用一個requestId來做唯一標識,
然後寫一個中間件,把requestId注入到context上下文中,當需要打日誌時,再從context中取出打印,
在第三方服務和SQL日誌中,還需要把requestId傳入到對應的函數裡面列印,這樣層層傳遞,實在太麻煩,程式碼侵入性也比較強。
我們的目標是降低程式碼侵入性,一次注入,自動追蹤。
經過研究,async_hooks可以追蹤異步行為的生命週期,在每個非同步資源(每個請求都是一個非同步資源)中,它都有2個ID,
分別是asyncId(非同步資源當前生命週期ID),trigerAsyncId(父級異步資源ID)。
async_hooks提供了以下生命週期鉤子來監聽非同步資源:
asyncHook = async_hook.createHook({ // 監聽非同步資源的建立init(asyncId,type,triggerAsyncId,resource){}, // 非同步資源回呼函數開始執行之前before(asyncId){}, // 非同步資源回呼函數開始執行後after(asyncId){}, // 監聽非同步資源的銷毀destroy(asyncId){} })
那如果我們做一個映射,每個asyncId映射一個storage,storage裡面再儲存對應的requestId,那requestId就可以很容易取得了。
正好cls-hooked這個函式庫已經基於async_hooks做好了封裝,在同一份非同步資源維護一份數據,以鍵值對的形式儲存。 (注意:async_hooked需要在高版本node>=8.2.1使用)當然社群中還有其他的實現,例如cls-session,node-continuation-local-storage等。
下面講下我把cls-hooked運用在我專案中的實例:
/session.js 建立命名儲存空間
const createNamespace = require('cls-hooked').createNamespace const session = createNamespace('requestId-store') module.exports = session
/logger.js 列印日誌
const session = require('./session') module.exports = { info: (message) => { const requestId = session.get('requestId') console.log(`requestId:${requestId}`, message) }, error: (message) => { const requestId = session.get('requestId') console.error(`requestId:${requestId}`, message) } }
/sequelize.js sql呼叫logger列印日誌
const logger = require("./logger") new Sequelize( logging: function (sql, costtime) { logger.error( `sql exe : ${sql} | costtime ${costtime} ms` ); } )
/app.js 設定requestId、設定requestId回傳回應頭、列印存取日誌
const session = require('./session') const logger = require('./logger') async function accessHandler(ctx, next) { const requestId = ctx.header['x-request-id'] || uuid() const params = ctx.request.body ? JSON.stringify(ctx.request.body) : JSON.stringify(ctx.request.query) // 設定requestId session.run(() => { session.set('requestId', requestId) logger.info(`url:${ctx.request.path};params:${params}`) next() // 設定回傳回應頭ctx.res.setHeader('X-Request-Id',requestId) }) }
我們看下當一條請求路徑是/home?a=1到來時的日誌:
存取日誌: requestId:79f422a6-6151-4bfd-93ca-3c6f892fb9ac url:/home;params:{"a":"1"} Sql日誌: requestId:79f422a6-6151-4bfd-93ca-3c6f892fb9ac sql exe : Executed (default): SELECT `id` FROM t_user
可以看到同一條請求整個連結的日誌requestId是一樣的。如果後面有告警發到告警平台,那麼我們根據requestId就可以找到這條請求執行的整個連結了。
細心的同學可能會觀察到我在介面回傳的回應頭裡面也設定了requestId,目的就是為了後續如果發現某條請求回應緩慢或有問題,那直接從瀏覽器就可以知道requestId,就可以做分析了。
我本地做了一下壓測,
這是記憶體的佔用比較:
比未使用async_hook多了約10%。
對於我們qps是百級的系統還好,但是如果是高併發的服務,可能要慎重考慮下了。
ps:有錯誤歡迎指出,不喜勿噴