--- tags: [grpc, interceptor, middleware, context-propagation, metadata] create time: 2026-05-07 16:00 --- # Interceptor 与上下文传播 ## 概述 Interceptor 是 gRPC 的切面编程能力,相当于 Web 框架中的 Middleware。每个 RPC 调用都会经过 **Unary Interceptor** 或 **Stream Interceptor** 链——无论它是普通的一问一答还是长时间的双向流。 本文涵盖拦截器的编写模式、链式串联技巧,以及最重要的**上下文传播**机制。 ## Interceptor 链架构 ```mermaid 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` 会**覆盖**而非追加。如果注册多次,只有最后一次生效。所以要用一个统一的包装函数串联所有逻辑: ## 链式包装函数 ```go 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) } } ``` 调用方只需传入所有拦截器即可: ```go 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` 间接实现 ## 实战:统一鉴权拦截器 ```go // 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` 传递自定义元数据——这是实现分布式追踪、链路溯源的核心机制。 ### 手动传播 ```go // 服务端注入 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: ```go 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` ## 关联笔记 - [[01-协议与架构]] — 拦截器位于 gRPC Framework 层,在 HTTP/2 之前处理请求 - [[05-错误处理]] — 拦截器经常需要根据错误码决定重试或降级策略 - [[02-服务治理/03-分布式追踪]] — Context 传播是实现分布式追踪的前提 - [[02-服务治理/02-安全机制]] — 鉴权拦截器是安全机制在代码层的落地形式