火の狐 的个人博客

记录精彩的程序人生

Open Source, Open Mind,
Open Sight, Open Future!
  menu
292 文章
2704 浏览
3 当前访客
ღゝ◡╹)ノ❤️

Go-Zero 是如何追踪你的请求链路?

image.png

“ go-zero 是一个集成了各种工程实践的 web 和 rpc 框架。通过弹性设计保障了大并发服务端的稳定性,经受了充分的实战检验。”

序言

微服务架构中,调用链可能很漫长,从 httprpc ,又从 rpchttp 。而开发者想了解每个环节的调用情况及性能,最佳方案就是 全链路跟踪。

追踪的方法就是在一个请求开始时生成一个自己的 spanID ,随着整个请求链路传下去。我们则通过这个 spanID 查看整个链路的情况和性能问题。

下面来看看 go-zero 的链路实现。

代码结构

  • spancontext:保存链路的上下文信息「traceid,spanid,或者是其他想要传递的内容」
  • span:链路中的一个操作,存储时间和某些信息
  • propagator:trace 传播下游的操作「抽取,注入」
  • noop:实现了空的tracer 实现

概念

SpanContext

在介绍 span 之前,先引入 context 。SpanContext 保存了分布式追踪的上下文信息,包括 Trace id,Span id 以及其它需要传递到下游的内容。OpenTracing 的实现需要将 SpanContext 通过某种协议 进行传递,以将不同进程中的 Span 关联到同一个 Trace 上。对于 HTTP 请求来说,SpanContext 一般是采用 HTTP header 进行传递的。

下面是 go-zero 默认实现的 spanContext

type spanContext struct {
	traceId string // TraceID 表示tracer的全局唯一ID
	spanId  string // SpanId  表示单个trace中某一个span的唯一ID,在trace中唯一
}

同时开发者也可以实现 SpanContext 提供的接口方法,实现自己的上下文信息传递:

type SpanContext interface {
	TraceId() string                     // get TraceId
	SpanId() string                      // get SpanId
	Visit(fn func(key, val string) bool) // 自定义操作TraceId,SpanId
}

Span

一个 REST 调用或者数据库操作等,都可以作为一个 spanspan 是分布式追踪的最小跟踪单位,一个 Trace 由多段 Span 组成。追踪信息包含如下信息:

type Span struct {
	ctx           spanContext // 传递的上下文
	serviceName   string      // 服务名
	operationName string      // 操作
	startTime     time.Time   // 开始时间戳
	flag          string      // 标记开启trace是 server 还是 client
	children      int         // 本 span fork出来的 childsnums
}

span 的定义结构来看:在微服务中, 这就是一个完整的子调用过程,有调用开始 startTime ,有标记自己唯一属性的上下文结构 spanContext 以及 fork 的子节点数。

实例应用

go-zero 中 http,rpc 中已经作为内置中间件集成。我们以 http,rpc 中,看看 tracing 是怎么使用的:

HTTP

func TracingHandler(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		// **1**
		carrier, err := trace.Extract(trace.HttpFormat, r.Header)
		// ErrInvalidCarrier means no trace id was set in http header
		if err != nil && err != trace.ErrInvalidCarrier {
			logx.Error(err)
		}

		// **2**
		ctx, span := trace.StartServerSpan(r.Context(), carrier, sysx.Hostname(), r.RequestURI)
		defer span.Finish()
		// **5**
		r = r.WithContext(ctx)

		next.ServeHTTP(w, r)
	})
}

func StartServerSpan(ctx context.Context, carrier Carrier, serviceName, operationName string) (
	context.Context, tracespec.Trace) {
	span := newServerSpan(carrier, serviceName, operationName)
	// **4**
	return context.WithValue(ctx, tracespec.TracingKey, span), span
}

func newServerSpan(carrier Carrier, serviceName, operationName string) tracespec.Trace {
	// **3**
	traceId := stringx.TakeWithPriority(func() string {
		if carrier != nil {
			return carrier.Get(traceIdKey)
		}
		return ""
	}, func() string {
		return stringx.RandId()
	})
	spanId := stringx.TakeWithPriority(func() string {
		if carrier != nil {
			return carrier.Get(spanIdKey)
		}
		return ""
	}, func() string {
		return initSpanId
	})

	return &Span{
		ctx: spanContext{
			traceId: traceId,
			spanId:  spanId,
		},
		serviceName:   serviceName,
		operationName: operationName,
		startTime:     timex.Time(),
		// 标记为server
		flag: serverFlag,
	}
}
  1. 将 header -> carrier,获取 header 中的 traceId 等信息
  2. 开启一个新的 span,并把「traceId,spanId」封装在 context 中
  3. 从上述的 carrier「也就是 header」获取 traceId,spanId
    • 看 header 中是否设置
    • 如果没有设置,则随机生成返回
  4. request 中产生新的 ctx,并将相应的信息封装在 ctx 中,返回
  5. 从上述的 context,拷贝一份到当前的request

image.png

这样就实现了 span 的信息随着 request 传递到下游服务。

RPC

在 rpc 中存在 client, server ,所以从 tracing 上也有 clientTracing, serverTracingserveTracing 的逻辑基本与 http 的一致,来看看 clientTracing 是怎么使用的?

func TracingInterceptor(ctx context.Context, method string, req, reply interface{},
	cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error {
	// open clientSpan
	ctx, span := trace.StartClientSpan(ctx, cc.Target(), method)
	defer span.Finish()

	var pairs []string
	span.Visit(func(key, val string) bool {
		pairs = append(pairs, key, val)
		return true
	})
	// **3** 将 pair 中的data以map的形式加入 ctx
	ctx = metadata.AppendToOutgoingContext(ctx, pairs...)

	return invoker(ctx, method, req, reply, cc, opts...)
}

func StartClientSpan(ctx context.Context, serviceName, operationName string) (context.Context, tracespec.Trace) {
	// **1**
	if span, ok := ctx.Value(tracespec.TracingKey).(*Span); ok {
		// **2**
		return span.Fork(ctx, serviceName, operationName)
	}

	return ctx, emptyNoopSpan
}
  1. 获取上游带下来的 span 上下文信息
  2. 从获取的 span 中创建新的 ctx,span「继承父 span 的 traceId」
  3. 将生成 span 的 data 加入 ctx,传递到下一个中间件,流至下游

总结

go-zero 通过拦截请求获取链路 traceID,然后在中间件函数入口会分配一个根 Span,然后在后续操作中会分裂出子 Span,每个 span 都有自己的具体的标识,Finsh 之后就会汇集在链路追踪系统中。开发者可以通过 ELK 工具追踪 traceID ,看到整个调用链。

同时 go-zero 并没有提供整套 trace 链路方案,开发者可以封装 go-zero 已有的 span 结构,做自己的上报系统,接入 jaeger, zipkin 等链路追踪工具。

参考

  • go-zero trace
  • 开放分布式追踪(OpenTracing)入门与 Jaeger 实现

同时欢迎大家使用 go-zero 并加入我们,https://github.com/tal-tech/go-zero