6.1 KiB
6.1 KiB
tags, create time
| tags | create time | |||||
|---|---|---|---|---|---|---|
|
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-服务治理/分布式追踪 的核心能力。
拦截器最佳实践
[!summary] 五条黄金法则
- 不要吞掉错误——拦截器应该记录错误再转发给下一个,而不是独自决定忽略
- 不要在拦截器里做重型计算——它处于热路径,每条 RPC 都要经过
- 设置合理的超时——拦截器里的
context.WithTimeout不应短于原始请求的 deadline- 白名单排除健康检查——
/health.Health/Check和 reflection 服务不需要鉴权和日志- 用
info.FullMethod做条件判断——格式为/package.Service/Method,如/order.v1.OrderService/CreateOrder
关联笔记
- 01-协议与架构 — 拦截器位于 gRPC Framework 层,在 HTTP/2 之前处理请求
- 05-错误处理 — 拦截器经常需要根据错误码决定重试或降级策略
- 02-服务治理/分布式追踪 — Context 传播是实现分布式追踪的前提
- 02-服务治理/安全机制 — 鉴权拦截器是安全机制在代码层的落地形式