什么是gRPC Metadata,使用场景是什么 | 拦截器 与 Metadata

文章目录

什么是gRPC Metadata,使用场景是什么

什么是gRPC Metadata

gRPC Metadata (元数据) 是 gRPC 中用于在客户端和服务器之间传输请求本身之外的附加信息的一种机制。

通俗地说,你可以把它理解为 HTTP 请求中的 Header(请求头)。

在 gRPC 调用中,主要的数据(如参数、返回值)是通过 Protocol Buffers 序列化后传输的,而 Metadata 则是以 Key-Value 对的形式,伴随着这些主数据一起传输,用于携带鉴权令牌、追踪 ID、超时设置等"控制面"信息。

概念 HTTP (REST) gRPC
载体 HTTP Headers Metadata
传输内容 Cookie, User-Agent, Auth Token Auth Token, Request ID, Trace ID
Key 格式 Content-Type (不区分大小写) content-type (必须小写)
获取方式 req.Header.Get("key") metadata.FromIncomingContext(ctx)
发送方式 req.Header.Set("key", "val") metadata.NewOutgoingContext(ctx, md)

metadata是以key-value的形式存储数据的,其中key是string类型,而value是[]string,即一个字符串数组类型。metadata使得client和server能够为对方提供关于本次调用的一些信息,就像一次http请求的RequestHeader和ResponseHeader一样。http中header的生命周周期是一次http请求,那么metadata的生命周期就是一次RPC调用。'
分布式追踪的隐形骨架:gRPC元数据(Metadata)

使用场景

Metadata 主要用于处理那些不属于业务逻辑,但对系统运行至关重要的信息。以下是 4 个最典型的使用场景:

  1. 身份验证 (Authentication)
    这是最常见的用途。就像在 HTTP API 中在 Header 里放 Authorization: Bearer 一样。

场景:客户端调用服务时,需要证明自己是谁。

做法:客户端在 Metadata 中放入 authorization 字段(存放 JWT Token 或 API Key)。服务端拦截器读取这个字段进行验签。

  1. 全链路追踪 (Distributed Tracing)
    这就是你刚才那段代码的场景。

场景:在微服务架构中,一个请求可能经过 A -> B -> C -> D 四个服务。如果报错了,我们需要知道这一串请求的完整路径。

做法:生成一个唯一的 request_id 或 trace_id,通过 Metadata 在服务之间层层传递。每个服务打日志时都带上这个 ID,排查问题时只需搜这个 ID 就能看到全链路日志。

  1. 负载均衡与路由 (Load Balancing & Routing)

    场景:灰度发布(金丝雀发布)或多租户系统。

    做法:

    灰度发布:在 Metadata 中添加 version: canary。网关或负载均衡器看到这个标记,就将请求转发到新版本的服务器,否则转发到旧版本。

    多租户:添加 tenant_id: 1001,数据库中间件据此决定连接哪个租户的数据库。

  2. 超时与取消控制 (Deadline & Cancellation)

    虽然 gRPC Context 本身支持 Deadline,但有时我们需要传递自定义的控制信息。

场景:客户端告诉服务端,"这个操作如果超过 500ms 还没处理完,你就别处理了,我不需要了"。

做法:gRPC 底层其实就是通过 Metadata 传输 grpc-timeout 字段来实现超时控制的。

  • 认证: 可以在 Metadata 中携带认证令牌(如 JWT 或 OAuth2 凭证)。
  • 追踪: 用于在分布式系统中追踪请求的调用链,例如传递 traceId。
  • 自定义头部信息: 可以用来传输应用程序特定的信息,例如负载均衡、速率限制或自定义错误信息。
  • 控制消息格式: 可以在 Metadata 中指示消息是否需要压缩或加密等。

go中使用metadata

