使用OpenTracing跟蹤Go中的HTTP請求延遲

banq發表於2016-11-25
在Go 1.7,我們有一個新包/ HTTP / httptrace提供了一個方便的機制,觀察一個HTTP請求時會發生什麼。在本文中,將說明如何能在分散式跟蹤的情況下被使用,透過使用OpenTracing API跟蹤觀察一個客戶端和一個伺服器,並視覺化的結果顯示在Zipkin UI

什麼是分散式跟蹤和OpenTracing?
分散式跟蹤是監測和分析微服務架構系統,匯出結果到為X-TRACE,如谷歌的Dapper和Twitter的Zipkin 。 它們的底層原理是分散式環境傳播 ,其中涉及的某些後設資料與進入系統的每個請求相關聯,並且跨執行緒和程式邊界傳播後設資料跟隨請求進出各種微服務呼叫。 如果我們為每個入站請求分配一個唯一的ID並將其作為分散式上下文的一部分,那麼我們可以將來自多個執行緒和多個程式的各種效能分析資料合併到統一的表示我們系統執行請求的“跟蹤”中。

分散式跟蹤需要使用Hook鉤子和上下文傳播機制來測試應用程式程式碼(或其使用的框架)。 當我們在Uber開始構建我們的分散式跟蹤系統時,我們很快意識到,沒有良好的API為開發人員提供在程式語言之間內部一致性,那就無法繫結到指定的跟蹤系統。原來,不只是我們有這種思維,2015年10月一個新的社群形成,催生了OpenTracing API,一個開放的,廠商中立的,與語言無關的分散式跟蹤標準。你可以閱讀更多關於Ben Sigelman有關OpenTracing動機和設計原理背後的文章 。

讓我們看看程式碼:

import (
   "net/http"
   "net/http/httptrace"

   "github.com/opentracing/opentracing-go"
   "github.com/opentracing/opentracing-go/log"
   "golang.org/x/net/context"
)

// We will talk about this later
var tracer opentracing.Tracer

func AskGoogle(ctx context.Context) error {
   // retrieve current Span from Context
   var parentCtx opentracing.SpanContext
   parentSpan := opentracing.SpanFromContext(ctx); 
   if parentSpan != nil {
      parentCtx = parentSpan.Context()
   }

   // start a new Span to wrap HTTP request
   span := tracer.StartSpan(
      "ask google",
      opentracing.ChildOf(parentCtx),
   )

   // make sure the Span is finished once we're done
   defer span.Finish()

   // make the Span current in the context
   ctx = opentracing.ContextWithSpan(ctx, span)

   // now prepare the request
   req, err := http.NewRequest("GET", "http://google.com", nil)
   if err != nil {
      return err
   }

   // attach ClientTrace to the Context, and Context to request 
   trace := NewClientTrace(span)
   ctx = httptrace.WithClientTrace(ctx, trace)
   req = req.WithContext(ctx)

   // execute the request
   res, err := http.DefaultClient.Do(req)
   if err != nil {
      return err
   }
   
   // Google home page is not too exciting, so ignore the result
   res.Body.Close()
   return nil
}
<p class="indent">


這裡有幾點要注意:
1.迴避了tracer變數初始化的問題。

2.AskGoogle函式接受context.Context物件。這是為開發分散式應用程式的Go推薦方式,因為上下文物件是要讓分散式環境傳播。

3.我們假設上下文已經包含父跟蹤Span。 OpenTracing API中的Span是用於表示由微服務執行的工作單元。HTTP呼叫就是可以包裹在跟蹤Span中的操作案例。 當我們執行處理入站請求的服務時,服務通常會為每個請求建立一個跟蹤範圍,並將其儲存在上下文中,以便在對另一個服務進行下游呼叫時可用(在我們的示例中為google.com )。

4.我們啟動一個新的子Span來包裝出站HTTP呼叫。 如果父Span缺失,這是好方法。

5.最後,在做出HTTP請求之前,我們例項化一個ClientTrace並將其附加到請求。

ClientTrace結構是httptrace的基本構建塊 。它允許我們在HTTP請求的生命週期內註冊將由HTTP客戶端執行的回撥函式。 例如,ClientTrace結構有這樣的方法:

type ClientTrace struct {
...
        // DNSStart is called when a DNS lookup begins.
        DNSStart func(DNSStartInfo)
        // DNSDone is called when a DNS lookup ends.
        DNSDone func(DNSDoneInfo)
...
}
<p class="indent">


我們在NewClientTrace方法中建立這個結構的一個例項:

func NewClientTrace(span opentracing.Span) *httptrace.ClientTrace {
   trace := &clientTrace{span: span}
   return &httptrace.ClientTrace {
      DNSStart: trace.dnsStart,
      DNSDone:  trace.dnsDone,
   }
}

