灰度路由必须在请求入口中间件完成,所有灰度标识须在最外层中间件统一提取并存入context,优先级为X-Internal-Gray-ID→X-Gray-ID→Cookie→URL参数,禁止字符串key、确保透传白名单及gRPC元数据显式传递。

灰度路由必须在请求入口中间件完成,不能拖到业务 handler 里判断——否则 DB 连接池、缓存 client、gRPC 客户端等组件已按默认配置初始化,灰度就失效了。
HTTP 中间件必须提前提取并注入 context
所有灰度标识(如 X-App-Version、X-Release-Stage、X-Gray-ID)必须在最外层中间件统一提取,立刻存入 context.Context,而不是让每个 handler 自己去 r.Header.Get()。
- 优先级顺序建议:先查
X-Internal-Gray-ID(可信头),再 fallback 到X-Gray-ID→Cookie→URL.Query().Get("beta") - 别用字符串当
context.WithValue的 key,定义为type grayKey string防止键名冲突 - 中间件必须在日志、panic 恢复等中间件之前注册,否则 context 丢失后无法追溯灰度上下文
- gorilla/mux 用户:把灰度逻辑放在
Subrouter().Use(),别用MatcherFunc——它不保证执行时机,且会干扰健康检查探针
Header 透传必须显式拷贝,net/http 不自动带
http.Request 的 header 是单向的:上游收到的 header 不会自动出现在下游请求中。现象是 A 收到 X-App-Version: v2,但调 B 时 B 的 r.Header.Get("X-App-Version") 返回空字符串——这不是 bug,是 Go 标准库的明确设计。
- 每次构造
http.Request前,必须手动设置:req.Header.Set(http.CanonicalHeaderKey("x-app-version"), version),确保大小写规范 - 别改
gin.Context.Request.Header或chi.Context里的原始 header——它只影响当前 request 生命周期,不会带到下游 - 若封装了通用 HTTP client 工具函数,参数里必须显式传入
version string,而不是从 context 隐式取(容易漏传或取错) - 透传时白名单过滤:只传
X-*类 header,避免意外透传Authorization或Cookie
gRPC 场景下 metadata 必须逐跳显式提取+附加
gRPC 的 metadata.MD 是单向、无状态的:client 发的元数据 server 能读,但 server 不会自动把它塞回 response 给下一级 client。A→B→C 链路中,B 若不主动提取 x-app-version 并在调 C 时重新注入,C 就收不到。
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
立即学习“go语言免费学习笔记(深入)”;
- server 端接收:
md, ok := metadata.FromIncomingContext(ctx),再用md.Get("x-app-version")取值 - client 端发送:
md := metadata.Pairs("x-app-version", version); ctx = metadata.NewOutgoingContext(ctx, md) - HTTP 到 gRPC 的桥接层(如网关)必须做 header → metadata 映射:
metadata.Pairs("x-app-version", r.Header.Get("X-App-Version")) - 别依赖服务名后缀(如
user-service-v2)做路由——注册中心通常不支持 service name 正则匹配,得靠标签路由
分流逻辑不能硬编码,必须可热更新
把 if r.Header.Get("X-App-Version") == "v2" 这类判断写死在代码里,等于把发布策略焊进二进制,每次调权都要发版重启。
- 权重计算必须用确定性算法,例如
hash(userID) % 100 ,确保同一用户始终命中同一版本 - 推荐轻量方案:本地 JSON 配置文件 +
fsnotify监听变更;或定时 HTTP 拉取配置(如 GET/api/gray/config),失败则沿用旧策略 - 规则匹配别用运行时反复编译正则,提前
var versionRe = regexp.MustCompile(`^v[1-9]\d*$`)存为包级变量 - 禁止在分流逻辑里查数据库或远程鉴权——超时或失败会拖垮整个请求;必要时用本地 LRU cache + 定期刷新
真正难的不是“怎么把 v2 流量导过去”,而是“怎么确保 v2 的每条 DB 查询、每个缓存 key、每次 gRPC 调用都带着 v2 上下文”。漏掉任何一环,灰度就只是个假象。


















