
Go 微服务里怎么用 Header 做 RPC 路由分发
不能直接靠 net/rpc 或 gRPC 自带机制实现——它们不解析 HTTP Header,也不支持运行时切换后端。真要按 Authorization、X-Tenant-ID 或 X-Env 这类 Header 动态选服务实例,得自己在网关或 RPC 客户端层插一手。
为什么不能在 gRPC Server 端做 Header 判断
gRPC over HTTP/2 的 metadata 是从 context 里取的,不是传统 HTTP Header;而你传的 X-Env: staging 这种会被自动转成小写 key(x-env),且必须通过 grpc.Peer() 或拦截器显式提取。更关键的是:路由决策必须发生在请求发出前,不是等 server 收到再跳转。
- server 端做判断 = 已经建立连接、分配资源、执行逻辑,无法“重定向”到另一台实例
- Header 在 client 发起 RPC 时已封装进 metadata,server 拿到时路由早已固定
- 除非你用
grpc.RoundRobin配合自定义Resolver,否则负载均衡器根本看不到原始 HTTP Header
在 HTTP 网关层做 Header 路由最实际
把 gRPC 请求先走一层轻量 HTTP 网关(比如用 gin 或 echo),解析 Header 后拼出目标 gRPC 地址,再用 grpc.Dial 连过去。这是目前生产环境最可控的做法。
- 匹配
X-Tenant-ID: tenant-a→ dialtenant-a-svc:9000 - 匹配
X-Env: prod→ dialsvc-prod.cluster.local:9000 - 务必校验 Header 值合法性,避免注入(如值含
..、@、端口冒号) - 注意 DNS 解析延迟:别每次请求都
net.LookupHost,缓存解析结果 + TTL 刷新
func routeByHeader(c *gin.Context) {
tenant := c.GetHeader("X-Tenant-ID")
target := tenantToAddr[tenant]
if target == "" {
c.AbortWithStatus(400)
return
}
conn, _ := grpc.Dial(target, grpc.WithTransportCredentials(insecure.NewCredentials()))
defer conn.Close()
// ... 转发逻辑
}
客户端 SDK 里硬编码路由逻辑的风险点
如果不想加网关,也可以让每个业务 client 自己读 context 里的 Header(比如从 HTTP handler 透传下来),再决定 dial 哪个地址。但容易踩三个坑:
立即学习“go语言免费学习笔记(深入)”;
- 所有调用点都要重复写路由逻辑,没法统一灰度或降级
-
grpc.Dial是重量级操作,不能每请求都 new 一个 conn;得配合sync.Pool或连接池管理 - Header 来源不可信(比如前端伪造),必须和服务注册中心联动验证 tenant 是否真实存在,否则 dial 失败会拖慢整个链路
真正难的不是“怎么写 if 判断”,而是怎么让路由规则可配置、可观测、可回滚。Header 值一变,背后可能是整套服务拓扑切换,没配套的实例健康检查和 fallback 策略,很容易雪崩。











