Koa 請求日誌打點工具(三)—— OpenTracing + Jaeger

什麼是 OpenTracing ?

OpenTracing(opentracing.io)是一個分散式追蹤規範。OpenTracing 通過提供平台無關、廠商無關的 API,為分散式追蹤提供統一的概念和數據標準,使得開發人員能夠方便的添加(或更換)追蹤系統的實現。OpenTracing 定義了幾個術語:

  • Span:代表了系統中的一個邏輯工作單元,它具有操作名、操作開始時間以及持續時長。Span可能會有嵌套或排序,從而對因果關係建模。
    • Tags:每個 Span 可以有多個鍵值對(key:value)形式的 Tags,Tags 是沒有時間戳的,支持簡單的對 Span 進行註解和補充。
    • Logs:每個 Span 可以進行多次 Log 操作,每一次 Log 操作,都需要一個帶時間戳的時間名稱,以及可選的任意大小的存儲結構。
  • Trace:代表了系統中的一個數據/執行路徑(一個或多個 Span),可以理解成 Span 的有向無環圖。

OpenTracing 還有其他一些概念,這裡不過多解釋。我們看個實際的例子:

在一個分散式系統中,追蹤一個事務或者調用流一般如上圖所示。雖然這種圖對於看清各組件的組合關係是很有用的,但是,它不能很好顯示組件的調用時間,是串列調用還是並行調用,如果展現更複雜的調用關係,會更加複雜,甚至無法畫出這樣的圖。另外,這種圖也無法顯示調用間的時間間隔以及是否通過定時調用來啟動調用。一種更有效的展現一個典型的 trace 過程,如下圖所示:

這種展現方式增加顯示了執行時間的上下文,相關服務間的層次關係,進程或者任務的串列或並行調用關係。這樣的視圖有助於發現系統調用的關鍵路徑。通過關注關鍵路徑的執行過程,項目團隊可能專註於優化路徑中的關鍵位置,最大幅度的提升系統性能。例如:可以通過追蹤一個資源定位的調用情況,明確底層的調用情況,發現哪些操作有阻塞的情況。

什麼是 Jaeger ?

Jaeger 是 OpenTracing 的一個實現,是 Uber 開源的一個分散式追蹤系統,其靈感來源於Dapper 和 OpenZipkin,從 2016 年開始,該系統已經在 Uber 內部得到了廣泛的應用,它可以用於微服務架構應用的監控,特性包括分散式上下文傳播(Distributed context propagation)、分散式事務監控、根原因分析、服務依賴分析以及性能/延遲優化。該項目已經被雲原生計算基金會(Cloud Native Computing Foundation,CNCF)接納為第 12 個項目。

啟動 Jaeger + Jaeger UI

我們使用 Docker 啟動 Jaeger + Jaeger UI(Jaeger 可視化 web 控制台),命令如下:

$ docker run -d -p5775:5775/udp n -p 6831:6831/udp n -p 6832:6832/udp n -p 5778:5778 n -p 16686:16686 n -p 14268:14268 n jaegertracing/all-in-one:latestn

打開瀏覽器 localhost:16686/,如下所示:

現在並沒有任何數據,接下來我們看如何使用 Jaeger 接收並查詢日誌。

如何使用 OpenTracing + Jaeger ?

OpenTracing 和 Jaeger 分別提供了 JavaScript/Node.js 的 SDK:

  • opentracing/opentracing-javascript
  • jaegertracing/jaeger-client-node

opentracing 示例代碼如下:

const http = require(http);nconst opentracing = require(opentracing);nn// NOTE: the default OpenTracing tracer does not record any tracing information.n// Replace this line with the tracer implementation of your choice.nconst tracer = new opentracing.Tracer();nnconst span = tracer.startSpan(http_request);nconst opts = {n host : example.com,n method: GET,n port : 80,n path: /,n};nhttp.request(opts, res => {n res.setEncoding(utf8);n res.on(error, err => {n // assuming no retries, mark the span as failedn span.setTag(opentracing.Tags.ERROR, true);n span.log({event: error, error.object: err, message: err.message, stack: err.stack});n span.finish();n });n res.on(data, chunk => {n span.log({event: data_received, chunk_length: chunk.length});n });n res.on(end, () => {n span.log({event: request_end});n span.finish();n });n}).end();n

有 2 點需要解釋下:

  1. 需要將上面的 const tracer = new opentracing.Tracer(); 替換成自己的 tracer 實現,即 jaeger 的實現。
  2. 通過 .startSpan 啟動一個 Span,.setTag 設置 Tags,.log 設置 Logs,.finish 結束一個 Span。

