使用OpenTracing跟踪Go中的HTTP请求延迟

在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
}


这里有几点要注意:
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)
...
}


我们在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"))
}



我们为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)
}


上面的客户端代码调用本地服务器。 让我们实现它。

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修改过]

b]安装运行
在这篇文章中使用的所有源代码可在Github上 。 假设你有一个本地安装的Go 1.7,以及一个运行Docker,我们将用来运行Zipkin服务器。

该演示项目使用glide进行依赖管理,请先安装它。 例如,在Mac OS上,您可以:
$ brew install glide
然后获取演示代码,构建并运行它:


$ go get github.com/yurishkuro/opentracing-go-nethttp-demo
$ cd $GOPATH/src/github.com/yurishkuro/opentracing-go-nethttp-demo
$ glide install
# ... lots of output ...
$ go build .
$ ./opentracing-go-nethttp-demo
2016/10/06 00:10:54 Starting server on port 8000


现在在另一个终端,让我们开始Zipkin服务器


$ docker run -d -p 9410-9411:9410-9411 openzipkin/zipkin:1.12.0
Unable to find image 'openzipkin/zipkin:1.12.0' locally
1.12.0: Pulling from openzipkin/zipkin
4d06f2521e4f: Already exists
93bf0c6c4f8d: Already exists
a3ed95caeb02: Pull complete
3db054dce565: Pull complete
9cc214bea7a6: Pull complete
Digest: sha256:bf60e4b0ba064b3fe08951d5476bf08f38553322d6f640d657b1f798b6b87c40
Status: Downloaded newer image for openzipkin/zipkin:1.12.0
da9353ac890e0c0b492ff4f52ff13a0dd12826a0b861a67cb044f5764195e005


如果你没有Docker,另一种运行Zipkin服务器的方法是直接从jar开始:
$ wget -O zipkin.jar'https://search.maven.org/remote_content?g=io.zipkin.java&a=zipkin-server&v=LATEST&c=exec'
$ java -jar zipkin.jar

打开UI:
http://localhost:9411/

请注意,屏幕UI界面中服务名称下拉列表为空,因为我们还没有生成任何跟踪。

现在打开另一个终端并运行:
$ cd $GOPATH/src/github.com/yurishkuro/opentracing-go-nethttp-demo
$ ./opentracing-go-nethttp-demo -actor client
Received result: The time is Thu Oct 6 00:23:22 2016

如果您重新加载UI页面,您应该会在第一个下拉列表中看到“客户端”。


。。。。


补充,之前提到跟踪变量初始化问题,tracer是是OpenTracing API的实际实现。 在我的例子用了https://github.com/uber/jaeger-client-go ,这是来自Jaeger的开源项目,是Uber的分布式跟踪系统的与OpenTracing兼容的客户端库。我们计划在2016年年底之前开源后端和UI。这个例子中的主程序看起来像这样:


package main

import (
"flag"
"log"

"github.com/uber/jaeger-client-go"
"github.com/uber/jaeger-client-go/transport/zipkin"
)

var (
zipkinURL = flag.String(
"url",
"http://localhost:9411/api/v1/spans", "Zipkin server URL")
serverPort = flag.String(
"port", "8000", "server port")
actorKind = flag.String(
"actor", "server", "server or client")
)

const (
server =
"server"
client =
"client"
)

func main() {
flag.Parse()

if *actorKind != server && *actorKind != client {
log.Fatal(
"Please specify '-actor server' or '-actor client'")
}

// Jaeger tracer can be initialized with a transport that will
// report tracing Spans to a Zipkin backend
transport, err := zipkin.NewHTTPTransport(
*zipkinURL,
zipkin.HTTPBatchSize(1),
zipkin.HTTPLogger(jaeger.StdLogger),
)
if err != nil {
log.Fatalf(
"Cannot initialize HTTP transport: %v", err)
}
// create Jaeger tracer
tracer, closer := jaeger.NewTracer(
*actorKind,
jaeger.NewConstSampler(true),
// sample all traces
jaeger.NewRemoteReporter(transport, nil),
)

if *actorKind == server {
runServer(tracer)
return
}

runClient(tracer)

// Close the tracer to guarantee that all spans that could
// be still buffered in memory are sent to the tracing backend
closer.Close()
}

Jaeger跟踪系统本身不同于Zipkin,但是我们目前使用的是Zipkin兼容的Thrift数据模型,它允许我使用Zipkin服务器来显示跟踪。

结论
我们使用几个组件的组合来说明跨过进程边界HTTP请求的简单跟踪,并深入了解实际的HTTP请求执行:
1.标准的net/http/httptrace package,
2.https://github.com/opentracing-contrib/go-stdlib instrumentation library,
3.Jaeger Go client https://github.com/uber/jaeger-client-go that implements OpenTracing API, and
4.Zipkin server and UI from https://github.com/openzipkin/zipkin.

本案例演示源码:https://github.com/yurishkuro/opentracing-go-nethttp-demo.