0x00 前言
前一篇文章 微服务基础之 链路追踪(OpenTracing),介绍了 OpenTracing 的理论,本文基于 gRPC 与 Zipkin && Jaeger 来实现 Tracing 的应用。
0x01 回顾 OpenTracing 数据模型
一个 Tracer 包含了若干个 Span,Span 是追踪链路中的基本组成元素,一个 Span 表示一个独立的工作单元,在链路追踪中可以表示一个接口的调用,一个数据库操作的调用等等。
- Span:调用链路的基本单元,使用 spanId 作为唯一标识;每个服务的每次调用都对应一个 Span,在其中记录服务名称、时间等基本信息
- Trace:表示一个调用链路,由若干 Span 组成,使用 traceId 作为唯一标识,对应一次完整的服务请求
一个 Span 中包含如下内容:
- 服务名称 (operation name,必选)
- 服务开始时间(必选)
- 服务的结束时间(必选)
- Tags:K/V 形式
- Logs:K/V 形式
- SpanContext
- Refrences:该 span 对一个或多个 span 的引用(通过引用 SpanContext)
详细说明下上面的字段:
1、Tags
每个 Span 可以有多个键值 K/V 对形式的 Tags,Tags 是没有时间戳的,支持简单的对 Span 进行注解和补充。Tags 是一个 K/V 类型的键值对,用户可以自定义该标签并保存。主要用于链路追踪结果对查询过滤。如某 Span 是调用 Redis ,而可以设置 redis
的标签,这样通过搜索 redis
关键字,可以查询出所有相关的 Span 以及 trace;又如 http.method="GET",http.status_code=200
,其中 key 值必须为字符串,value 必须是字符串,布尔型或者数值型。Span 中的 Tag 仅自己可见,不会随着 SpanContext 传递给后续 Span。
span.SetTag("http.method","GET")
span.SetTag("http.status_code",200)
2、Logs
Logs 也是一个 K/V 类型的键值对,与 Tags 不同的是,Logs 还会记录写入 Logs 的时间,因此 Logs 主要用于记录某些事件发生的时间。
span.LogFields(
log.String("database","mysql"),
log.Int("used_time":5),
log.Int("start_ts":1596335100),
)
PS:Opentracing 给出了一些惯用的 Tags 和 Logs,链接
3、SpanContext(核心字段)
每个 Span 必须提供方法访问 SpanContext,SpanContext 代表跨越进程边界(在不同的 Span 中传递信息),传递到下级 Span 的状态。SpanContext 携带着一些用于跨服务通信的(跨进程)数据,主要包含:
- 该 Span 的唯一标识信息,如:
span_id
、trace_id
、parent_id
或sampled
等 - Baggage Items,为整条追踪连保存跨服务(跨进程)的 K/V 格式的用户自定义数据
4、Baggage Items
Baggage Items 与 Tags 类似,也是 K/V 键值对。与 tags 不同的是:Baggage Items 的 Key 和 Value 都只能是 string 格式,Baggage items 不仅当前 Span 可见,其会随着 SpanContext 传递给后续所有的子 Span。要小心谨慎的使用 Baggage Items:因为在所有的 Span 中传递这些 Key/Value 会带来不小的网络和 CPU 开销。Baggage 是存储在 SpanContext 中的一个键值对集合。它会在一条追踪链路上的所有 Span 内全局传输,包含这些 Span 对应的 SpanContexts
5、References(引用关系)
Opentracing 定义了两种引用关系: ChildOf 和 FollowFrom,分别来看:
- ChildOf: 父 Span 的执行依赖子 Span 的执行结果时,此时子 Span 对父 Span 的引用关系是 ChildOf。比如对于一次 RPC 调用,服务端的 Span(子 Span)与客户端调用的 Span(父 Span)是 ChildOf 关系。
- FollowFrom:父 Span 的执不依赖子 Span 执行结果时,此时子 Span 对父 Span 的引用关系是 FollowFrom。FollowFrom 常用于异步调用的表示,例如消息队列中 Consumerspan 与 Producerspan 之间的关系。
6、Trace
Trace 表示一次完整的追踪链路,trace 由一个或多个 Span 组成。它表示从头到尾的一个请求的调用链,它的标识符是 traceID。 下图示例表示了一个由 8
个 Span 组成的 trace:
[Span A] ←←←(the root span)
|
+------+------+
| |
[Span B] [Span C] ←←←(Span C is a `ChildOf` Span A)
| |
[Span D] +---+-------+
| |
[Span E] [Span F] >>> [Span G] >>> [Span H]
↑
↑
↑
(Span G `FollowsFrom` Span F)
以时间轴的展现方式如下:
––|–––––––|–––––––|–––––––|–––––––|–––––––|–––––––|–––––––|–> time
[Span A···················································]
[Span B··············································]
[Span D··········································]
[Span C········································]
[Span E·······] [Span F··] [Span G··] [Span H··]
跟踪上下文
此外,跟踪上下文(Trace Context)也是很重要的场景,它定义了传播跟踪所需的所有信息,例如 traceID,parent-SpanId 等。OpenTracing 提供了两个处理跟踪上下文(Trace Context)的方法:
Inject(SpanContext,format,carrier)
:Inject 将跟踪上下文放入媒介,来保证跟踪链的连续性,常用于客户端Extract(format.Carrier)
:一般从媒介(通常是 HTTP 头)获取跟踪上下文,常用于服务端
OpenTracing API 的路径
在 OpenTracing API 中,有三个主要对象:
- Tracer
- Span
- SpanContext Tracer 可以创建 Spans 并了解如何跨流程边界对它们的元数据进行 Inject(序列化)和 Extract(反序列化),通常的有如下流程:
- 开始一个新的 Span
- Inject 一个 SpanContext 到一个载体
- 从载体 Extract 一个 SpanContext
- 由起点进程创建一个 Tracer,然后启动进程发起请求,每个动作产生一个 Span,如果有父子关系,Tracer 将它们关联
- 当请求 / Span 完成后,Tracer 将跟踪信息推送到 Collector
0x02 ZipKin-Tracing 的一般流程
Zipkin 是一款开源的分布式实时数据追踪系统(Distributed Tracking System),由 Twitter 公司开发和贡献。其主要功能是聚合来自各个异构系统的实时监控数据。在链路追踪 Tracing Analysis 中,可以通过 Zipkin 上报 Golang 应用数据。
使用 Zipkin 上报数据的流程如下图所示:
使用的 package:
下面介绍通过 Zipkin 将 Golang 应用数据上报至链路追踪控制台的方法: 1、创建 Tracer,Tracer 对象可以用来创建 Span 对象(记录分布式操作时间)。Tracer 对象还配置了上报数据的网关地址、本机 IP、采样频率等数据,您可以通过调整采样率来减少因上报数据产生的开销。
func getTracer(serviceName string, ip string) *zipkin.Tracer {
// create a reporter to be used by the tracer
reporter := httpreporter.NewReporter("http://tracing-analysis-dc-hz.aliyuncs.com/adapt_aokcdqnxyz@123456ff_abcdef123@abcdef123/api/v2/spans")
// set-up the local endpoint for our service
endpoint, _ := zipkin.NewEndpoint(serviceName, ip)
// set-up our sampling strategy 设置采样率
sampler := zipkin.NewModuloSampler(1)
// initialize the tracer
tracer, _ := zipkin.NewTracer(
reporter,
zipkin.WithLocalEndpoint(endpoint),
zipkin.WithSampler(sampler),
)
return tracer;
}
2、记录请求数据,下面代码用于记录请求的根操作:
// tracer can now be used to create spans.
span := tracer.StartSpan("some_operation")
// ... do some work ...
// span 完成,必须调用 finish
span.Finish()
// Output:
如果需要记录请求的上一步和下一步操作,则需要传入上下文。如下代码所示,childSpan
为 span
的孩子节点:
childSpan := tracer.StartSpan("some_operation2", zipkin.Parent(span.Context()))
// ... do some work ...
childSpan.Finish()
3、可选:(为了快速定位问题)可以为某个记录添加一些自定义标签(Tags),例如记录是否发生错误、请求的返回值等:
childSpan.Tag("http.status_code", statusCode)
4、在分布式系统中发送 RPC 请求时会带上 Tracing 数据,包括 TraceId、ParentSpanId、SpanId、Sampled 等。可以在 HTTP 请求中使用 Extract/Inject 方法在 HTTP Request Headers 上透传数据。即 在 Client 端执行 `Inject`,在 Server 端执行 `Extract`, 目前 Zipkin 已有组件支持以 HTTP、gRPC 这两种 RPC 协议透传 Context 信息。总体数据流程如下:
Client Span Server Span
┌──────────────────┐ ┌──────────────────┐
│ │ │ │
│ TraceContext │ Http Request Headers │ TraceContext │
│ ┌──────────────┐ │ ┌───────────────────┐ │ ┌──────────────┐ │
│ │ TraceId │ │ │ X-B3-TraceId │ │ │ TraceId │ │
│ │ │ │ │ │ │ │ │ │
│ │ ParentSpanId │ │ Inject │ X-B3-ParentSpanId │Extract │ │ ParentSpanId │ │
│ │ ├─┼─────────>│ ├────────┼>│ │ │
│ │ SpanId │ │ │ X-B3-SpanId │ │ │ SpanId │ │
│ │ │ │ │ │ │ │ │ │
│ │ Sampled │ │ │ X-B3-Sampled │ │ │ Sampled │ │
│ └──────────────┘ │ └───────────────────┘ │ └──────────────┘ │
│ │ │ │
└──────────────────┘ └──────────────────┘
这里以 HTTP 为例,在客户端调用 Inject
方法传入 Context
信息:
req, _ := http.NewRequest("GET", "/", nil)
// configure a function that injects a trace context into a reques
injector := b3.InjectHTTP(req)
injector(sp.Context())
在服务端调用 Extract
方法解析 Context
信息:
req, _ := http.NewRequest("GET", "/", nil)
b3.InjectHTTP(req)(sp.Context())
b.ResetTimer()
_ = b3.ExtractHTTP(copyRequest(req))
0x03 Jaeger-Tracing 的一般流程
本小节介绍使用 Jaeger 来实现链路追踪。Jaeger 是 Uber 开源的分布式追踪系统,也是遵循 Opentracing 的系统之一。
1、Jaeger 提供了 all-in-one 镜像,方便我们快速开始测试:
// 通过 http://localhost:16686 可以打开 Jaeger UI
$ 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 9411:9411 \
jaegertracing/all-in-one:1.14
2、初始化 Jaeger tracer,设置 endpoint / 采样率等信息
import (
"context"
"errors"
"fmt"
"io"
"time"
"github.com/opentracing/opentracing-go"
"github.com/opentracing/opentracing-go/log"
"github.com/uber/jaeger-client-go"
jaegercfg "github.com/uber/jaeger-client-go/config"
)
// initJaeger 将 jaeger tracer 设置为全局 tracer
func initJaeger(service string) io.Closer {
cfg := jaegercfg.Configuration{
// 将采样频率设置为 1,每一个 span 都记录,方便查看测试结果
Sampler: &jaegercfg.SamplerConfig{
Type: jaeger.SamplerTypeConst,
Param: 1,
},
Reporter: &jaegercfg.ReporterConfig{
LogSpans: true,
// 将 span 发往 jaeger-collector 的服务地址
CollectorEndpoint: "http://localhost:14268/api/traces",
},
}
closer, err := cfg.InitGlobalTracer(service, jaegercfg.Logger(jaeger.StdLogger))
if err != nil {
panic(fmt.Sprintf("ERROR: cannot init Jaeger: %v\n", err))
}
return closer
}
3、创建 tracer,生成 Root Span,下面这段代码创建了一个 Root span,并将该 Span 通过 context
传递给 Foo
方法,以便在 Foo
方法中将追踪链继续延续下去:
func main() {
closer := initJaeger("in-process")
defer closer.Close()
// 获取 jaeger tracer
t := opentracing.GlobalTracer()
// 创建 root span
sp := t.StartSpan("in-process-service")
// main 执行完结束这个 span
defer sp.Finish()
// 将 span 传递给 Foo
ctx := opentracing.ContextWithSpan(context.Background(), sp)
Foo(ctx)
}
4、Foo
、Bar
方法模拟了独立的子操作 Span,Foo
方法调用了 Bar
,假设在 Bar
中发生了一些错误,可以通过 span.LogFields
和 span.SetTag
将错误记录在追踪链中。StartSpanFromContext
这个方法看起来是直接从 ctx
中拿到 Span 并继承?
func Foo(ctx context.Context) {
// 开始一个 span, 设置 span 的 operation_name=Foo
span, ctx := opentracing.StartSpanFromContext(ctx, "Foo")
defer span.Finish()
// 将 context 传递给 Bar
Bar(ctx)
// 模拟执行耗时
time.Sleep(1 * time.Second)
}
func Bar(ctx context.Context) {
// 开始一个 span,设置 span 的 operation_name=Bar
span, ctx := opentracing.StartSpanFromContext(ctx, "Bar")
defer span.Finish()
// 模拟执行耗时
time.Sleep(2 * time.Second)
// 假设 Bar 发生了某些错误
err := errors.New("something wrong")
span.LogFields(
log.String("event", "error"),
log.String("message", err.Error()),
)
span.SetTag("error", true)
}
最后,简单看下 StartSpanFromContext
的 实现,印证了上面的猜想:
func StartSpanFromContextWithTracer(ctx context.Context, tracer Tracer, operationName string, opts ...StartSpanOption) (Span, context.Context) {
if parentSpan := SpanFromContext(ctx); parentSpan != nil {
opts = append(opts, ChildOf(parentSpan.Context()))
}
span := tracer.StartSpan(operationName, opts...)
return span, ContextWithSpan(ctx, span)
}
小结下上面的过程,如果要确保追踪链在程序中不断开,需要将函数的第一个参数设置为 context.Context
,通过 opentracing.ContextWithSpan
将保存到 context 中,通过 opentracing.StartSpanFromContext
开始一个新的子 span,然后设置直到调用流程结束。
假设我们需要在 gRPC 服务中调用另外一个微服务(如 RESTFul 服务),该如何跟踪?简单来说就是使用 HTTP 头作为媒介(Carrier)来传递跟踪信息(traceID)。下一小节,来看下 gRPC 中的 Opentracing 实现。
0x04 gRPC 中的 OpenTracing
本小节,介绍下 gRPC 与 OpenTracing 的结合使用,跟踪一个完整的 RPC 请求,从客户端到服务端的实现。分为两种:
- gRPC-client 端,使用了
tracer.Inject
方法,可以将 Span 的 Context 信息注入到 carrier 中,再将 carrier 写入到 metadata 中,即完成 span 信息的传递 - grpc-server 端,使用
tracer.Extract
函数将 carrier 从 metadata 中提取出来,再通过StartSpan
与ChildOf
方法创建新的子Span,这样 client 端与 server 端就能建立 Span 信息的关联
客户端
客户端的实现如下所示:
const (
endpoint_url = "http://localhost:9411/api/v1/spans" //Zipkin-UI 的 URL
host_url = "localhost:5051" // 作为标识
service_name_cache_client = "cache service client"
service_name_call_get = "callGet"
)
func newTracer () (opentracing.Tracer, zipkintracer.Collector, error) {
// 创建 HTTP Collector,用来收集跟踪数据并将其发送到 Zipkin-UI
collector, err := openzipkin.NewHTTPCollector(endpoint_url)
if err != nil {
return nil, nil, err
}
// 创建了一个记录器 (recorder) 来记录端口上的信息
recorder :=openzipkin.NewRecorder(collector, true, host_url, service_name_cache_client)
// 使用记录器 recorder 创建了一个新的跟踪器 (tracer)
tracer, err := openzipkin.NewTracer(
recorder,
openzipkin.ClientServerSameSpan(true))
if err != nil {
return nil,nil,err
}
// 设置全局 tracer
opentracing.SetGlobalTracer(tracer)
return tracer,collector, nil
}
func DoRPCMethods(c pb.HelloServiceClient) ( []byte, error) {
span := opentracing.StartSpan("RPC-CALL-METHOD-NAME")
defer span.Finish()
time.Sleep(5*time.Millisecond)
// Put root span in context so it will be used in our calls to the client
// 通过 opentracing.ContextWithSpan 拿到传递的 ctx
ctx := opentracing.ContextWithSpan(context.Background(), span)
//ctx := context.Background()
getReq:=&pb.RPCMethodReq{}
getResp, err :=RPCMethod(ctx, getReq)
value := getResp.Value
return value, err
}
func main(){
// 初始化 tracer
tracer, collector, err :=newTracer()
if err != nil {
panic(err)
}
defer collector.Close()
// 注意这里使用了拦截器 OpenTracingClientInterceptor
connection, err := grpc.Dial(host_url,
grpc.WithInsecure(), grpc.WithUnaryInterceptor(otgrpc.OpenTracingClientInterceptor(tracer, otgrpc.LogPayloads())),
)
if err != nil {
panic(err)
}
defer connection.Close()
client := pb.NewHelloServiceClient(connection)
err := DoRPCMethods(client)
......
}
服务端实现
下面是服务端代码,同样使用拦截器 OpenTracingServerInterceptor
来构建,服务端的 RPC 方法中包含了一次对 Database 的请求。
func main(){
connection, err := net.Listen(network, host_url)
if err != nil {
panic(err)
}
tracer,err := newTracer()
if err != nil {
panic(err)
}
opts := []grpc.ServerOption{
grpc.UnaryInterceptor(
otgrpc.OpenTracingServerInterceptor(tracer,otgrpc.LogPayloads()),
),
}
srv := grpc.NewServer(opts...)
cs := initCache()
pb.RegisterCacheServiceServer(srv, cs)
err = srv.Serve(connection)
if err != nil {
panic(err)
}
}
const service_name_db_query_user = "QueryDatabase"
func (c *HelloService) RPCMethod(ctx context.Context, req *pb.GetReq) (*pb.GetResp, error) {
if parent := opentracing.SpanFromContext(ctx); parent != nil {
pctx := parent.Context()
if tracer := opentracing.GlobalTracer(); tracer != nil {
mysqlSpan := tracer.StartSpan(service_name_db_query_user, opentracing.ChildOf(pctx))
defer mysqlSpan.Finish()
//do some operations
time.Sleep(time.Millisecond * 10)
}
}
key := req.GetKey()
value := c.storage[key]
fmt.Println("get called with return of value:", value)
resp := &pb.GetResp{Value: value}
return resp, nil
}
前一节,说到跨进程传递 Trace 的时候需要进行的 Inject
和 Extract
操作,上面的示例代码并没有出现。那么客户端 / 服务端如何实现对 Span 的 Inject
/Extract
呢?答案就是拦截器 otgrpc.OpenTracingServerInterceptor/OpenTracingClientInterceptor
方法,这里以 OpenTracingClientInterceptor
方法为例:
// OpenTracingClientInterceptor returns a grpc.UnaryClientInterceptor suitable
// for use in a grpc.Dial call.
//
// For example:
//
// conn, err := grpc.Dial(
// address,
// ..., // (existing DialOptions)
// grpc.WithUnaryInterceptor(otgrpc.OpenTracingClientInterceptor(tracer)))
//
// All gRPC client spans will inject the OpenTracing SpanContext into the gRPC
// metadata; they will also look in the context.Context for an active
// in-process parent Span and establish a ChildOf reference if such a parent
// Span could be found.
func OpenTracingClientInterceptor(tracer opentracing.Tracer, optFuncs ...Option) grpc.UnaryClientInterceptor {
otgrpcOpts := newOptions()
otgrpcOpts.apply(optFuncs...)
return func(
ctx context.Context,
method string,
req, resp interface{},
cc *grpc.ClientConn,
invoker grpc.UnaryInvoker,
opts ...grpc.CallOption,
) error {
var err error
var parentCtx opentracing.SpanContext
if parent := opentracing.SpanFromContext(ctx); parent != nil {
parentCtx = parent.Context()
}
if otgrpcOpts.inclusionFunc != nil &&
!otgrpcOpts.inclusionFunc(parentCtx, method, req, resp) {
return invoker(ctx, method, req, resp, cc, opts...)
}
clientSpan := tracer.StartSpan(
method,
opentracing.ChildOf(parentCtx),
ext.SpanKindRPCClient,
gRPCComponentTag,
)
defer clientSpan.Finish()
// 调用 injectSpanContext
ctx = injectSpanContext(ctx, tracer, clientSpan)
if otgrpcOpts.logPayloads {
clientSpan.LogFields(log.Object("gRPC request", req))
}
err = invoker(ctx, method, req, resp, cc, opts...)
if err == nil {
if otgrpcOpts.logPayloads {
clientSpan.LogFields(log.Object("gRPC response", resp))
}
} else {
SetSpanTags(clientSpan, err, true)
clientSpan.LogFields(log.String("event", "error"), log.String("message", err.Error()))
}
if otgrpcOpts.decorator != nil {
otgrpcOpts.decorator(clientSpan, method, req, resp, err)
}
return err
}
}
//injectSpanContext 方法
func injectSpanContext(ctx context.Context, tracer opentracing.Tracer, clientSpan opentracing.Span) context.Context {
md, ok := metadata.FromOutgoingContext(ctx)
if !ok {
md = metadata.New(nil)
} else {
md = md.Copy()
}
mdWriter := metadataReaderWriter{md}
err := tracer.Inject(clientSpan.Context(), opentracing.HTTPHeaders, mdWriter)
// We have no better place to record an error than the Span itself :-/
if err != nil {
clientSpan.LogFields(log.String("event", "Tracer.Inject() failed"), log.Error(err))
}
return metadata.NewOutgoingContext(ctx, md)
}
从客户端 injectSpanContext
的实现来看,最终在 RPC 调用前,通过 metadata.NewOutgoingContext
将 Context 信息(包含了 Tracer),即获取了跟踪上下文并将其注入 HTTP 头,因此我们不需要再次调用 Inject
函数。而在服务器端,从 HTTP 头中 Extract
跟踪上下文并将其放入 Golang context 中,无须手动调用 Extract 方法。
但对于其他基于 HTTP 的服务(如 RESTFul-API 服务),情况就并非如此,需要写代码从服务器端的 HTTP 头中提取跟踪上下文,亦或也使用拦截器实现,如 Kratos 的 bm 框架的 Trace 拦截器
0x05 数据库追踪
0x06 TraceId 的生成机制
这里引用下阿里云推荐的 traceId 生成规则:
TraceId 生成规则
TraceId 一般由接收请求经过的第一个服务器产生。规则是:服务器 IP + ID 产生的时间 + 自增序列 + 当前进程号(忽略 |
号),如 0ad1348f|1403169275002|1003|56696
:
- 前
8
位0ad1348f
即产生 TraceId 的机器的 IP(16
进制,每2
位转换一次,10.209.52.143
),可以根据这个规律来查找到请求经过的第一个服务器 - 中间
13
位1403169275002
是产生 TraceId 的时间 - 之后的
4
位1003
是一个自增的序列,从1000
涨到9000
,到达9000
后回到1000
回环 - 最后的
5
位56696
是当前的进程 ID,为了防止单机多进程出现 TraceId 冲突的情况,所以在 TraceId 末尾添加了当前的进程 ID
SpanId 生成规则
SpanId 代表本次调用在整个调用链路树中的(相对)位置。
假设一个 Web 系统 A 接收了一次用户请求,那么在这个系统日志中,记录下的 SpanId 是 0
,代表是整个调用的根节点,如果 A 系统处理这次请求,需要通过 RPC 依次调用 B、C、D 三个系统,那么在 A 系统的客户端日志中,SpanId 分别是 0.1
,0.2
和 0.3
,在 B、C、D 三个系统的服务端日志中,SpanId 也分别是 0.1
,0.2
和 0.3
;如果 C 系统在处理请求的时候又调用了 E,F 两个系统,那么 C 系统中对应的客户端日志是 0.2.1
和 0.2.2
,E、F 两个系统对应的服务端日志也是 0.2.1
和 0.2.2
。如果把一次调用中所有的 SpanId 收集起来,可以组成一棵完整的链路树。
0x07 一些代码上的细节
进程内与跨进程
在实现 tracing 逻辑的时候,一定要注意 Span 是否跨进程!二者的实现是不同的:
- 进程(同一个服务)内,通常用
StartSpanFromContext
实现 或SpanFromContext
实现 来模拟从 context 中启动一个子 span, - 对于 gRPC 中 client 的
context
和 server 的context
是跨进程context
,必须采用tracer.Inject
(客户端)以及tracer.Extract
(服务端)的方式,通过metadata
来传递; - 对于 http 框架,与 gRPC 类似(见下面例子)
生成span的方法
官方给出了4
种常用例子:
1、Creating a Span given an existing Go context.Context
If you use context.Context in your application, OpenTracing’s Go library will happily rely on it for Span propagation. To start a new (blocking child) Span, you can use StartSpanFromContext.
func xyz(ctx context.Context, ...) {
//...
span, ctx := opentracing.StartSpanFromContext(ctx, "operation_name")
defer span.Finish()
span.LogFields(
log.String("event", "soft error"),
log.String("type", "cache timeout"),
log.Int("waited.millis", 1500))
//...
}
2、Starting an empty trace by creating a “root span”
//It's always possible to create a "root" Span with no parent or other causal reference.
func xyz() {
...
sp := opentracing.StartSpan("operation_name")
defer sp.Finish()
...
}
3、Creating a (child) Span given an existing (parent) Span
func xyz(parentSpan opentracing.Span, ...) {
//...
sp := opentracing.StartSpan(
"operation_name",
opentracing.ChildOf(parentSpan.Context()))
defer sp.Finish()
//...
}
4、Serializing to the wire
跨进程Inject
:
func makeSomeRequest(ctx context.Context) ... {
if span := opentracing.SpanFromContext(ctx); span != nil {
httpClient := &http.Client{}
httpReq, _ := http.NewRequest("GET", "http://myservice/", nil)
// Transmit the span's TraceContext as HTTP headers on our
// outbound request.
opentracing.GlobalTracer().Inject(
span.Context(),
opentracing.HTTPHeaders,
opentracing.HTTPHeadersCarrier(httpReq.Header))
resp, err := httpClient.Do(httpReq)
//...
}
//...
}
4、Deserializing from the wire
跨进程Extract
:
//...
http.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) {
var serverSpan opentracing.Span
appSpecificOperationName := ...
wireContext, err := opentracing.GlobalTracer().Extract(
opentracing.HTTPHeaders,
opentracing.HTTPHeadersCarrier(req.Header))
if err != nil {
// Optionally record something about err here
}
// Create the span referring to the RPC client if available.
// If wireContext == nil, a root span will be created.
serverSpan = opentracing.StartSpan(
appSpecificOperationName,
ext.RPCServerOption(wireContext))
defer serverSpan.Finish()
ctx := opentracing.ContextWithSpan(context.Background(), serverSpan)
...
}
StartSpanFromContext 的第二个返回值
先看 StartSpanFromContext
方法的定义:
type contextKey struct{}
var activeSpanKey = contextKey{}
func StartSpanFromContext(ctx context.Context, operationName string, opts ...StartSpanOption) (Span, context.Context) {
//调用StartSpanFromContextWithTracer
return StartSpanFromContextWithTracer(ctx, GlobalTracer(), operationName, opts...)
}
func StartSpanFromContextWithTracer(ctx context.Context, tracer Tracer, operationName string, opts ...StartSpanOption) (Span, context.Context) {
if parentSpan := SpanFromContext(ctx); parentSpan != nil {
opts = append(opts, ChildOf(parentSpan.Context()))
}
span := tracer.StartSpan(operationName, opts...)
return span, ContextWithSpan(ctx, span)
}
// ContextWithSpan returns a new `context.Context` that holds a reference to
// the span. If span is nil, a new context without an active span is returned.
func ContextWithSpan(ctx context.Context, span Span) context.Context {
if span != nil {
if tracerWithHook, ok := span.Tracer().(TracerContextWithSpanExtension); ok {
ctx = tracerWithHook.ContextWithSpanHook(ctx, span)
}
}
return context.WithValue(ctx, activeSpanKey, span)
}
可以看到StartSpanFromContext
方法的第2
个返回值最终来源于ContextWithSpan
的context.WithValue
,就是使用StartSpanFromContext
创建了子Span的时候,同时生成了一个子context,用于后续的进程内tracing的context上下文,如果在之后还需要启动进程内的函数调用,那么就需要传入这个新的context(而不是创建之前的context),如果不需要的话,那么可以忽略掉这个返回值
0x08 一个综合的示例
下图演示了一个HTTP->RPC的调用路径上的Span传递方法:
0x09 小结
小结下,要实现 Tracing 机制及 Tracing 应用的关键点:
- 选择合适的数据收集端
- 明确当前是跨进程调用还是进程内调用,如何获取到
spanContext
- 传输
Span
的方法,一般会放在各类 header 中,避免侵入业务 - 采样率
- 对于 Tracing-Span 的代码实现而言:
- 如果 Tracing 不存在,那么则需要新建一个 Tracing,并且生成唯一的 TracingId
- 如果 Tracing 存在,那么需要查看调用代码处是一个 Span(不存在 Span,需新建),还是一个子 Span(从当前的 Span Fork 一个新的)
- Span 过程中,需要明确是否在本 Span 中进行 Finish(还是不需要),记录耗时,Tags,错误信息或者日志等