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

177 lines
6.2 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
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-安全机制]] — 鉴权拦截器是安全机制在代码层的落地形式