go 复制代码
// Metadata(元数据)本质是键值对集合,类似HTTP Headers
// 在gRPC中通过context.Context传递,不体现在protobuf定义中
md := metadata.Pairs(
  "x-request-id", "req-123456",
  "x-trace-id", "trace-7890ab",
)
ctx := metadata.NewOutgoingContext(context.Background(), md)

(1)创建metadata

MD 类型实际上是map,key是string,value是string类型的slice。

bash 复制代码
type MD map[string][]string
go 复制代码
//第一种方式
md := metadata.New(map[string]string{"key1": "val1", "key2": "val2"})
//第二种方式 key不区分大小写,会被统一转成小写。
md := metadata.Pairs(
    "key1", "val1",
    "key1", "val1-2", // "key1" will have map value []string{"val1", "val1-2"}
    "key2", "val2"}

(2)发送metadata

go 复制代码
md := metadata.Pairs("key", "val")

// 新建一个有 metadata 的 context
ctx := metadata.NewOutgoingContext(context.Background(), md)

// 单向 RPC
response, err := client.SomeRPC(ctx, someRequest)

(3)接收metadata

go 复制代码
func (s *server) SomeRPC(ctx context.Context, in *pb.SomeRequest) (*pb.SomeResponse, err) {
    md, ok := metadata.FromIncomingContext(ctx)
    // do something with metadata
}

拦截器 与 Metadata

根据RPC调用类型可以将gRPC拦截器分为两种:

  • 一元拦 截器(Unary Interceptor) :拦截和处理一元RPC调用。
  • 流拦截器(Stream Interceptor) :拦截和处理流式RPC调用。

拦截器(或过滤器/中间件)的作用是将这种"横切关注点"与"业务逻辑"解耦。

如果没有拦截器,你的代码会变成这样:

go 复制代码
// ❌ 反面教材:手动传递,代码侵入性极强,容易漏传
public void createOrder(Request req) {
    String reqId = req.getHeader("x-request-id"); // 1. 手动获取
    
    HttpHeaders headers = new HttpHeaders();
    headers.add("x-request-id", reqId); // 2. 手动设置
    
    paymentClient.pay(headers, ...); // 3. 修改接口签名以支持传 Header
}

Go: 使用 context.Context,这是 Go 语言天然的优势,Header 随 ctx 传递。

Header 随 ctx 传递 那我还需要操作metadata吗

很多开发者误以为把数据 context.WithValue 存进去,下游微服务就能收到了,这是错误的。

  • context.WithValue (内存):

    作用:在同一个进程内的函数调用链之间传递数据(例如:从 Controller -> Service -> DAO)。

    限制:这些数据完全不会被序列化,也不会自动发送给下游服务。它只存在于当前服务的内存堆栈中。

  • metadata / Header (网络):

    作用:通过网络协议(HTTP/gRPC)携带数据。

    机制:必须显式地将数据从 Context 中"取出来",放到网络请求的"载体"里。

错误的写法 (下游收不到)

go 复制代码
// ❌ 仅使用 WithValue
func (s *Service) CallDownstream(ctx context.Context) {
    // 你以为这样下游就能收到,其实下游收不到
    // 这只是把 key-value 存到了当前内存的 ctx 对象里
    ctx = context.WithValue(ctx, "x-request-id", "12345") 
    
    client.SomeMethod(ctx, &req) 
}

正确的写法 (必须操作 Metadata)
你需要使用 gRPC 的 metadata 包将数据"挂载"到 context 上,这样 gRPC 框架在发起网络请求时,才会去扫描并发送这些数据。

go 复制代码
import "google.golang.org/grpc/metadata"

// ✅ 正确:使用 metadata 包装
func (s *Service) CallDownstream(ctx context.Context) {
    // 1. 创建 metadata (本质是 map[string]string)
    md := metadata.Pairs(
        "x-request-id", "12345",
        "x-app-version", "v1.0",
    )
    
    // 2. 将 metadata 注入到 OutgoingContext 中
    // gRPC 客户端中间件会检查 OutgoingContext,发现有 metadata 才会通过网络发出去
    ctx = metadata.NewOutgoingContext(ctx, md)
    
    client.SomeMethod(ctx, &req)
}

