Go从入门到精通(25)
一个简单web项目-实现链路跟踪
文章目录
前言
分布式链路跟踪技术(Distributed Tracing)是分布式系统(尤其是微服务架构)中用于追踪请求全链路流转的核心技术。它通过记录请求在多个服务间的传播路径、执行耗时、状态等信息,帮助开发者定位跨服务调用的性能瓶颈、故障点和依赖关系,是保障分布式系统可观测性(Observability)的三大支柱之一(另外两个是日志和指标)。
为什么需要分布式链路跟踪?
在单体应用中,一个请求的处理流程在单一进程内完成,通过日志即可定位问题;但在分布式系统(如微服务)中,一个用户请求可能经历以下流程:
客户端 → API网关 → 认证服务 → 订单服务 → 库存服务 → 数据库 → 缓存
这种场景下,传统调试方式面临三大挑战:
- 链路断裂:无法确定请求经过了哪些服务、调用顺序如何;
- 责任模糊:某个请求超时 / 失败时,无法判断是哪个服务导致(如订单服务本身慢,还是依赖的库存服务响应延迟);
- 性能盲区:无法量化各服务的耗时占比(如 90% 的耗时在数据库,还是在网络传输)。
分布式链路跟踪技术通过全链路数据采集与可视化,解决了这些问题。
go实现链路跟踪
这里以 OpenTelemetry+Zipkin为例实现链路跟踪
搭建zipkin 服务
一般公司会统一搭建zipkin服务,自行搭建参考官网
安装依赖
go get “go.opentelemetry.io/contrib/propagators/b3”
go get “go.opentelemetry.io/otel”
go get “go.opentelemetry.io/otel/exporters/zipkin”
go get “go.opentelemetry.io/otel/propagation”
go get “go.opentelemetry.io/otel/sdk/resource”
go get “go.opentelemetry.io/otel/sdk/trace”
go get go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin
添加tracing包,OpenTelemetry 和Zipkin
//tracing/tracing.go
package tracing
import (
"go-web-demo/logger"
"go.opentelemetry.io/contrib/propagators/b3"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/exporters/zipkin"
"go.opentelemetry.io/otel/propagation"
"go.opentelemetry.io/otel/sdk/resource"
"go.opentelemetry.io/otel/sdk/trace"
semconv "go.opentelemetry.io/otel/semconv/v1.30.0"
oteltrace "go.opentelemetry.io/otel/trace"
"go.uber.org/zap"
"os"
)
var (
tracer oteltrace.Tracer
)
func InitTracer() error {
// 初始化链路跟踪
zipkinEndpoint := os.Getenv("ZIPKIN_ENDPOINT")
if zipkinEndpoint == "" {
logger.Sugar.Warn("ZIPKIN_ENDPOINT 未设置,链路跟踪将被禁用")
return nil
}
zipkinExporter, err := zipkin.New(zipkinEndpoint)
if err != nil {
zap.L().Error("Failed to create Zipkin exporter", zap.Error(err))
return err
}
tp := trace.NewTracerProvider(
trace.WithSampler(trace.AlwaysSample()),
trace.WithBatcher(zipkinExporter),
trace.WithResource(resource.NewWithAttributes(semconv.SchemaURL, semconv.ServiceName("go-web-demo"))),
)
otel.SetTracerProvider(tp)
// 创建专门的W3C propagator
w3cPropagator := propagation.TraceContext{}
// 创建B3 propagator,同时支持单头和多头格式
b3Propagator := b3.New(
b3.WithInjectEncoding(b3.B3MultipleHeader | b3.B3SingleHeader),
)
otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator(w3cPropagator, b3Propagator, propagation.TraceContext{}, propagation.Baggage{}))
tracer = otel.Tracer("go-web-demo")
logger.Sugar.Infow("Zipkin 链路跟踪初始化成功,服务名: %s,端点: %s", "go-web-demo", zipkinEndpoint)
return nil
}
- zipkinEndpoint 来源于你搭建的服务地址
- trace.WithSampler(trace.AlwaysSample()) 这里使用100%采样,如果生成环境可以做适当的调整,比如0.1
sampler := sdktrace.TraceIDRatioBased(0.1) // 采样率10%
在 Gin 中集成 OpenTelemetry 中间件
import "go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin"
// 创建默认引擎,包含日志和恢复中间件
router := gin.Default()
// 添加Zipkin中间件
router.Use(otelgin.Middleware("go-web-demo"))
//其他之前的逻辑。。
log包添加获取traceId方法
func WithTraceCtx(ctx context.Context) *zap.Logger {
span := trace.SpanFromContext(ctx)
if !span.SpanContext().IsValid() {
return Logger
}
traceID := span.SpanContext().TraceID().String()
spanID := span.SpanContext().SpanID().String()
return Logger.With(
zap.String("traceId", traceID),
zap.String("spanId", spanID),
)
}
日志打印加入traceId
func HealthHandler(c *gin.Context) {
ctx := c.Request.Context()
logger.WithTraceCtx(ctx).Info("health check")
c.JSON(http.StatusOK, gin.H{"message": "success"})
}
日志打印效果
{"service": "user-api", "traceId": "579ec5e6c0a21967e628266103499008", "spanId": "9053af570d799499"}
当然我们也可以在zipkin服务器查看,把我们日志的traceId输入并且搜索就可以得到类型下图的效果。大家也可以多搭建一个服务调用效果更佳
sql 操作加入链路跟踪
//repository/database_connection.go
package repository
import gormTracing "gorm.io/plugin/opentelemetry/tracing"
func InitGormDB() error {
//。。。 之前的的数据库连接逻辑
//添加链路跟踪
err = DB.Use(gormTracing.NewPlugin(
//这里使用实际的数据类型,比如mysql
gormTracing.WithDBSystem(dbDriver),
gormTracing.WithoutServerAddress(),
))
}
客户端请求传递追踪上下文
// 调用其他服务并传递追踪上下文
func callAnotherService(ctx context.Context, url string) (*http.Response, error) {
// 从上下文中获取当前span
span := trace.SpanFromContext(ctx)
// 创建HTTP请求
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
if err != nil {
span.RecordError(err)
span.SetStatus(codes.Error, err.Error())
return nil, err
}
// 使用OpenTelemetry传播器将上下文注入到请求头
otel.GetTextMapPropagator().Inject(
ctx,
propagation.HeaderCarrier(req.Header),
)
// 发送请求
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
span.RecordError(err)
span.SetStatus(codes.Error, err.Error())
return nil, err
}
// 记录响应状态
span.SetAttributes(attribute.Int("http.status_code", resp.StatusCode))
if resp.StatusCode >= 400 {
span.SetStatus(codes.Error, resp.Status)
}
return resp, nil
}
关键点总结
- 使用 OpenTelemetry 标准:
通过 go.opentelemetry.io/otel 实现与 Zipkin 的集成,遵循 CNCF 标准。 - 中间件集成:
使用 go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin 自动处理 Gin 请求的追踪。