go链路追踪
trace 的玩法
下面是基于 otel 结合 jaeger 实现的基于 grpc 分布式的链路追踪
了解什么是链路追踪?
定义
简单说,链路追踪是指在分布式系统中,将一次请求的所有调用过程进行记录和展示,以便于开发人员快速定位和解决问题。
作用
- 定位问题:通过记录和展示一次请求的所有调用过程,开发人员可以快速定位问题所在,减少定位问题的时间。
- 优化性能:通过分析一次请求的所有调用过程,开发人员可以了解系统的性能瓶颈,从而优化系统的性能。
- 监控系统:通过记录和展示一次请求的所有调用过程,开发人员可以了解系统的运行状态,及时发现系统的问题。
实现原理
链路追踪的实现原理主要是通过在分布式系统中添加一个中间件,该中间件负责记录和展示一次请求的所有调用过程。在一次请求中,每个服务都需要添加一个中间件,该中间件负责记录和展示该服务的调用过程。
工作流程
在链路追踪的业务架构上讲,他的工作流程是:
- 客户端发送请求到服务端。
- 服务端收到请求后,先添加一个中间件,该中间件负责记录和展示该服务的调用过程。
- 服务端调用其他服务时,也需要添加一个中间件,该中间件负责记录和展示该服务的调用过程。
- 服务端返回响应给客户端时,中间件负责记录和展示该服务的调用过程。
- 客户端收到响应后,中间件负责记录和展示该服务的调用过程。
- 链路追踪的中间件会将一次请求的所有调用过程记录下来,开发人员可以在 jaeger 中查看这些记录。
在链路追踪的实现架构上讲,他的工作流程是:
链路追踪系统(以 Jaeger 为例)的工作流程依赖于多个核心组件的协作,从数据生成到最终可视化,形成完整的追踪链路。以下是各组件的角色及协作流程:
核心组件
链路追踪系统通常包含 4 个核心组件,各组件职责明确且紧密协作:
组件 | 作用 |
---|---|
客户端 SDK | 嵌入到业务应用中,负责生成、采集追踪数据(如 span、标签、日志) |
Agent | 本地代理服务,轻量级组件,接收 SDK 发送的追踪数据并转发给 Collector |
Collector | 收集器,接收 Agent 或 SDK 直接发送的数据,进行验证、清洗、存储 |
Storage | 存储组件,持久化保存追踪数据(如 Elasticsearch、Cassandra、Badger) |
Query | 查询服务,提供 API 接口,支持从 Storage 中查询追踪数据并展示给 UI |
UI | 可视化界面,供用户查询、分析追踪链路(如调用关系、耗时、错误信息) |
完整工作流程
-
追踪数据生成(客户端 SDK)
- 业务应用通过集成链路追踪 SDK(如 Jaeger SDK),在代码中自动或手动生成追踪数据:
- Span:表示一个独立的工作单元(如一次 RPC 调用、数据库操作),包含唯一 ID、父 Span ID(用于关联调用链路)、开始/结束时间、标签(如服务名、方法名)、日志(如错误信息)等。
- Trace:由多个相关联的 Span 组成,代表一个完整的分布式请求链路(如用户请求从网关到多个微服务的全过程)。
- SDK 会通过上下文(Context)传递追踪信息(如 Trace ID、Span ID),确保跨服务调用时链路的连贯性。
- 业务应用通过集成链路追踪 SDK(如 Jaeger SDK),在代码中自动或手动生成追踪数据:
-
数据发送(SDK → Agent)
- SDK 生成追踪数据后,通过 UDP 或 HTTP 协议发送到本地的 Agent(通常与应用部署在同一台机器)。
- 选择 Agent 的原因:
- 减少 SDK 与 Collector 的网络交互开销(Agent 本地通信更快)。
- 批量处理数据,降低 Collector 的压力。
- 隔离应用与 Collector 的依赖(Agent 负责重试、缓冲,避免 Collector 故障影响应用)。
-
数据汇聚(Agent → Collector)
- Agent 接收 SDK 的数据后,进行简单处理(如批量打包),再转发给 Collector(通常通过 TCP 协议)。
- 部分场景下,SDK 也可跳过 Agent 直接向 Collector 发送数据(如 Agent 未部署时)。
-
数据处理与存储(Collector → Storage)
- Collector 接收数据后,执行验证(格式检查)、清洗(过滤无效数据)、转换(统一格式)等操作。
- 处理完成后,将数据持久化到 Storage(如 Elasticsearch 适合分布式存储和高效查询)。
- 部分系统支持数据采样(如只保留 10% 的追踪数据),避免存储压力过大。
-
查询与可视化(Query → UI)
- 用户通过 UI(如 Jaeger UI)输入查询条件(如 Trace ID、服务名、时间范围)。
- UI 向 Query 服务发送请求,Query 从 Storage 中查询符合条件的追踪数据。
- Query 将数据解析为结构化的链路信息(如调用树、各节点耗时),返回给 UI 展示。
- 最终用户可在 UI 中查看完整链路:服务调用关系、每个步骤的耗时、错误节点等,用于排查性能瓶颈或故障。
组件协作示例(一次用户请求)
- 用户请求到达网关服务,网关 SDK 生成初始 Span(根 Span),并将 Trace ID 传递给下游服务 A。
- 服务 A 接收请求,生成子 Span,调用服务 B 时携带 Trace ID 和父 Span ID。
- 服务 B 生成子 Span,完成处理后返回结果,SDK 将 Span 数据发送到本地 Agent。
- Agent 汇总服务 A、B、网关的 Span 数据,转发给 Collector。
- Collector 处理后存储到 Elasticsearch。
- 用户在 Jaeger UI 中查询该 Trace ID,Query 从 Elasticsearch 读取数据,UI 展示完整链路(网关 → 服务 A → 服务 B)及各步骤耗时。
启动 jaeger
docker run --rm --name jaeger
-e COLLECTOR_ZIPKIN_HOST_PORT=:9411
-p 6831:6831/udp
-p 6832:6832/udp
-p 5778:5778
-p 16686:16686
-p 4317:4317
-p 4318:4318
-p 14250:14250
-p 14268:14268
-p 14269:14269
-p 9411:9411
jaegertracing/all-in-one:1.55
Port | Protocol | Component | Function |
---|---|---|---|
6831 | UDP | agent | accept jaeger.thrift over Thrift-compact protocol (used by most SDKs) |
6832 | UDP | agent | accept jaeger.thrift over Thrift-binary protocol (used by Node.js SDK) |
5775 | UDP | agent | (deprecated) accept zipkin.thrift over compact Thrift protocol (used by legacy clients only) |
5778 | HTTP | agent | serve configs (sampling, etc.) |
16686 | HTTP | query | serve frontend |
4317 | HTTP | collector | accept OpenTelemetry Protocol (OTLP) over gRPC |
4318 | HTTP | collector | accept OpenTelemetry Protocol (OTLP) over HTTP |
14268 | HTTP | collector | accept jaeger.thrift directly from clients |
14250 | HTTP | collector | accept model.proto |
9411 | HTTP | collector | Zipkin compatible endpoint (optional) |
容器启动后,使用浏览器打开 http://localhost:16686 即可访问 Jaeger UI。
什么是 open telemetry?
open telemetry 是一个开源的 observability 框架,用于收集、处理、导出分布式系统的遥测数据(如指标、日志、跟踪)。它提供了一个统一的 API,支持多种语言和多种后端存储。
简单的说,他就是一个标准,上面的jaeger实现了这个标准。
open telemetry 定义了数据模型(如 Span、Trace、Metric)和 API,不同的实现(如 Jaeger、Prometheus、Zipkin)根据这个标准进行数据采集、处理和导出。
otel + jaeger + grpc
初始化 tracer
package traceimport ("context""go.opentelemetry.io/otel""go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc""go.opentelemetry.io/otel/propagation""go.opentelemetry.io/otel/sdk/resource"sdktrace "go.opentelemetry.io/otel/sdk/trace"semconv "go.opentelemetry.io/otel/semconv/v1.31.0""time"
)const (serviceName = "gRPC-Jaeger-Demo"// 应用集群名jaegerRPCEndpoint = "127.0.0.1:4317" // Jaeger RPC端口
)// 定义每个服务上的Tracer:单独记录每个服务链路,便于追踪数据的区分
var (TracerA = otel.Tracer("service-a")TracerB = otel.Tracer("service-b")TracerC = otel.Tracer("service-c")
)// newJaegerTraceProvider 创建一个 Jaeger Trace Provider
func newJaegerTraceProvider(ctx context.Context) (*sdktrace.TracerProvider, error) {// 创建一个使用 HTTP 协议连接本机Jaeger的 Exporterexp, err := otlptracegrpc.New(ctx,otlptracegrpc.WithEndpoint(jaegerRPCEndpoint),otlptracegrpc.WithInsecure())if err != nil {return nil, err}res, err := resource.New(ctx, resource.WithAttributes(semconv.ServiceName(serviceName)))if err != nil {return nil, err}traceProvider := sdktrace.NewTracerProvider(sdktrace.WithResource(res),sdktrace.WithSampler(sdktrace.AlwaysSample()), // 采样sdktrace.WithBatcher(exp, sdktrace.WithBatchTimeout(time.Second)),)return traceProvider, nil
}// InitTracer 初始化 Tracer
func InitTracer(ctx context.Context) (*sdktrace.TracerProvider, error) {tp, err := newJaegerTraceProvider(ctx)if err != nil {return nil, err}otel.SetTracerProvider(tp)otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator(propagation.TraceContext{}, propagation.Baggage{}),)return tp, nil
}
Server 端
- 初始化 tracer
// 初始化 Tracerctx := context.Background()tp, err := traceCli.InitTracer(ctx)if err != nil {log.Fatal(err)}defer func() {if err := tp.Shutdown(context.Background()); err != nil {log.Printf("Error shutting down tracer provider: %v", err)}}()
- 注册 tracer 到 grpcServer 中
// 创建grpc服务器grpcServer := grpc.NewServer(grpc.StatsHandler(otelgrpc.NewServerHandler()), // 设置 StatsHandler)
- 对服务的方法中手动添加span
- 在方法前面,创建span,用于继承上下文
- 在方法调用前,创建span,用于记录方法调用
- 记得结束span
Client 端
- 初始化 tracer
// 初始化 Tracerctx := context.Background()tp, err := traceCli.InitTracer(ctx)if err != nil {log.Fatal(err)}defer func() {if err := tp.Shutdown(context.Background()); err != nil {log.Printf("Error shutting down tracer provider: %v", err)}}()
- 注册 tracer 到 grpcClient 中
// 连接 A 服务conn1, err := grpc.NewClient(addr1, grpc.WithInsecure(), grpc.WithStatsHandler(otelgrpc.NewClientHandler()))if err != nil {panic(err)}
- 生成上下文,比如:
func CreateContext() context.Context {// 生成上下文: 包含必要的 client_id、user_id 等md := metadata.Pairs("timestamp", time.Now().Format(time.StampNano),"client-id", "client-lion","user-id", "yym",)ctx := metadata.NewOutgoingContext(context.Background(), md)return ctx
}
网关服务
中间件
用于生成根span,eg:
package midimport ("github.com/gin-gonic/gin""go.opentelemetry.io/otel/attribute""go.opentelemetry.io/otel/codes"semconv "go.opentelemetry.io/otel/semconv/v1.17.0""go.opentelemetry.io/otel/trace""net/http"traceCli "simple-grpc/trace"
)// TracingMiddleware 为每个请求创建根Span,作为链路起点
func TracingMiddleware() gin.HandlerFunc {return func(c *gin.Context) {// 从请求中提取可能存在的追踪上下文(如客户端传递的)// 没有则创建全新的根Spanctx, span := traceCli.TracerGateway.Start(c.Request.Context(),"Gateway "+c.Request.Method+" "+c.Request.URL.Path, // Span名称:包含HTTP方法和路径trace.WithAttributes(semconv.HTTPMethodKey.String(c.Request.Method), // 标准HTTP方法属性semconv.HTTPTargetKey.String(c.Request.URL.Path), // 请求路径semconv.HTTPUserAgentKey.String(c.Request.UserAgent()), // 用户代理attribute.String("client.ip", c.ClientIP()), // 客户端IP),)defer span.End() // 确保请求结束时关闭Span// 将包含根Span的上下文注入到Gin上下文,供后续处理函数使用c.Set("traceContext", ctx)// 处理请求c.Next()// 记录HTTP响应状态码span.SetAttributes(semconv.HTTPStatusCodeKey.Int(c.Writer.Status()))// 如果请求出错,标记Span状态if c.Writer.Status() >= http.StatusInternalServerError {span.SetStatus(codes.Error, "请求处理失败")}}
}
网关集成
- 先初始化 tracer
- use 中间件
- handler函数中,从 中间件传递的context 中获取链路追踪的上下文,用于调用 rpc 服务