4. gRPC拦截器实战

gRPC 拦截器(Interceptor)是框架提供的一种 AOP(面向切面编程)机制,允许开发者在 RPC 调用链中插入自定义逻辑,而无需修改业务代码。本章将分别讲解服务端拦截器(日志、鉴权、限流)与客户端拦截器(超时、重试、熔断)的编写与注册,并给出完整的工程案例。

核心概念:拦截器分为两类——一元拦截器(Unary Interceptor) 处理普通 RPC 调用,流式拦截器(Stream Interceptor) 处理流式 RPC。本章以一元拦截器为主,流式拦截器原理类似。
gRPC 拦截器调用链全景 gRPC 拦截器调用链全景 客户端拦截器链 超时拦截器 重试拦截器 熔断拦截器 gRPC 调用 网络 服务端拦截器链 日志拦截器 鉴权拦截器 限流拦截器 业务 Handler 执行顺序 客户端:超时 → 重试 → 熔断 服务端:日志 → 鉴权 → 限流 → 业务 Handler

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
}
生产建议:日志拦截器中避免打印敏感字段(如密码、Token),可使用 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)
}
注意:限流拦截器应放在鉴权拦截器之后,避免对未认证请求也消耗令牌;同时建议对不同的 RPC 方法设置不同的限流阈值,可通过 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
    }
}
熔断器配置建议:半开状态探测间隔 5 秒,失败阈值 5 次,超时时间 3 秒。熔断器状态变化应输出日志,便于排查。

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. 熔断
    ),
)
客户端拦截器执行流程 客户端拦截器执行流程(超时 → 重试 → 熔断) 发起 RPC 调用 超时拦截器 重试拦截器 熔断拦截器 熔断器打开:快速失败 熔断器关闭:执行实际调用

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 控制是否开启、阈值参数),避免硬编码。