Space Lions

OpenTelemetry的一些随笔(1)

中文网站上面关于 OpenTelemetry 的介绍非常之少,仅有的也只是稍微介绍一下发展历史,并没有深入到它的架构、里面所用到的一些基础概念。

当然在开始之前,还是需要稍微提及一下 OpenTelemetry 的概念。

Observability #


最近呈冉冉向上之势的 CNCF,在前段时间出了个Trail Map,上面洋洋洒洒写了十大项。大体意思就是呢,你要搞 Cloud Native 是不是,那你对着这张图看看你到底有没有脸说。

这十大项是

这十项中,很多公司都有在做,但本文的主题 observability 这个在我看来是属于跟 CI/CD 一样的基础设施,但很少有公司从一开始就考虑。

在这个言必称 Cloud Native 的时代,张口 docker(哪怕 dockerfile 写得跟 xml 一样又臭又长),闭口 kubernetes(哪怕完全不理解 chart 只会东抄一点西抄一点),service mesh、distribution 这些 buzzword 满大街飞,observability 被提及的概率实在是太小了点。

为什么?因为实在不够高大上。

observability 说到底,只是用来看看整个系统运行状态,用于报警或者 debug 用的。整个系统需要记录的三样东西也非常没有技术含量:logs/metrics/traces。

可以看得到 CNCF 在这三块都有孵化项目,但是都是各司一职。OpenTelemetry 应运而生,目标是整合这三大块的标准,并且可以利用现有的项目:metrics 可以输出到 prometheus,tracing 可以输出到 jaeger。logs 这一块 OpenTelemetry 还没有动手,但也已经在时间表上了。

关于 OpenTelemetry 的历史,大家可以去网上查查看,在此就只是给大家一个概念。下面我们要结合一个具体的例子来说明 OpenTelemetry 的基础概念,以及它的实际用处。

实践 #


安装 Jaeger #

# 请先确保你机器上有docker
$ 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 14250:14250 \
-p 9411:9411 \
jaegertracing/all-in-one:1.21

打开浏览器 http://localhost:16686/ 就可以看到 Jaeger UI。

安装Go #

如果你已经安装有Go可以跳进这一步。如果没有的话,可以参考GVM

开始 #

package main

// initTracer creates a new trace provider instance and registers it as global trace provider.
func initTracerProvider() func() {
// Create and install Jaeger export pipeline.
flush, err := jaeger.InstallNewPipeline(
jaeger.WithCollectorEndpoint("http://localhost:14268/api/traces"),
jaeger.WithProcess(jaeger.Process{
ServiceName: "trace-demo",
Tags: []label.KeyValue{
label.String("exporter", "jaeger"),
},
}),
jaeger.WithSDK(&sdktrace.Config{DefaultSampler: sdktrace.AlwaysSample()}),
)
if err != nil {
log.Fatal(err)
}
return flush
}

func main() {
ctx := context.Background()

flush := initTracerProvider()
defer flush()

tr := otel.Tracer("component-main")
ctx, span := tr.Start(ctx, "foo")
defer span.End()

bar(ctx)
}

func bar(ctx context.Context) {
tr := otel.Tracer("component-bar")
_, span := tr.Start(ctx, "bar")
defer span.End()

// Do bar...
}

尝试运行一下这个例子

$ go run main.go

然后到http://localhost:16686,就能看到数据就已经进去(虽然这时候并不知道这些东西都是什么)。

下面我要开始解耦整个OpenTelemetry了(用最简单但并不一定准确的语言,因为我们需要先架构一个大蓝图)

《史记》 #


如果我们要给一个人写传记,大体上应该会照着几个步骤:

这一人的一生,就可以理解为OpenTelemetry的tracer。而TA所做的每一件事,都可以理解为span。而记录TA这一生故事的纸,可以理解为tracer provider,在上面的例子中,tracer provider就是jaeger

所以,在上面的例子中,我们实际上做了这些事情:

在这里可能有点混淆,看jaeger里面的意思,这两个名字分别是foobar是属于同一个trace的,但是为什么在这里我们却是两个tracer

答案很简单,因为trace的信息并不保存在tracer里面,而是保存在context里面。

前面有提到,context里面实际保存了span foo的完整信息,span foo中有一个SpanContext对象,SpanContext对象其中保存着TraceID。当用非空的context作为参数调用tracer.Start时,新生成的span bar将用同样的TraceID。所以在jaeger里面这两个自然会被当成同一个trace

这一段代码里在opentelemetry-go/sdk/trace/span.gostartSpanInternal

func startSpanInternal(ctx context.Context, tr *tracer, name string, parent trace.SpanContext, remoteParent bool, o *trace.SpanConfig) *span {
span := &span{}
span.spanContext = parent

cfg := tr.provider.config.Load().(*Config)

if hasEmptySpanContext(parent) {
// Generate both TraceID and SpanID
span.spanContext.TraceID, span.spanContext.SpanID = cfg.IDGenerator.NewIDs(ctx)
} else {
// TraceID already exists, just generate a SpanID
span.spanContext.SpanID = cfg.IDGenerator.NewSpanID(ctx, parent.TraceID)
}
...
}

可以在上面例子中尝试打印span.SpanContext().TraceID,会发现两者是一样的。

$ go run main.go
foo span trace id 16749d09802cf5e0c8299edb59a6b6ed
bar span trace id 16749d09802cf5e0c8299edb59a6b6ed

现在相信大家已经明白如何在同一个服务中,利用jaeger进行tracing,在下一篇里面会讲解open-telemetry真正的作用,如何在不同的服务中进行tracing。