// clientTrace holds a reference to the Span and
// provides methods used as ClientTrace callbacks
type clientTrace struct {
   span opentracing.Span
}

func (h *clientTrace) dnsStart(info httptrace.DNSStartInfo) {
   h.span.LogKV(
      log.String("event", "DNS start"),
      log.Object("host", info.Host),
   )
}

func (h *clientTrace) dnsDone(httptrace.DNSDoneInfo) {
   h.span.LogKV(log.String("event", "DNS done"))
}

<p class="indent">


我們為DBBStart和DNSDone事件實現註冊兩個回撥函式,透過私有結構clientTrace保有一個指向跟蹤Span。 在回撥方法中,我們使用Span的鍵值記錄API來記錄事件的資訊,以及Span本身隱含捕獲的時間戳。

你不是說關於UI的東西嗎?
OpenTracing API的工作方式是,一旦呼叫跟蹤Span的Finish()方法,Span捕獲的資料將傳送到跟蹤系統後端,通常在後臺非同步傳送。然後,我們可以使用跟蹤系統UI來查詢跟蹤並在時間線上將其視覺化。 然而,上述例子只是為了說明使用OpenTracing與httptrace的原理。對於真正的工作示例,我們將使用現有的庫https://github.com/opentracing-contrib/go-stdlib 。 使用這個庫我們的客戶端程式碼不需要擔心跟蹤實際的HTTP呼叫。但是,我們仍然希望建立一個頂層跟蹤Span來表示客戶端應用程式的整體執行,並記錄任何錯誤。

package main

import (
   "fmt"
   "io/ioutil"
   "log"
   "net/http"

   "github.com/opentracing-contrib/go-stdlib/nethttp"
   "github.com/opentracing/opentracing-go"
   "github.com/opentracing/opentracing-go/ext"
   otlog "github.com/opentracing/opentracing-go/log"
   "golang.org/x/net/context"
)

func runClient(tracer opentracing.Tracer) {
   // nethttp.Transport from go-stdlib will do the tracing
   c := &http.Client{Transport: &nethttp.Transport{}}

   // create a top-level span to represent full work of the client
   span := tracer.StartSpan(client)
   span.SetTag(string(ext.Component), client)
   defer span.Finish()
   ctx := opentracing.ContextWithSpan(context.Background(), span)

   req, err := http.NewRequest(
      "GET",
      fmt.Sprintf("http://localhost:%s/", *serverPort),
      nil,
   )
   if err != nil {
      onError(span, err)
      return
   }

   req = req.WithContext(ctx)
   // wrap the request in nethttp.TraceRequest
   req, ht := nethttp.TraceRequest(tracer, req)
   defer ht.Finish()

   res, err := c.Do(req)
   if err != nil {
      onError(span, err)
      return
   }
   defer res.Body.Close()
   body, err := ioutil.ReadAll(res.Body)
   if err != nil {
      onError(span, err)
      return
   }
   fmt.Printf("Received result: %s\n", string(body))
}

func onError(span opentracing.Span, err error) {
   // handle errors by recording them in the span
   span.SetTag(string(ext.Error), true)
   span.LogKV(otlog.Error(err))
   log.Print(err)
}
<p class="indent">


上面的客戶端程式碼呼叫本地伺服器。 讓我們實現它。
package main

import (
   "fmt"
   "io"
   "log"
   "net/http"
   "time"

   "github.com/opentracing-contrib/go-stdlib/nethttp"
   "github.com/opentracing/opentracing-go"
)

func getTime(w http.ResponseWriter, r *http.Request) {
   log.Print("Received getTime request")
   t := time.Now()
   ts := t.Format("Mon Jan _2 15:04:05 2006")
   io.WriteString(w, fmt.Sprintf("The time is %s", ts))
}

func redirect(w http.ResponseWriter, r *http.Request) {
   http.Redirect(w, r,
      fmt.Sprintf("http://localhost:%s/gettime", *serverPort), 301)
}

func runServer(tracer opentracing.Tracer) {
   http.HandleFunc("/gettime", getTime)
   http.HandleFunc("/", redirect)
   log.Printf("Starting server on port %s", *serverPort)
   http.ListenAndServe(
      fmt.Sprintf(":%s", *serverPort),
      // use nethttp.Middleware to enable OpenTracing for server
      nethttp.Middleware(tracer, http.DefaultServeMux))
}
 


注意,客戶端向根端點“/”發出請求,但伺服器將其重定向到“/ gettime”端點。 這樣做允許我們更好地說明如何在跟蹤系統中捕獲跟蹤。



Tracing HTTP request latency in Go with OpenTracin

[該貼被banq於2016-11-25 09:00修改過]

相關文章