4. gRPC拦截器实战
gRPC 拦截器(Interceptor)是框架提供的一种 AOP(面向切面编程)机制,允许开发者在 RPC 调用链中插入自定义逻辑,而无需修改业务代码。本章将分别讲解服务端拦截器(日志、鉴权、限流)与客户端拦截器(超时、重试、熔断)的编写与注册,并给出完整的工程案例。
4.1 服务端拦截器
服务端拦截器在请求到达业务 Handler 之前执行,常用于横切关注点。Go 中通过 grpc.UnaryServerInterceptor 类型定义一元拦截器,签名如下:
type UnaryServerInterceptor func(
ctx context.Context,
req interface{},
info *UnaryServerInfo,
handler UnaryHandler,
) (resp interface{}, err error)
4.1.1 日志拦截器
记录每个 RPC 调用的方法名、耗时、请求大小和错误信息,是线上排查问题的第一道防线。
func LoggingInterceptor(ctx context.Context, req interface{},
info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
start := time.Now()
// 记录请求入参(注意脱敏)
log.Printf("[gRPC] 请求方法: %s, 请求体: %+v", info.FullMethod, req)
resp, err := handler(ctx, req)
duration := time.Since(start)
if err != nil {
log.Printf("[gRPC] 失败 方法: %s, 耗时: %v, 错误: %v", info.FullMethod, duration, err)
} else {
log.Printf("[gRPC] 成功 方法: %s, 耗时: %v", info.FullMethod, duration)
}
return resp, err
}
proto.MarshalJSON 后做字段脱敏处理;同时建议使用结构化日志库(如 zap)替代标准 log 包。
4.1.2 鉴权拦截器
从 gRPC 的 metadata 中提取 Token,验证身份合法性。未通过鉴权的请求直接返回 codes.Unauthenticated 错误。
func AuthInterceptor(ctx context.Context, req interface{},
info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
// 从 metadata 中提取 token
md, ok := metadata.FromIncomingContext(ctx)
if !ok {
return nil, status.Errorf(codes.Unauthenticated, "缺少 metadata")
}
tokens := md.Get("authorization")
if len(tokens) == 0 {
return nil, status.Errorf(codes.Unauthenticated, "缺少 authorization token")
}
token := tokens[0]
// 验证 token(示例:简单校验,生产应接入 JWT/OAuth2)
if token != "valid-token" {
return nil, status.Errorf(codes.Unauthenticated, "无效 token")
}
// 将用户信息注入 context,供后续业务使用
newCtx := context.WithValue(ctx, "user_id", "user-123")
return handler(newCtx, req)
}
4.1.3 限流拦截器
基于令牌桶或漏桶算法,控制服务端并发请求量,防止突发流量打垮服务。以下使用 Go 标准库 golang.org/x/time/rate 实现:
import "golang.org/x/time/rate"
var limiter = rate.NewLimiter(rate.Limit(100), 200) // 每秒 100 个请求,桶容量 200
func RateLimitInterceptor(ctx context.Context, req interface{},
info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
if !limiter.Allow() {
return nil, status.Errorf(codes.ResourceExhausted, "请求被限流,请稍后重试")
}
return handler(ctx, req)
}
info.FullMethod 区分。
4.2 服务端拦截器注册
使用 grpc.NewServer 时通过 grpc.ChainUnaryInterceptor 注册多个拦截器,执行顺序为注册顺序:
server := grpc.NewServer(
grpc.ChainUnaryInterceptor(
LoggingInterceptor, // 1. 日志
AuthInterceptor, // 2. 鉴权
RateLimitInterceptor, // 3. 限流
),
)
// 注册业务服务
pb.RegisterUserServiceServer(server, &UserServiceServer{})
| 拦截器 | 执行时机 | 失败处理 | 典型错误码 |
|---|---|---|---|
| 日志拦截器 | 最先执行 | 仅记录,不阻断 | 无 |
| 鉴权拦截器 | 日志之后 | 直接返回错误 | Unauthenticated |
| 限流拦截器 | 鉴权之后 | 直接返回错误 | ResourceExhausted |
4.3 客户端拦截器
客户端拦截器在发起 RPC 调用前/后执行,用于增强调用的可靠性。Go 中通过 grpc.UnaryClientInterceptor 定义:
type UnaryClientInterceptor func(
ctx context.Context,
method string,
req, reply interface{},
cc *ClientConn,
invoker UnaryInvoker,
opts ...CallOption,
) error
4.3.1 超时拦截器
为每个 RPC 调用设置默认超时时间,防止调用方无限等待。注意:如果调用方已设置 context deadline,则不应覆盖。
func TimeoutInterceptor(timeout time.Duration) grpc.UnaryClientInterceptor {
return func(ctx context.Context, method string, req, reply interface{},
cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error {
// 如果 context 没有 deadline,则设置默认超时
if _, ok := ctx.Deadline(); !ok {
var cancel context.CancelFunc
ctx, cancel = context.WithTimeout(ctx, timeout)
defer cancel()
}
return invoker(ctx, method, req, reply, cc, opts...)
}
}
4.3.2 重试拦截器
当 RPC 调用失败时,根据错误类型自动重试。通常只对幂等操作(如查询)进行重试,写操作需谨慎。
func RetryInterceptor(maxRetries int) grpc.UnaryClientInterceptor {
return func(ctx context.Context, method string, req, reply interface{},
cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error {
var err error
for i := 0; i <= maxRetries; i++ {
if i > 0 {
// 指数退避等待
time.Sleep(time.Duration(100*(1<<i)) * time.Millisecond)
}
err = invoker(ctx, method, req, reply, cc, opts...)
if err == nil {
return nil
}
// 只对可重试错误进行重试
if !isRetryable(err) {
return err
}
}
return err
}
}
func isRetryable(err error) bool {
st, ok := status.FromError(err)
if !ok {
return false
}
// 常见可重试错误码
switch st.Code() {
case codes.Unavailable, codes.DeadlineExceeded, codes.ResourceExhausted:
return true
}
return false
}
4.3.3 熔断拦截器
熔断器(Circuit Breaker)在连续失败达到阈值后,快速失败后续请求,避免级联故障。可使用第三方库 github.com/sony/gobreaker 实现:
import "github.com/sony/gobreaker"
func CircuitBreakerInterceptor(cb *gobreaker.CircuitBreaker) grpc.UnaryClientInterceptor {
return func(ctx context.Context, method string, req, reply interface{},
cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error {
_, err := cb.Execute(func() (interface{}, error) {
return nil, invoker(ctx, method, req, reply, cc, opts...)
})
return err
}
}
4.4 客户端拦截器注册
创建 gRPC 连接时通过 grpc.WithUnaryInterceptor 或 grpc.WithChainUnaryInterceptor 注册:
cb := gobreaker.NewCircuitBreaker(gobreaker.Settings{
Name: "user-service",
MaxRequests: 3,
Interval: 5 * time.Second,
Timeout: 3 * time.Second,
ReadyToTrip: func(counts gobreaker.Counts) bool {
return counts.ConsecutiveFailures > 5
},
})
conn, err := grpc.Dial(
"localhost:50051",
grpc.WithInsecure(),
grpc.WithChainUnaryInterceptor(
TimeoutInterceptor(5*time.Second), // 1. 超时
RetryInterceptor(2), // 2. 重试
CircuitBreakerInterceptor(cb), // 3. 熔断
),
)
4.5 拦截器组合策略与避坑
| 场景 | 推荐组合 | 注意事项 |
|---|---|---|
| 高并发读服务 | 超时 + 重试 + 熔断 | 重试次数不超过 2 次,避免雪崩 |
| 写服务(订单/支付) | 超时 + 熔断(不重试) | 写操作重试可能导致重复提交,需幂等设计 |
| 内部微服务调用 | 超时 + 重试 + 熔断 + 日志 | 日志采样率建议 1%,避免日志爆炸 |
| 对外 API 网关 | 鉴权 + 限流 + 日志 | 限流阈值需根据后端容量动态调整 |
- 拦截器顺序错误:限流放在鉴权之前,导致未认证请求消耗令牌。
- 重试放大:多个客户端同时重试,导致服务端瞬间流量暴增。建议使用 jitter(随机抖动)分散重试时间。
- 熔断器未重置:熔断器半开状态探测间隔过短,导致服务刚恢复又被熔断。建议间隔至少 5 秒。
- context 传递丢失:在拦截器中创建新的 context 时,忘记携带原有的 metadata 或 deadline。
4.6 完整案例:服务端 + 客户端拦截器集成
以下展示一个完整的 gRPC 服务端与客户端拦截器集成示例,包含所有上述拦截器的注册与使用:
// ========== 服务端 main.go ==========
func main() {
lis, _ := net.Listen("tcp", ":50051")
server := grpc.NewServer(
grpc.ChainUnaryInterceptor(
LoggingInterceptor,
AuthInterceptor,
RateLimitInterceptor,
),
)
pb.RegisterUserServiceServer(server, &UserServiceServer{})
server.Serve(lis)
}
// ========== 客户端 main.go ==========
func main() {
cb := gobreaker.NewCircuitBreaker(gobreaker.Settings{
Name: "user-service",
Timeout: 3 * time.Second,
})
conn, _ := grpc.Dial(
"localhost:50051",
grpc.WithInsecure(),
grpc.WithChainUnaryInterceptor(
TimeoutInterceptor(5*time.Second),
RetryInterceptor(2),
CircuitBreakerInterceptor(cb),
),
)
defer conn.Close()
client := pb.NewUserServiceClient(conn)
// 发起调用
resp, err := client.GetUser(context.Background(), &pb.GetUserRequest{Id: "123"})
}
- 服务端拦截器执行顺序 = 注册顺序(日志 → 鉴权 → 限流 → Handler)。
- 客户端拦截器执行顺序 = 注册顺序(超时 → 重试 → 熔断 → 网络调用)。
- 拦截器内尽量使用
status.Errorf返回标准 gRPC 错误码,便于客户端识别错误类型。 - 生产环境建议将拦截器配置化(如通过 YAML 控制是否开启、阈值参数),避免硬编码。