最近做了一点traces相关的工作,看了关于jaeger的一些内容,来水一篇。(艰难地保持着一月一篇)
随着应用的发展,分布式是不可避免的趋势,无论是随着业务的复杂庞大由单体应用拆分为微服务、出于扩展以及容灾的考虑将服务多机房部署多份还是各种分布式中间件的引入等原因。分布式使应用各方面的能力大幅提升,但同时使应用的复杂度大幅提高,问题定位变得困难。
链路追踪traces,顾名思义,就是记录一个请求的调用链路。其侧重点为调用的链路,通常是以有向无环图的形式展示,能反应服务之间的依赖关系。在系统的可观测性领域,与traces并列的是mertics和logs。metrics侧重于指标,包括通用的系统性能指标,例如cpu、内存等,以及开发者关心的业务指标,例如时延、用户数等,通过指标可以展示系统的状态。logs的数据最为零散但是最为详细,其不够系统但能提供最具体最深入的信息。
在分布式环境中,一个请求会有很长的、横跨多个服务或者中间件的、复杂的调用链路。当想分析某个请求为什么出错时,traces能帮助我们迅速定位出错的环节,这就是为什么需要分布式的链路追踪。
简单介绍下jaeger的架构。
write directly to storage
write to Kafka as a preliminary buffer
分布式链路追踪业界有很多实现,但是原理上差不多。因为我使用的是jaeger,所以会以jaeger为例。另外不会讲太多具体实现的内容,例如traces数据的序列化、传输、存储、检索等等,主要的内容会放在基本概念、数据结构、如何使用。如果后面有时间,会再写一些jaeger的演进的内容,都是来自这篇Evolving Distributed Tracing at Uber Engineering,可能会对我们迭代产品有一些启发。
jaeger中的数据模型如下。下面会结合数据模型一起介绍基本的概念。
trace和span是分布式链路追踪最基本的概念。trace代表一个请求的链路,会有一个全局唯一的trace_id标识;span代表请求中的一个具体的执行单元,在同一个trace下每个span都有一个唯一的span_id进行标识。
在数据模型中可以看到trace并没有具体的数据结构,其是由同属于一个trace_id的span组织而成。span是主要的数据结构,其携带了详细的调用信息,主要包括这些字段:opreationName表示span的主要操作,是由开发人员进行赋值的;startTime和duration携带了时间相关的信息;tags和logs都是kv形式的数据,只不过logs携带了时间戳。
span中除了携带调用相关的信息外还携带了span之间的关联数据。看references字段知道span之间有父子关系(child_of)和顺序执行(follow_from)的关系,span按照相互之间关系组织起来就是trace。trace通常表现为由span组成的有向无环图。下图展示了trace和span之间的关系。
demo如下。
首先初始化全局的tracer对象。
func JaegerInit() (opentracing.Tracer, io.Closer) {
cfg := &config.Configuration{
ServiceName: "MY_PROJECT_NAME",
Sampler: &config.SamplerConfig{
Type: "const",
Param: 1,
},
Reporter: &config.ReporterConfig{
LogSpans: true,
LocalAgentHostPort: "127.0.0.1:6831",
},
Tags: []opentracing.Tag{},
}
tracer, closer, err := cfg.NewTracer()
if err != nil {
panic(fmt.Sprintf("ERROR: cannot init Jaeger: %v\n", err))
}
// 这里顺便将 tracer 放入全局范围,opentracing 其他 api 的内部
// 实现会使用该全局的 tracer
opentracing.SetGlobalTracer(tracer)
return tracer, closer
}
开始记录trace数据。
func main() {
// "main" 是上面提到的operationName。通常是根据业务自定义名称,
// 这里 StartSpan 没有传递任何的 opentracing.StartSpanOption 参数,
// 所以得到的 span 是 root span。
span := tracer.StartSpan("start")
defer span.Finish()
// 将 span 存放到 context 中,其他函数可以从这个 context 中提取出
// span 的拓扑关系
ctx := opentracing.ContextWithSpan(context.Background(), span)
helloStr := methodA(ctx, helloTo)
methodB(ctx, helloStr)
}
func methodA(ctx context.Context, helloTo string) string {
// 从 context 中提取出span,如果 context 中没有span,
// 则这里得到的 span 将是 root span。所以如果 span 在链路的前进过程中忘记
// 传递,将会导致断链。
span, _ := opentracing.StartSpanFromContext(ctx, "methodA")
defer span.Finish()
return "Hello! " + helloTo
}
func methodB(ctx context.Context, helloStr string) {
span, _ := opentracing.StartSpanFromContext(ctx, "methodB")
defer span.Finish()
}
上面演示的是大多数情况下的场景,如果是调用的第一环就创建rootspan,否则可以从ctx中拿到相关信息并创建child span。ctx中传递数据其实本质是通过携带spanContext的数据结构实现的。spanContext的结构如下。
// SpanContext represents propagated span identity and state
type SpanContext struct {
// traceID represents globally unique ID of the trace.
// Usually generated as a random number.
traceID TraceID
// spanID represents span ID that must be unique within its trace,
// but does not have to be globally unique.
spanID SpanID
// parentID refers to the ID of the parent span.
// Should be 0 if the current span is a root span.
parentID SpanID
// Distributed Context baggage. The is a snapshot in time.
baggage map[string]string
// debugID can be set to some correlation ID when the context is being
// extracted from a TextMap carrier.
//
// See JaegerDebugHeader in constants.go
debugID string
// samplingState is shared across all spans
samplingState *samplingState
// remote indicates that span context represents a remote parent
remote bool
}
但是很多情况下调用链路中不一定全是rpc请求,比如中间可能会经历http调用或者kafka等消息队列。这些情况在jaeger中是通过"uber-trace-id"的key来传递信息的,其value为trace-id:span-id:parent-span-id:sample的字符串,该信息会设置在http的header或者kafka的message中。这种情况下的demo如下。
func main() {
clientContext, _ := opentracing.GlobalTracer().Extract(opentracing.TextMap, opentracing.TextMapCarrier(map[string]string{"uber-trace-id": traceId}))
span := tracer.StartSpan("start",opentracing.ChildOf(clientContext))
defer span.Finish()
ctx := opentracing.ContextWithSpan(context.Background(), span)
helloStr := methodA(ctx, helloTo)
methodB(ctx, helloStr)
}