是不是類似於我們的手動埋點。。只不過變成了一個規範而已。但 OpenTracing 不止如此,上面只是一個 Span 的用法,Span 之間還可以關聯調用關係,最後得到一個 DAG(有向無環圖)。

舉個例子:假如你正在搞微服務,多個服務之間有調用關係(不管是 HTTP 還是 RPC 等),每次調用服務內部可能產生多個 Span,最終會在 Jaeger 控制台頁面看到一個完整的 Trace 和 DAG 圖(微服務之間的調用關係)。

jaeger-client-node 使用如下:

const tracer = new jaeger.Tracer(n serviceName,n new jaeger.RemoteReporter(new UDPSender()),n new jaeger.RateLimitingSampler(1)n)n

創建一個 tracer 接收 3 個參數:

  1. serviceName:服務名
  2. Reporter:上報器,即往哪發日誌,如上例是通過 UDP 發送日誌,默認地址 localhost:6832
  3. Sampler:採樣器,即日誌如何採樣,如上例是限制 1 秒採樣一次

更多選項這裡不再詳細說明,可去查閱 jaeger-client-node 的文檔。

koa-await-breakpoint-jaeger

通過上面例子我們知道,使用 Jaeger 需要手動埋點,是不是很 low?沒關係,我們有 koa-await-breakpoint(koa-yield-breakpoint 姊妹版,即支持 async/await),koa-await-breakpoint-jaeger 是為 koa-await-breakpoint 寫的 store 的 adaptor,實現上有一些小技巧,這裡也不過多講解,有興趣的可以去看下源碼。

還是拿 koa-await-breakpoint 的 example 舉例,只添加了 2 行代碼引入 jaeger 的使用。代碼如下:

app.js

const path = require(path)nconst JaegerStore = require(koa-await-breakpoint-jaeger)nconst koaAwaitBreakpoint = require(koa-await-breakpoint)({n name: api,n files: [path.join(__dirname, **/*.js)],n store: new JaegerStore()n})nnconst Koa = require(koa)nconst Router = require(koa-router)nconst app = new Koa()nconst router = new Router()nnapp.use(koaAwaitBreakpoint)nnrouter.post(/users, require(./routes/users).createUser)nnapp.use(router.routes())napp.use(router.allowedMethods())nnapp.listen(3000, () => {n console.log(listening on 3000)n})n

routes/users.js

const Mongolass = require(mongolass)nconst mongolass = new Mongolass(mongodb://localhost:27017/test)nconst User = mongolass.model(User)nconst Post = mongolass.model(Post)nconst Comment = mongolass.model(Comment)nnexports.createUser = async function (ctx) {n const name = ctx.query.name || defaultn const age = +ctx.query.age || 18n await createUser(name, age)n ctx.status = 204n}nnasync function createUser (name, age) {n const user = (await User.create({n name,n agen })).ops[0]n await createPost(user)n}nnasync function createPost (user) {n const post = (await Post.create({n uid: user._id,n title: post,n content: postn })).ops[0]nn await createComment(user, post)n}nnasync function createComment (user, post) {n // throw new Error(test)n await Comment.create({n userId: user._id,n postId: post._id,n content: commentn })n}n

運行:

$ curl -XPOST localhost:3000/usersn

刷新 http://localhost:16686 可以看到已經有日誌了:

選擇 Sercice -> api,Operation -> POST /users,點擊 Find Traces 查看所有結果,右側展示了一條日誌,點進去如下所示:

Tips:可以根據 tags 過濾結果。

注意:Jaeger 是分散式追蹤系統嘛,通常用來追蹤多個服務之間的調用關係,這裡用來追蹤一個服務多個函數之間的調用關係。

將上述代碼 createComment 中的錯誤注釋解開,重新運行,如下所示:

完美的展現了一個請求到來時,函數之間的調用關係和層級關係以及耗時,甚至函數字元串和錯誤棧都有!

警告

目前沒有做性能測試,估計慘不忍睹,所以不要在生產環境使用。

參考文獻

  • Introduction · opentracing文檔中文版 ( 翻譯 ) 吳晟
  • [譯] OpenTracing語義規範
  • Uber正式開源其分散式跟蹤系統Jaeger

推薦閱讀:

Koa 請求日誌打點工具
為什麼Node的web端框架express和Koa的生態環境差距還是巨大(2017.11)?

TAG:koa | 日志 | 分布式系统 |