基于 gRPC Metadata 实现分布式链路追踪并集成 Jaeger/Zipkin
基于 gRPC Metadata 实现分布式链路追踪并集成 Jaeger/Zipkin
在微服务架构中,一个请求往往需要经过多个服务才能完成,这使得问题排查变得异常困难。分布式链路追踪技术可以帮助我们追踪请求在各个服务之间的调用链,从而快速定位问题。本文将介绍如何使用 gRPC 的 Metadata 实现链路追踪,并将其集成到流行的追踪系统 Jaeger 或 Zipkin 中。
1. gRPC Metadata 简介
gRPC Metadata 是一种键值对形式的元数据,可以在 gRPC 调用中传递。它类似于 HTTP Header,但更加灵活,可以携带任何自定义信息。Metadata 在 gRPC 的拦截器(Interceptor)中被广泛使用,例如用于身份验证、授权、链路追踪等。
在 gRPC 中,可以使用 context 对象来传递和获取 Metadata。客户端可以使用 grpc.WithPerRPCCredentials 或 grpc.WithUnaryInterceptor 等选项来添加 Metadata,服务端可以使用 grpc.ServerInterceptor 来获取 Metadata。
2. 链路追踪原理
链路追踪的核心思想是在每个请求中添加唯一的追踪 ID(Trace ID),并在请求经过的每个服务中记录该 ID。同时,每个服务还会生成一个 Span ID,用于标识该服务在该请求中的调用。通过收集这些信息,我们可以构建出完整的请求调用链。
常见的链路追踪术语包括:
- Trace ID: 唯一标识一个请求的 ID,贯穿整个调用链。
- Span ID: 唯一标识一个服务调用的 ID。
- Parent Span ID: 当前 Span 的父 Span ID,用于构建调用链的层次关系。
- Span Context: 包含 Trace ID、Span ID 和其他追踪信息的上下文。
3. 使用 gRPC Metadata 实现链路追踪
我们可以使用 gRPC Metadata 来传递 Trace ID、Span ID 和 Parent Span ID 等追踪信息。以下是一个简单的示例,演示如何在客户端和服务端之间传递这些信息。
3.1 定义 Metadata Key
首先,我们需要定义用于传递追踪信息的 Metadata Key:
const (
TraceIDKey = "trace-id"
SpanIDKey = "span-id"
ParentIDKey = "parent-id"
)
3.2 客户端拦截器
客户端拦截器负责在每个 gRPC 请求中添加追踪信息:
import (
"context"
"fmt"
"google.golang.org/grpc"
"google.golang.org/grpc/metadata"
"github.com/google/uuid"
)
func ClientInterceptor() grpc.UnaryClientInterceptor {
return func(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error {
// 生成 Trace ID 和 Span ID
traceID := uuid.New().String()
spanID := uuid.New().String()
// 从 Context 中获取 Parent Span ID
parentID := ""
if md, ok := metadata.FromOutgoingContext(ctx); ok {
if len(md.Get(SpanIDKey)) > 0 {
parentID = md.Get(SpanIDKey)[0]
}
}
// 将追踪信息添加到 Metadata
md := metadata.Pairs(
TraceIDKey, traceID,
SpanIDKey, spanID,
ParentIDKey, parentID,
)
ctx = metadata.NewOutgoingContext(ctx, md)
fmt.Printf("Client: TraceID=%s, SpanID=%s, ParentID=%s\n", traceID, spanID, parentID)
// 调用 gRPC 方法
err := invoker(ctx, method, req, reply, cc, opts...)
return err
}
}
3.3 服务端拦截器
服务端拦截器负责从 gRPC 请求中提取追踪信息:
import (
"context"
"fmt"
"google.golang.org/grpc"
"google.golang.org/grpc/metadata"
)
func ServerInterceptor() grpc.UnaryServerInterceptor {
return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
// 从 Metadata 中获取追踪信息
md, ok := metadata.FromIncomingContext(ctx)
if !ok {
fmt.Println("Server: No metadata found")
} else {
traceID := ""
spanID := ""
parentID := ""
if len(md.Get(TraceIDKey)) > 0 {
traceID = md.Get(TraceIDKey)[0]
}
if len(md.Get(SpanIDKey)) > 0 {
spanID = md.Get(SpanIDKey)[0]
}
if len(md.Get(ParentIDKey)) > 0 {
parentID = md.Get(ParentIDKey)[0]
}
fmt.Printf("Server: TraceID=%s, SpanID=%s, ParentID=%s\n", traceID, spanID, parentID)
}
// 调用 gRPC 方法
resp, err := handler(ctx, req)
return resp, err
}
}
3.4 使用拦截器
在创建 gRPC 客户端和服务端时,需要分别注册这些拦截器:
// 客户端
conn, err := grpc.Dial(
address,
grpc.WithInsecure(),
grpc.WithUnaryInterceptor(ClientInterceptor()),
)
// 服务端
s := grpc.NewServer(grpc.UnaryInterceptor(ServerInterceptor()))
4. 集成 Jaeger 或 Zipkin
仅仅在服务间传递追踪信息是不够的,我们还需要将这些信息发送到追踪系统进行存储和可视化。Jaeger 和 Zipkin 是两个流行的开源分布式追踪系统。
4.1 集成 Jaeger
以下是将 gRPC Metadata 中提取的追踪信息发送到 Jaeger 的示例:
安装 Jaeger 客户端库:
go get github.com/uber/jaeger-client-go配置 Jaeger:
需要配置 Jaeger Agent 的地址和采样策略。
修改服务端拦截器:
在服务端拦截器中,从 Metadata 中提取追踪信息,并使用 Jaeger 客户端库创建 Span:
import ( "context" "fmt" "google.golang.org/grpc" "google.golang.org/grpc/metadata" "github.com/opentracing/opentracing-go" "github.com/uber/jaeger-client-go" "github.com/uber/jaeger-client-go/config" ) var tracer opentracing.Tracer func init() { // 配置 Jaeger cfg := config.Configuration{ ServiceName: "my-grpc-service", Sampler: &config.SamplerConfig{ Type: jaeger.SamplerTypeConst, Param: 1, }, Reporter: &config.ReporterConfig{ LogSpans: true, // Jaeger Agent 地址 LocalAgentHostPort: "127.0.0.1:6832", }, } // 初始化 Tracer jaegerTracer, _, err := cfg.NewTracer(config.Logger(jaeger.StdLogger)) if err != nil { panic(fmt.Sprintf("ERROR: cannot init Jaeger: %v\n", err)) } tracer = jaegerTracer opentracing.SetGlobalTracer(tracer) } func ServerInterceptor() grpc.UnaryServerInterceptor { return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) { md, ok := metadata.FromIncomingContext(ctx) if !ok { fmt.Println("Server: No metadata found") } else { traceID := "" spanID := "" parentID := "" if len(md.Get(TraceIDKey)) > 0 { traceID = md.Get(TraceIDKey)[0] } if len(md.Get(SpanIDKey)) > 0 { spanID = md.Get(SpanIDKey)[0] } if len(md.Get(ParentIDKey)) > 0 { parentID = md.Get(ParentIDKey)[0] } fmt.Printf("Server: TraceID=%s, SpanID=%s, ParentID=%s\n", traceID, spanID, parentID) // 创建 Span var span opentracing.Span if parentID != "" { // 从 Parent Span 创建 Span parentSpanContext, err := tracer.Extract(opentracing.TextMap, metadataTextMap{md}) if err != nil && err != opentracing.ErrSpanContextNotFound { fmt.Printf("Extract from metadata err: %v\n", err) } span = tracer.StartSpan(info.FullMethod, opentracing.ChildOf(parentSpanContext)) } else { // 创建 Root Span span = tracer.StartSpan(info.FullMethod) } defer span.Finish() // 将 Span 注入到 Context 中 ctx = opentracing.ContextWithSpan(ctx, span) } resp, err := handler(ctx, req) return resp, err } } type metadataTextMap struct { metadata.MD } func (m metadataTextMap) Set(key, value string) { m.MD.Set(key, value) } func (m metadataTextMap) ForeachKey(handler func(key, val string) error) error { for key, values := range m.MD { for _, value := range values { if err := handler(key, value); err != nil { return err } } } return nil }在客户端添加 SpanContext 到 Metadata:
func ClientInterceptor() grpc.UnaryClientInterceptor { return func(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error { // 从 Context 中获取 Span span := opentracing.SpanFromContext(ctx) if span == nil { // 生成 Trace ID 和 Span ID traceID := uuid.New().String() spanID := uuid.New().String() // 将追踪信息添加到 Metadata md := metadata.Pairs( TraceIDKey, traceID, SpanIDKey, spanID, ) ctx = metadata.NewOutgoingContext(ctx, md) } else { // 将 SpanContext 注入到 Metadata md := metadata.MD{} err := tracer.Inject(span.Context(), opentracing.TextMap, metadataTextMap{md}) if err != nil { fmt.Printf("inject to metadata err: %v\n", err) } ctx = metadata.NewOutgoingContext(ctx, md) } // 调用 gRPC 方法 err := invoker(ctx, method, req, reply, cc, opts...) return err } }
4.2 集成 Zipkin
集成 Zipkin 的步骤与 Jaeger 类似,需要安装 Zipkin 客户端库,配置 Zipkin Collector 的地址,并在拦截器中使用 Zipkin 客户端库创建 Span 并发送到 Zipkin Collector。
5. 最佳实践和注意事项
- 性能优化: 链路追踪会对性能产生一定的影响,需要尽量减少追踪信息的数量,并使用异步方式发送追踪数据。
- 错误处理: 在拦截器中需要处理各种错误情况,例如 Metadata 获取失败、Span 创建失败等。
- 采样策略: 可以根据实际需求配置采样策略,例如只对一部分请求进行追踪。
- 上下文传递: 除了 Metadata,还可以使用 Context 来传递追踪信息,例如使用
opentracing.ContextWithSpan将 Span 注入到 Context 中。
总结
本文介绍了如何使用 gRPC Metadata 实现分布式链路追踪,并将其集成到 Jaeger 或 Zipkin 中。通过链路追踪,我们可以更好地监控和调试 gRPC 应用,快速定位问题,提高开发效率。