最佳实践:封装在拦截器中

你不想在每个业务方法里都写一遍 metadata.NewOutgoingContext。这就是为什么我们之前谈到的 拦截器 (Interceptor) 如此重要。

拦截器帮你自动完成了这个"搬运"工作:

  1. Inbound Interceptor (入口):

    收到网络请求 -> 读取 Metadata/Header -> 写入 ctx.WithValue。

    目的:让你的业务代码可以用 ctx.Value("key") 方便地读取。

  2. Outbound Interceptor (出口):

    准备发起调用 -> 读取 ctx.Value -> 写入 Metadata/Header。

    目的:确保数据跨网络传给下一个服务。

总结

Context 是数据在内存中流转的载体。

Metadata/Header 是数据在网络上流转的载体。

**你需要操作 Metadata,作为这两个世界的桥梁。**如果不操作 Metadata,数据就死在当前服务的内存里了,传不出去。

grpc Metadata元数据命名规范

分类 命名格式 示例 作用范围 传输策略
标准字段 x-{key} x-request-id%%BR%%x-app-version 跨服务通用 透传 (必须在整个链路中保留)
业务字段 x-{service}-{key} x-payment-user-id%%BR%%x-order-channel 特定业务域 按需 (仅流向下游相关服务或消费后丢弃)
追踪字段 x-trace-{key} x-trace-id%%BR%%x-span-id 分布式追踪 透传 (由中间件/Mesh自动处理)
  • 全链路透传:对于 Request ID 和 App Version,必须全链路携带,这对于日志聚合和灰度发布至关重要。
  • 命名空间(Namespacing):使用 x-{service}- 前缀(如 x-payment-)是防止头部冲突的最佳手段。在微服务链路中,服务 A 和服务 B 可能会无意中使用相同的 Key(如 x-user-id),加上服务前缀能有效隔离上下文。

gRPC 元数据(Metadata)深入解析

原文链接:https://article.juejin.cn/post/7532307118671495210

客户端户端发送元数据

客户端通过 context.Context 机制发送元数据到服务端。这是 gRPC 中传递请求级别信息的标准方式。

核心概念:

  • 元数据必须在 RPC 调用之前附加到 context 中
  • 使用 metadata.NewOutgoingContext() 创建带有元数据的 context
  • 元数据键名不区分大小写,会自动转换为小写
  • 元数据键名不能以 grpc- 开头(保留给系统使用)

服务端接收元数据

服务端通过 metadata.FromIncomingContext() 从请求 context 中提取客户端发送的元数据。

相关推荐
空空kkk1 天前
SpringMVC——拦截器
java·数据库·spring·拦截器
我叫张小白。2 天前
Spring Boot拦截器详解:实现统一的JWT认证
java·spring boot·web·jwt·拦截器·interceptor
C++chaofan13 天前
基于session实现短信登录
java·spring boot·redis·mybatis·拦截器·session
一缕南风21 天前
Spring Boot 响应拦截器(Jackson)实现时间戳自动添加
java·spring boot·后端·拦截器
pan30350747923 天前
GRPC详解
微服务·grpc
optimistic_chen1 个月前
【Java EE进阶 --- SpringBoot】统一功能处理(拦截器)
spring boot·后端·java-ee·log4j·拦截器
Zz_waiting.2 个月前
Spring 统一功能处理 - 拦截器与适配器
java·spring·拦截器·适配器·dispatcher
神云瑟瑟2 个月前
spring boot拦截器获取requestBody的巨坑
java·spring boot·拦截器
GM_8283 个月前
【Go项目基建】GORM框架实现SQL校验拦截器(完整源码+详解)
sql·golang·拦截器·gorm·慢查询·持久层基建