暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

微服务场景下请求链路追踪

lei子 2021-04-12
1884

1. 概述

在单体应用中,客户端请求服务端获得相应,如果出现异常,则很容易定位到问题所在,简单的加一些日志打印、xdebug等皆可完成问题的定位。
那么在当前并发高、微服务场景下,一个请求经过多个服务,如果链路中某个服务异常,问题的排查、定位则会变得异常困难,此时请求链路追踪则发挥较大的作用。
基于以上的场景、问题,引入本文主要要介绍的内容Tracing。

2. 什么是Distributed Tracing

Distributed Tracing提供了在复杂网络中展示、解析链路调用的方法、理论,最早出现在Google的Dapper论文中。在互联网中,一个trace即代表本次请求、事务或者流程在系统中执行的过程,从trace中也可以解析出如:请求链路、请求时间、响应时间以及其他数据,帮助开发者更快速的定位问题。
现在市场上也有诸如:Zipkin, Dapper, HTrace, X-Trace等链路追踪系统,但各个系统之间不兼容,开发人员若想整合一套跨语言、系统的分布式追踪系统还是比较困难的。

3. OpenTracing Api

OpenTracing Api 提供了一套标准的、独立的框架用来解决分布式链路跟踪的兼容问题。这就意味着,如果开发者想要尝试不同的分布式链路跟踪系统,则只需要替换掉Trace相关部分,或者部分配置即可。
主要特点:

  • 后台无关的一套接口

    • 只要实现了接口,即可实现应用跟踪

  • 标准化了对跟踪最小单位Span的管理

    • 定义了开始Span,结束Span和记录Span耗时的API。

  • 标准化了进程间跟踪数据传递的方式

    • 定义了一套API方便跟踪数据的传递

  • 标准化了进程内当前Span的管理 

    • 定义了存储和获取当前Span的API

分布式追踪的思维模型,大多数的链路追踪思维模型多来自Google的Dapper论文。OpenTracing也使用相似的语言描述

目前实现了整套Opentracing Api的系统有Jaeger(Uber出品)、Zipkin(Twitter出品) 等。

4. OpenTracing 实践

1. Jaeger安装

Jaeger 提供了用于展示、解析OpenTracing的一整套方案,为简单化安装,使用容器化方式进行安装,安装命令如下:

docker run -d --name jaeger -e COLLECTOR_ZIPKIN_HTTP_PORT=9411 \
-p 5775:5775/udp \
-p 6831:6831/udp \
-p 6832:6832/udp \
-p 5778:5778 \
-p 16686:16686 \
-p 14268:14268 \
-p 14250:14250 \
-p 9411:9411 \
jaegertracing/all-in-one:1.21

端口介绍

安装后,在浏览器中访问:http://localhost:16686 即可看到trace的相关信息,如 

在页面左侧选择Service即可根据业务设置查看Service下的trace信息


2. Go中链路追踪

OpenTracing链路追踪,理想情况下以CS架构较为合理,这样客户端、服务端的链路都能进行跟踪、分析,当然,如果只实现服务端,通过http等方式请求服务端接口,服务端返回时增加trace-id给到客户端,也可以方便定位、跟踪问题。 

1. Demo目录结构
.
├── Runtime
│ └── trace_20201213.log
├── client.go
├── go.mod
├── go.sum
├── server.go
└── trace
├── logger.go
└── trace.go

2. 实现Tracer

tracer的实现,以单例方式实现,便于在整个系统中统一使用,其定义如下:

package trace

import (
"github.com/opentracing/opentracing-go"
"github.com/uber/jaeger-client-go"
jaegerCfg "github.com/uber/jaeger-client-go/config"
"github.com/uber/jaeger-lib/metrics"
"io"
)

func InitTrace() io.Closer {
// Sample configuration for testing. Use constant sampling to sample every trace
// and enable LogSpan to log every span via configured Logger.
cfg := jaegerCfg.Configuration{
ServiceName: "TraceTest", // 定义服务名称
Reporter: &jaegerCfg.ReporterConfig{ // 定义trace上报服务器
LogSpans: true,
LocalAgentHostPort: "127.0.0.1:6831",
},
}
sampler, err := jaeger.NewProbabilisticSampler(0.01) // 定义采样率,按照指定的采样率进行采样, 如 万分之一
// relateSampler := jaeger.NewRateLimitingSampler(100) 定义固定每秒采样数,如每秒采样数:100
/* 自定义 数据上报,如上报到文件日志
report := &jaegercfg.ReporterConfig{
LogSpans: true,
}
_ , err := report.NewReporter("httpTestReporter", nil, &Logger{})
if err != nil {
fmt.Println("NewReport Error")
}
cfg.Reporter = report*/


// Example logger and metrics factory. Use github.com/uber/jaeger-client-go/log
// and github.com/uber/jaeger-lib/metrics respectively to bind to real logging and metrics
// frameworks.
jLogger := jaeger.StdLogger
jMetricsFactory := metrics.NullFactory

// Initialize tracer with a logger and a metrics factory
tracer, closer, err := cfg.NewTracer(
jaegerCfg.Logger(jLogger),
jaegerCfg.Metrics(jMetricsFactory),
jaegerCfg.Sampler(sampler),
)
if err != nil {
panic("Init Trace Falied With "+err.Error())
}
// Set the singleton opentracing.Tracer with the Jaeger tracer.
opentracing.SetGlobalTracer(tracer)

return closer
}

