爱豆吧!

idouba@beta.

Opentracing 调用链服务端埋点

1 前言

通过一个最简单的代码例子来看使用opentracing在服务端埋点的逻辑。因为调用链的抽象模型都是来自dapper这篇论文,所以其他的方式也是类似,只是接口方法上稍有不同。

2 关于服务端埋点

主要流程看上是:

    1. 收到请求
    1. 从通信协议(如常用的HTTP header)中解出客户端传递的trace;
    1. 构造span
    1. 保存span
    1. 服务端返回请求
    1. 关闭span,将span的数据结构上传到调用链的数据存储节点。

下面结合一个例子中的额使用看下2 3 4 6四个调用链埋点步骤具体做了哪些事情。

3 主要流程

以最典型的http服务端为例。如下面是个http服务端的里,一般的最简单的一个http服务端写出来可能是这样,从request中解到请求的参数,然后执行服务端处理的逻辑,然后结果写到response。

func (s *httpService) sumHandler(w http.ResponseWriter, req *http.Request) {
// parse query parameters
v := req.URL.Query()
a, err := strconv.ParseInt(v.Get(a), 10, 64)
b, err := strconv.ParseInt(v.Get(b), 10, 64)
// call our Sum binding
result, err := s.service.Sum(req.Context(), a, b)
// return the result
w.Write([]byte(fmt.Sprintf(%d, result)))
}

看下如果在这端代码中加入埋点,能把对这个接口的调用跟踪起来。

下面是埋点的接口,在构造handler的时候通过了一个wrapper方法加入了埋点。即将一个handler Decorator成了另外一个handler,即在业务的handler上加入了埋点功能。

func NewHTTPHandler(tracer opentracing.Tracer, service Service) http.Handler {
vc := &httpService{service: service}
// Create the mux.
mux := http.NewServeMux()
// Create the Sum handler.
var sumHandler http.Handler
sumHandler = http.HandlerFunc(svc.sumHandler)
// Wrap the Sum handler with our tracing middleware.
sumHandler = middleware.FromHTTPRequest(tracer, Sum)(sumHandler)
// Wire up the mux.
mux.Handle(/sum/, sumHandler)
// Return the mux.
return mux
}

那么这修饰器的逻辑就是埋点的逻辑了。详细的解析这部分代码来看使用opentracing在服务端埋点的逻辑。

func FromHTTPRequest(tracer opentracing.Tracer, operationName string,
) HandlerFunc {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
// Try to join to a trace propagated in `req`.
//步骤1 解客户端span
wireContext, err := tracer.Extract(
opentracing.TextMap,
opentracing.HTTPHeadersCarrier(req.Header),
)
//步骤2 启动服务端span
span := tracer.StartSpan(operationName, ext.RPCServerOption(wireContext))
span.SetTag(serverSide, here)
//部署4 关闭span
defer span.Finish()
// 部署3 store span in context
ctx := opentracing.ContextWithSpan(req.Context(), span)
// update request context to include our new span
req = req.WithContext(ctx)
// next middleware or actual request handler
next.ServeHTTP(w, req)
})
}
}

代码中间的注释了下这4个步骤,这里详细描述下:

**步骤1 **从Request中解出SpanContext,即调用Tracer接口上定义的Extract方法。

Opentracing定义了inject和extract方法,将span的信息(SpanContext)编码到carrier中。当一个埋点的客户端发起请求时,span信息就加到了请求内容中。

Extract和Inject是跨进程Tracing的主要机制,一个span的发起方是在客户端,响应方是在服务端,要共享一个调用Id,在tracing中术语是spanId,需要有一种机制能从客户端发出来,在服务端再解出来(当然实际使用中不止这点信息,详细参照Span的数据结构和trace的模型定义),需要在跨进程调用的时候能传递出来,不管是RPC,消息队列还是HTTP等其他协议的调用。

Extract方法作为opentracing Tracer的三个基础方法之一,在opentracing接口中被定义。

Extract(format interface{}, carrier interface{}) (SpanContext, error)

在各个Opentracing的实现中,会有对应的实现。如openzipkin/zipkin-go-opentracing 有对应的实现:

对于http类型的服务调用也就是从http header里解出客户端传来的trace信息。

关于extract和inject详细可以参照opentracing中这部分描述。

**步骤2 **启动span

