Files
cs-note/hhs/MS/06-gRPC/04-拦截器.md
T

177 lines
6.2 KiB
Markdown
Raw Normal View History

2026-05-24 11:42:38 +08:00
---
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-安全机制]] — 鉴权拦截器是安全机制在代码层的落地形式