分布式链路追踪续集
本文是 分布式链路追踪 的续集。
在上一文中提到为了统一各种分布式追踪系统的实现,CNCF (云原生计算基金会)下的 OpenTracing 项目定义了一套分布式追踪的标准,可以使用 OpenTracing API 完成代码的监控埋点,最后用户自由选择遵循 OpenTracing 标准的链路追踪系统,比如 jaeger 。
但是现在访问 OpenTracing 的 官网[1] ,可以发现官网提醒 OpenTracing 和 OpenCensus 已经被合并成为 OpenTelemetry 。(在上文编写时我未发现到,感谢 Donald Liu 大佬的提醒)
在 cncf[2] 上也可以发现 OpenTracing 已经被 Archived 了
相应地,OpenTelemetry 已经正式成为了 CNCF 的孵化项目 参考[3]
Both OpenTracing and OpenCensus will be further deprecated in the coming weeks, with OpenTracing being formally archived by the CNCF TOC.
缘由
参考[4]
在 APM (Application Performance Monitoring) 领域,或者说微服务的可观察性方面包括有分布式链路追踪,指标监控和日志。
OpenTracing 是最早为分布式追踪制定了一套平台无关、厂商无关的协议标准的项目,并以此成为了 CNCF 的孵化项目。
在之后,谷歌牵头,微软加入,创建了 OpenCensus 项目统一 Metrics 基础指标监控的使用方式,还做了 OpenTracing 的老本行:分布式追踪。
一山不容二虎,OpenTracing 和 OpenCensus 愈打愈烈,对我们用户来讲,实在是太不友好了。
然后 OpenTelemetry 横空出世了,OpenTracing 和 OpenCensus 既然都这么好,干脆你们合并起来吧,我 OpenTelemetry 来兼容你们。
OpenTelemetry 的自身定位十分明确:数据采集和标准规范的统一,对于数据如何去使用、存储、展示、告警,官方是不涉及的。
OpenTelemetry 的终极目标十分伟大:实现 Metrics、Tracing、Logging 的融合及大一统,作为 APM 的数据采集终极解决方案。
然后就是现在的故事了,OpenTelemetry 正式成为 CNCF 的孵化项目,OpenTracing 和 OpenCensus 不再维护。
兼容性
这个放心,大部分的知识点定义还是一致的 (如数据模型:Trace, Span, SpanContext) ,但是 API 的使用会有所区别,详细可以查看 OpenTelemetry 规范文档[5]
承接上文
我们现在对上文的 OpenTracing API 的代码做出修改,拥抱 OpenTelemetry 吧!
安装 OpenTelemetry for Go 依赖
go get go.opentelemetry.io/otel@v1.0.0-RC1 go.opentelemetry.io/otel/sdk@v1.0.0-RC1 go.opentelemetry.io/otel/exporters/stdout/stdouttrace@v1.0.0-RC1 go.opentelemetry.io/otel/trace@v1.0.0-RC1
安装 OpenTelemetry for jaeger 依赖
go get -u go.opentelemetry.io/otel/exporters/jaeger
OpenTelemetry 官方为多种开源框架提供了开箱即用的 Instrumentation Packages ,如 gin , beego , mux , go-kit 等,当然也支持 net/http 标准库 ,更多可浏览opentelemetry-go-contrib[6]
我们使用的是 net/http 标准库,所以直接导入 otelhttp
go get go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp
main.go
修改如下:
package main
import (
"fmt"
"log"
"math/rand"
"net/http"
"time"
"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/exporters/jaeger"
"go.opentelemetry.io/otel/propagation"
"go.opentelemetry.io/otel/sdk/resource"
tracesdk "go.opentelemetry.io/otel/sdk/trace"
semconv "go.opentelemetry.io/otel/semconv/v1.4.0"
"go.opentelemetry.io/otel/trace"
)
func init() {
exp, err := jaeger.New(jaeger.WithAgentEndpoint())
if err != nil {
panic(err)
}
tp := tracesdk.NewTracerProvider(
tracesdk.WithSampler(tracesdk.AlwaysSample()),
tracesdk.WithBatcher(exp),
tracesdk.WithResource(resource.NewWithAttributes(
semconv.SchemaURL,
semconv.ServiceNameKey.String("opentelemetry-example"), // 服务名
semconv.ServiceVersionKey.String("0.0.1"),
)),
)
otel.SetTracerProvider(tp)
otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator(propagation.TraceContext{}, propagation.Baggage{}))
}
func main() {
port := 8080
addr := fmt.Sprintf(":%d", port)
mux := http.NewServeMux()
mux.HandleFunc("/", indexHandler)
mux.Handle("/home", otelhttp.NewHandler(http.HandlerFunc(homeHandler), "请求 /home"))
mux.Handle("/async", otelhttp.NewHandler(http.HandlerFunc(serviceHandler), "请求 /async"))
mux.Handle("/service", otelhttp.NewHandler(http.HandlerFunc(serviceHandler), "请求 /service"))
mux.Handle("/db", otelhttp.NewHandler(http.HandlerFunc(dbHandler), "请求 /db"))
fmt.Printf("http://localhost:%d\n", port)
log.Fatal(http.ListenAndServe(addr, mux))
}
// 主页 Html
func indexHandler(w http.ResponseWriter, r *http.Request) {
w.Write([]byte(` 点击开始发起请求 `))
}
func homeHandler(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("开始请求...\n"))
ctx := r.Context()
span := trace.SpanFromContext(ctx)
defer span.End()
// 发起异步请求
asyncReq, _ := http.NewRequest("GET", "http://localhost:8080/async", nil)
// 传递span的上下文信息
// 将关于本地追踪调用的span context,设置到http header上,并传递出去
otel.GetTextMapPropagator().Inject(ctx,
propagation.HeaderCarrier(asyncReq.Header),
)
go func() {
if _, err := http.DefaultClient.Do(asyncReq); err != nil {
span.RecordError(err)
span.SetAttributes(
attribute.String("请求 /async error", err.Error()),
)
}
}()
time.Sleep(time.Duration(rand.Intn(200)) * time.Millisecond)
// 发起同步请求
syncReq, _ := http.NewRequest("GET", "http://localhost:8080/service", nil)
otel.GetTextMapPropagator().Inject(ctx,
propagation.HeaderCarrier(syncReq.Header),
)
if _, err := http.DefaultClient.Do(syncReq); err != nil {
span.RecordError(err)
span.SetAttributes(
attribute.String("请求 /service error", err.Error()),
)
}
w.Write([]byte("请求结束!"))
}
// 模拟业务请求
func serviceHandler(w http.ResponseWriter, r *http.Request) {
// 通过http header,提取span元数据信息
span := trace.SpanFromContext(
otel.GetTextMapPropagator().Extract(r.Context(), propagation.HeaderCarrier(r.Header)),
)
defer span.End()
dbReq, _ := http.NewRequest("GET", "http://localhost:8080/db", nil)
otel.GetTextMapPropagator().Inject(r.Context(), propagation.HeaderCarrier(dbReq.Header))
if _, err := http.DefaultClient.Do(dbReq); err != nil {
span.RecordError(err)
span.SetAttributes(
attribute.String("请求 /db error", err.Error()),
)
}
time.Sleep(time.Duration(rand.Intn(200)) * time.Millisecond)
}
// 模拟DB调用
func dbHandler(w http.ResponseWriter, r *http.Request) {
// 通过http header,提取span元数据信息
span := trace.SpanFromContext(
otel.GetTextMapPropagator().Extract(r.Context(), propagation.HeaderCarrier(r.Header)),
)
defer span.End()
time.Sleep(time.Duration(rand.Intn(200)) * time.Millisecond)
}
同样的部署到集群中
kubectl apply -f https://raw.githubusercontent.com/togettoyou/jaeger-example/master/jaeger-example-opentelemetry.yaml -n observability
访问后观察 Jaeger UI 可以发现调用链和之前的也一致,包含 5 个 Span
具体 Span 信息
参考资料
官网: https://opentracing.io/
[2]cncf: https://www.cncf.io/archived-projects/
[3]参考: https://www.cncf.io/blog/2021/08/26/opentelemetry-becomes-a-cncf-incubating-project/
[4]参考: https://github.com/open-telemetry/docs-cn/blob/main/OT.md
[5]OpenTelemetry 规范文档: https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/api.md
[6]opentelemetry-go-contrib: https://github.com/open-telemetry/opentelemetry-go-contrib/blob/main/instrumentation/README.md