3. 自定义上报的Logger
package trace

import rLogger "github.com/reaburoa/utils/logger"

func init() {
rLogger.InitLogger("trace", "../Runtime", "json", 1, 10, 10, true, true, true)
}

// 实现jaeger 的logger接口
type Logger struct {
}

func (l *Logger) Error(msg string) {
rLogger.Sugar.Errorf("error %v", msg)
}

func (l *Logger) Infof(msg string, args ...interface{}) {
rLogger.Sugar.Infof(msg, args)
}

4. Client端OpenTracing的使用
package main

import (
"context"
"fmt"
"github.com/opentracing/opentracing-go"
"github.com/opentracing/opentracing-go/ext"
"github.com/opentracing/opentracing-go/log"
"io/ioutil"
"net/http"
"trace/trace"
)

func main() {
// 初始化OpenTracing
closer := trace.InitTrace()
defer closer.Close()

// 使用全局的Tracer
tracer := opentracing.GlobalTracer()

ctx := context.Background()
clientSpan, ctx := opentracing.StartSpanFromContext(ctx, "clientSpan") // 通过context生成Span
defer clientSpan.Finish()

url := "http://localhost:8082/publish"
req, _ := http.NewRequest("GET", url, nil)

// Set some tags on the clientSpan to annotate that it's the client span. The additional HTTP tags are useful for debugging purposes.
ext.SpanKindRPCClient.Set(clientSpan)
ext.HTTPUrl.Set(clientSpan, url)
ext.HTTPMethod.Set(clientSpan, "GET")

// Inject the client span context into the headers
spanCtx := clientSpan.Context()
// 注入trace信息
tracer.Inject(spanCtx, opentracing.HTTPHeaders, opentracing.HTTPHeadersCarrier(req.Header))
// 设置log信息,需要根据业务自行设定
clientSpan.LogFields(
log.String("event", "soft error"),
log.String("type", "cache timeout"),
log.Int("waited.millis", 1500),
)
clientSpan.SetTag("dddd", "ddd123")

resp, _ := http.DefaultClient.Do(req)
bb, err := ioutil.ReadAll(resp.Body)
fmt.Println("body", string(bb), err)

defer resp.Body.Close()
}

5. server端实现
package main

import (
"fmt"
"github.com/opentracing/opentracing-go"
"github.com/opentracing/opentracing-go/ext"
"log"
"net/http"

"trace/trace"
)

func main() {
closer := trace.InitTrace()
defer closer.Close()

http.HandleFunc("/publish", func(w http.ResponseWriter, r *http.Request) {
// Extract the context from the headers
tracer := opentracing.GlobalTracer() // 全局tracer
spanCtx, _ := tracer.Extract(opentracing.HTTPHeaders, opentracing.HTTPHeadersCarrier(r.Header)) // 从请求中提取span
serverSpan := tracer.StartSpan("server", ext.RPCServerOption(spanCtx)) // 开启span
defer serverSpan.Finish()

t := string([]rune(fmt.Sprint(serverSpan)[:16])) // 获取trace-id
fmt.Println("server span ", t)
w.Write([]byte("ok trace_id " + t))
})

log.Fatal(http.ListenAndServe(":8082", nil))
}

通过以上的设置整理,简单的OpenTracing使用基本上搭建完成,运行server.go开启服务端,接收客户端请求。

6. 运行分析

服务端日志输出: 

客户端日志输出:

 其中 44c43dabdc0256ff 即是本次请求的一个trace-id

那整个请求的链路到底是怎么样的呢?在网页中能有一个比较清晰的查看

从图中可以看到请求链路深度为 2,请求耗时2.05ms(非常短,都是本地的),同时看到client、server的一个调用层次关系。

打开client端的span,也可以看到我们程序中设置的各种日志、tag、时间等各数据 

打开server端的span,同样的看到server端的各数据 

7. 独立server端运行

如果只是浏览器或者Postman或者Ajax调用是否也可以做到链路跟踪分析呢?通过在浏览器或者Postman中调用服务端接口即可进行测试验证。

我们在浏览器请求接口,获得了服务端返回的teace-id信息,如图展示 

在刚才的jaeger的UI中也找到了本次请求的链路信息,如图展示 

以上实验也说明,只要服务端实现了OpenTracing的链路跟踪,客户端是否实现均可进行链路跟踪分析。

5. 回顾

通过进行安装以及Go中类库的使用,我们简单实现了OpenTracing的使用。在服务端中,将Opentracing放入到Context中,后续的各种函数调用等均可进行链路跟踪分析,在生产环境中同时设置一定的采样率,可以减少日志的数量,带来一定的成本降低。

在OpenTracing的使用中,还处于初级阶段,很多的功能还需要在进行深入挖掘。

参考资料

1. [OpenTracing](https://opentracing.io)

2. [Jaeger](https://www.jaegertracing.io)

3. [Jaeger安装](https://www.jaegertracing.io/docs/1.21/getting-started)

4. [opentracing-go](https://github.com/opentracing/opentracing-go)

5. [jaeger-client-go](https://github.com/jaegertracing/jaeger-client-go)


文章转载自lei子,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论