spanContext
横跨几十个分布式组件的慢请求要如何排查,我们可能会想到用request_id将多个服务器上的日志串起来,但仅仅依靠 requestId 很难表达清楚服务之间的调用关系,所以从日志中就无法了解服务之间是谁在调用谁
因此,我们采用 traceId + spanId 这两个数据维度来记录服务之间的调用关系(这里 traceId 就是 requestId),也就是使用 traceId 串起单次请求,用 spanId 记录每一次 RPC 调用
通过这种方式,我们可以在日志中清晰地看出服务的调用关系是如何的,方便在后续计算中调整日志顺序,打印出完整的调用链路
spanId 是何时生成的,如何传递的
A 服务在发起 RPC 请求服务 B 前,先从线程上下文中获取当前的 traceId 和 spanId,然后依据上面的逻辑生成本次 RPC 调用的 spanId,再将 spanId 和 traceId 序列化后装配到请求体中,发送给服务方 B
服务方 B 获取请求后,从请求体中反序列化出 spanId 和 traceId,同时设置到线程上下文中,以便给下次 RPC 调用使用。在服务 B 调用完成返回响应前,计算出服务 B 的执行时间发送给消息队列
Go-Zero中的使用
在/etc/{your-service}.yaml下添加
Telemetry:
Name: your service name
Endpoint: http://127.0.0.1:14268/api/traces
Sampler: 1.0
Batcher: jaeger
配置完成后,重启,go-zero 会按拦截器的形式往ctx中注入trace_id 和span_id
rpc
// UnaryTracingInterceptor returns a grpc.UnaryClientInterceptor for opentelemetry.
func UnaryTracingInterceptor(ctx context.Context, method string, req, reply any,
cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error {
ctx, span := startSpan(ctx, method, cc.Target())
defer span.End()
ztrace.MessageSent.Event(ctx, 1, req)
err := invoker(ctx, method, req, reply, cc, opts...)
ztrace.MessageReceived.Event(ctx, 1, reply)
if err != nil {
s, ok := status.FromError(err)
if ok {
span.SetStatus(codes.Error, s.Message())
span.SetAttributes(ztrace.StatusCodeAttr(s.Code()))
} else {
span.SetStatus(codes.Error, err.Error())
}
return err
}
span.SetAttributes(ztrace.StatusCodeAttr(gcodes.OK))
return nil
}