Files
cs-note/hzh/MS/06-gRPC/04-拦截器.md
T
2026-05-24 11:42:38 +08:00

6.2 KiB
Raw Blame History

tags, create time
tags create time
grpc
interceptor
middleware
context-propagation
metadata
2026-05-07 16:00

Interceptor 与上下文传播

概述

Interceptor 是 gRPC 的切面编程能力,相当于 Web 框架中的 Middleware。每个 RPC 调用都会经过 Unary Interceptor 或 Stream Interceptor 链——无论它是普通的一问一答还是长时间的双向流。

本文涵盖拦截器的编写模式、链式串联技巧,以及最重要的上下文传播机制。

Interceptor 链架构

graph LR
    subgraph ClientChain["客户端拦截器链"]
        CAuth["认证拦截"] --> CMetric["指标采集"] --> CRetry["重试策略"] --> CHand["RPC 调用"]
    end

    subgraph ServerChain["服务端拦截器链"]
        SHand["RPC Handler"] --> SMetric["指标采集"] --> SLog["日志记录"] --> SAuth["鉴权拦截"]
    end

    CHand ==>|HTTP/2| SHand

    style CAuth fill:#fce4ec
    style SAuth fill:#e3f2fd

典型执行顺序

位置 拦截器 职责
客户端 Metrics 记录请求耗时、成功/失败计数
客户端 Retry 根据错误码决定是否自动重试
客户端 Auth 注入 Token / mTLS 证书
服务端 Auth 验证 Token 有效性
服务端 Logging 记录完整的请求/响应元信息
服务端 Metrics 统计服务端处理时间
服务端 Handler 实际业务逻辑

[!warning] Go 中拦截器注册的陷阱

Go 的 grpc.WithUnaryInterceptor 会覆盖而非追加。如果注册多次,只有最后一次生效。所以要用一个统一的包装函数串联所有逻辑:

链式包装函数

func chainUnaryInterceptors(interceptors ...grpc.UnaryServerInterceptor) grpc.UnaryServerInterceptor {
    n := len(interceptors)
    return func(ctx context.Context, req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (any, error) {
        ch := handler
        for i := n - 1; i >= 0; i-- {
            ih := ch
            ch = func(c context.Context, r any) (any, error) {
                return interceptors[i](c, r, info, ih)
            }
        }
        return ch(ctx, req)
    }
}

调用方只需传入所有拦截器即可:

allInterceptors := []grpc.UnaryServerInterceptor{
    TraceInterceptor(),
    AuthInterceptor(),
    LoggingInterceptor(),
    MetricsInterceptor(),
}

server := grpc.NewServer(
    grpc.UnaryInterceptor(chainUnaryInterceptors(allInterceptors...)),
    grpc.StreamInterceptor(chainStreamInterceptors(...)),
)

[!tip] 其他语言的差异

  • Go:需要通过上述手动链式包装,因为 NewServer 只接受一个拦截器
  • Java:Server.intercept() 支持注册多个拦截器,按注册顺序依次执行
  • Node.js:通过插件体系 Server.addServiceDefinition 间接实现

实战:统一鉴权拦截器

// auth interceptor
func AuthInterceptor() grpc.UnaryServerInterceptor {
    return func(ctx context.Context, req any,
        info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (any, error) {

        // 白名单路径跳过鉴权
        skipPaths := []string{"/health.Health/Check", "/grpc.reflection.v1.ServerReflection/ServerReflectionInfo"}
        for _, p := range skipPaths {
            if info.FullMethod == p {
                return handler(ctx, req)
            }
        }

        // 从 metadata 中提取 Token
        md, ok := metadata.FromIncomingContext(ctx)
        if !ok {
            return nil, status.Error(codes.Unauthenticated, "missing metadata")
        }

        tokens := md.Get("authorization")
        if len(tokens) == 0 || !validateToken(tokens[0]) {
            return nil, status.Error(codes.Unauthenticated, "invalid token")
        }

        // 将用户信息注入 ctx(传递给下游 handler)
        userCtx := context.WithValue(ctx, "userID", extractUserID(tokens[0]))
        return handler(userCtx, req)
    }
}

上下文传播 (Context Propagation)

gRPC 天然支持通过 metadata 传递自定义元数据——这是实现分布式追踪、链路溯源的核心机制。

手动传播

// 服务端注入 trace ID
ctx = metadata.AppendToOutgoingContext(ctx,
    "x-trace-id", traceID,
    "x-user-id", userID,
)

// 客户端接收
md, ok := metadata.FromOutgoingContext(ctx)
traceID := md.Get("x-trace-id")

OpenTelemetry 自动传播

手动维护 metadata 繁琐且易遗漏。使用 otelgrpc 中间件可以自动注入 W3C Trace Context:

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

server := grpc.NewServer(
    grpc.StatsHandler(otelgrpc.NewServerStatsHandler()),
    // otelgrpc 自动在 metadata 中注入 traceparent / tracestate
)

conn, _ := grpc.Dial(target,
    grpc.WithStatsHandler(otelgrpc.NewClientStatsHandler()),
)

[!keypoint] 为什么需要上下文传播

当一个请求穿越 5 个微服务时,如果没有上下文传播,你在日志系统中看到的是 5 条孤立的记录。有了 W3C Trace Context,每层服务自动将 traceparent 透传给下一跳——最终汇聚成一条完整的调用链路,这就是 02-服务治理/03-分布式追踪 的核心能力。

拦截器最佳实践

[!summary] 五条黄金法则

  1. 不要吞掉错误——拦截器应该记录错误再转发给下一个,而不是独自决定忽略
  2. 不要在拦截器里做重型计算——它处于热路径,每条 RPC 都要经过
  3. 设置合理的超时——拦截器里的 context.WithTimeout 不应短于原始请求的 deadline
  4. 白名单排除健康检查——/health.Health/Check 和 reflection 服务不需要鉴权和日志
  5. 用 info.FullMethod 做条件判断——格式为 /package.Service/Method,如 /order.v1.OrderService/CreateOrder

关联笔记