如果可以解出来客户端的span,则说明这个请求的客户端已经埋点,服务端的处理会和客户端在一个trace上,如果未解出来,则客户端未埋点,或者埋点数据丢失,在服务端重新创建一个span,即开始一个新的trace。在span的模型中,对客户端Span存在也是有两种风格的处理一种是服务端构造一个新的Span,parentspan设为客户端那个span,另外一种是直接更新解出来客户端的那个span。

**步骤3 **保存span

在服务端的请求过程中需要,需要保存span信息,以便在业务需要的时候可以访问到span。只有这样业务代码才可以设置自定义的tag,记录event,或者根据需要创建子span(如果需要对服务内部的调用详情在追踪的话)。

在openttracing的go的埋点方中是将span保存到到Request的context中,后面处理请求的业务代码会根据需要从Request中得到Context。关于为什么没有使用threadlocal而是使用Contex参照关于context的设计思想。

gocontext.go其实就是把span作为一个固定key的value设置给context,并借助context向下传递。

type contextKey struct{}
var activeSpanKey = contextKey{}
// ContextWithSpan returns a new `context.Context` that holds a reference to
// `span`’s SpanContext.
func ContextWithSpan(ctx context.Context, span Span) context.Context {
return context.WithValue(ctx, activeSpanKey, span)
}

如在前面例子中sumHandler的业务方法Sum就可以从Request的context中得到当前span并进行操作。

func (s *svc1) Sum(ctx context.Context, a, b int64) (int64, error) {
span := opentracing.SpanFromContext(ctx)
span.SetTag(proxy-to, svc2)

return result, nil
}

其中SpanFromContext是从Context中解出对应的对象

func SpanFromContext(ctx context.Context) Span {
val := ctx.Value(activeSpanKey)
if sp, ok := val.(Span); ok {
return sp
}
return nil
}

步骤4 结束span。就是服务端调处理完毕调用结束span。主要是标注span结束时间,同时通过recorder接口上报span。

这个方法定义在opentracing的span接口上,一般实现是这样(如zipkin-go-opentracing中)

func (s *spanImpl) Finish() {
s.FinishWithOptions(opentracing.FinishOptions{})
}
func (s *spanImpl) FinishWithOptions(opts opentracing.FinishOptions) {
finishTime := opts.FinishTime
if finishTime.IsZero() {
finishTime = time.Now()
}
duration := finishTime.Sub(s.raw.Start)
s.raw.Duration = duration
s.onFinish(s.raw)
s.tracer.options.recorder.RecordSpan(s.raw)
}

即记录span结束时间,并通过recordspan保存span,这就是调用方span的埋点数据。通过一种通道(实现了recorder接口的方法)上报到调用链的服务中心去,供调用链检索。

zipkin-recorder.go

func (r *Recorder) RecordSpan(sp RawSpan) {
if !sp.Context.Sampled {
return
}
span := &zipkincore.Span{
Name: sp.Operation,
ID: int64(sp.Context.SpanID),
TraceID: int64(sp.Context.TraceID.Low),
TraceIDHigh: traceIDHigh,
ParentID: parentSpanID,
Debug: r.debug || (sp.Context.Flags&flag.Debug == flag.Debug),
}
_ = r.collector.Collect(span)
}

将rawspan的数据格式转换为,然后通过一个collector上报到服务端。

上报方式collector可以有不同的实现,可以有基于http,也可以由基于消息方式的。

zipkin的span定义在这里,包括比较完备的解释参照原文件中注释。

type Span struct {
TraceID int64 `thrift:”trace_id,1″ json:”trace_id”`
// unused field # 2
Name string `thrift:”name,3″ json:”name”`
ID int64 `thrift:”id,4″ json:”id”`
ParentID *int64 `thrift:”parent_id,5″ json:”parent_id,omitempty”`
Annotations []*Annotation `thrift:”annotations,6″ json:”annotations”`
// unused field # 7
BinaryAnnotations []*BinaryAnnotation `thrift:”binary_annotations,8″ json:”binary_annotations”`
Debug bool `thrift:”debug,9″ json:”debug,omitempty”`
Timestamp *int64 `thrift:”timestamp,10″ json:”timestamp,omitempty”`
Duration *int64 `thrift:”duration,11″ json:”duration,omitempty”`
TraceIDHigh *int64 `thrift:”trace_id_high,12″ json:”trace_id_high,omitempty”`
}

4 最后

服务端埋点这部分逻辑,需要在服务端有个合适的地方修改,类似过滤器,或者相应请求的公共地方,避免在每个业务请求中加代码。

除了例子中的这个 zipkin-go-opentracing ,还有很多调用链的项目实行了opentracing的规范和定义。