小Cの已经记不起来的博客

Go 语言 链路追踪 实战:从原理到落地

前阵子线上一个下单接口突然变慢,用户反馈要等七八秒才有响应。这个接口一路串了订单、库存、支付三个服务,日志翻了半天,只知道确实慢,就是不知道慢在哪一段。最后我干了一件很蠢但有效的事:在三个服务里手动打时间戳日志,一条一条对出来,是库存服务里一条没走索引的 SQL。问题解决了,但这个过程实在太难受,于是决定认真搞一搞链路追踪。

结果一查资料就被劝退了一次:OpenTelemetry 的 Go 官方文档写得是真跳跃,大部分中文教程还停留在两三年前的老 API,跟着敲不是包路径对不上就是刷一屏 deprecated 警告,没准儿等你搜到某篇教程的时候,里面用的老上报方式已经废了。下面记录一下我实测跑通的完整过程,从原理到代码到部署,一次说清楚。

原理其实就三句话

链路追踪这玩意儿拆开看就三个概念,十分钟就能讲明白:

巧的是,Go 天生就有现成的载体,context.Context,trace 信息跟着 context 在函数之间传就行,这点比 Java 那些靠线程变量硬塞的方案舒服太多了。

选型:OpenTelemetry + Jaeger

没什么好纠结的,直接上 OpenTelemetry(下称 OTel)。它是 CNCF 的标准,SDK 统一,以后想换后端(Jaeger、Tempo、SkyWalking 随便换)只需要改 exporter 配置,业务代码不用动。后端我选 Jaeger,够轻量,UI 也够用,开发调试阶段用 all-in-one 镜像一行命令起:

docker run -d --name jaeger -p 16686:16686 -p 4318:4318 jaegertracing/all-in-one:1.57

16686 是 UI 页面端口,4318 是 OTLP over HTTP 的接收端口。注意老教程里常见的那种往 14268 上报 Thrift 格式的方式已经不推荐了,Jaeger 现在的官方口径就是让你用 OTLP。

写代码:初始化 TracerProvider

先把依赖拉下来:

go get go.opentelemetry.io/otel go.opentelemetry.io/otel/sdk go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp

然后写个初始化函数:

package trace

import (
	"context"

	"go.opentelemetry.io/otel"
	"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp"
	"go.opentelemetry.io/otel/sdk/resource"
	sdktrace "go.opentelemetry.io/otel/sdk/trace"
	semconv "go.opentelemetry.io/otel/semconv/v1.26.0"
)

func Init(ctx context.Context) (func(context.Context) error, error) {
	exporter, err := otlptracehttp.New(ctx,
		otlptracehttp.WithEndpoint("localhost:4318"),
		otlptracehttp.WithInsecure(), // 本地没配证书,必须加这个,不加连不上
	)
	if err != nil {
		return nil, err
	}

	tp := sdktrace.NewTracerProvider(
		sdktrace.WithBatcher(exporter),
		sdktrace.WithResource(resource.NewWithAttributes(
			semconv.SchemaURL,
			semconv.ServiceName("order-service"), // Jaeger 里显示的服务名
		)),
	)
	otel.SetTracerProvider(tp)

	return tp.Shutdown, nil
}

main 里启动时调一下,退出时记得 shutdown,不然内存里攒着的 span 可能还没发出去进程就没了,你会看到链路莫名其妙缺一段:

shutdown, err := trace.Init(context.Background())
if err != nil {
    log.Fatal(err)
}
defer shutdown(context.Background())

中间件:把 trace 织进 HTTP 服务

以 gin 为例,写一个中间件干两件事:从请求头里 Extract 上游传来的 trace 上下文,再为当前请求开一个根 span。

func Middleware() gin.HandlerFunc {
	propagator := propagation.TraceContext{}
	tracer := otel.Tracer("order-service")

	return func(c *gin.Context) {
		ctx := propagator.Extract(c.Request.Context(), propagation.HeaderCarrier(c.Request.Header))
		ctx, span := tracer.Start(ctx, c.Request.Method+" "+c.FullPath())
		defer span.End()

		c.Request = c.Request.WithContext(ctx)
		c.Next()
	}
}

这里最容易犯的错就是 Extract 完忘了 c.Request = c.Request.WithContext(ctx),上下文没塞回去,后面所有业务代码拿到的都是干净的 context,span 全断。

然后是往下游传。发起 HTTP 调用时把当前上下文 Inject 进请求头:

req, _ := http.NewRequestWithContext(ctx, "POST", "http://stock-service/api/deduct", body)
propagation.TraceContext{}.Inject(ctx, propagation.HeaderCarrier(req.Header))

gRPC 服务更省事,社区有现成的拦截器:

import "go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc"

// 服务端
grpc.NewServer(grpc.StatsHandler(otelgrpc.NewServerHandler()))
// 客户端
grpc.NewClient(addr, grpc.WithStatsHandler(otelgrpc.NewClientHandler()))

顺带一提,网上还能搜到大量 otelgrpc.UnaryServerInterceptor() 的写法,旧版本能用,但新版推荐 StatsHandler 这种方式,性能和流式调用支持都更好。数据库同理,GORM 有 gorm.io/plugin/opentelemetry 插件,一行挂上就行,SQL 耗时会以子 span 的形式挂在链路上。

看效果

不出问题的话,访问几次接口,打开 http://localhost:16686,选中 order-service,Find Traces,就能看到瀑布图了:订单服务里嵌着库存服务和支付服务的 span,库存服务里又嵌着那条 SQL,慢在哪一眼就看出来。之前手动对日志的活儿,以后是真不用再干了。

踩过的几个坑

最后记一下我踩过的坑,没准儿能帮你省点时间:

  1. WithInsecure() 忘了加。本地起的 Jaeger 没有证书,OTLP exporter 默认走 TLS,表现就是一直连接超时,报错还不怎么直白。
  2. 链路断成两截。Jaeger 里看到两条互不相干的 trace,八成是某个服务的中间件忘了 Extract,或者 Extract 了忘了写回 context。
  3. goroutine 里 trace 丢了。开协程前记得把 ctx 传进去;协程里要调下游的话,用 trace.ContextWithRemoteSpanContext 把 span 上下文带过去。
  4. 生产全量采样把 Jaeger 打爆了。开发环境全量没问题,生产建议 sdktrace.WithSampler(sdktrace.ParentBased(sdktrace.TraceIDRatioBased(0.1))),采样率 10%,并且 ParentBased 能保证同一条链路要么全采要么全丢,不然拼出来的链路是残的。
  5. 退出没 Shutdown。表现为部分请求的 trace 随机丢失,排查半天以为网络问题,其实是进程退出时 batcher 里的数据没刷出去。

写在最后

链路追踪不算什么黑科技,原理十分钟讲完,真正花时间的是落地:每个服务的框架、每个中间件、每个下游调用都得接一遍。我的建议是别贪多,先挑一条最核心的调用链打通,在 Jaeger 里看到完整瀑布图的那一下还是很爽的,剩下的服务照葫芦画瓢,一个下午基本都能铺完。

评论

还没有评论。

发表评论

提交后评论将经过自动审核,审核通过后公开展示。

未在播放