灰度中间件必须置于所有中间件最前面,否则下游无法感知分流意图;需用c.request.withcontext()注入context,支持header/cookie/query标识,规则应预加载至内存,http/grpc调用须显式透传,日志/metrics/trace需统一打标。

灰度中间件必须放在所有中间件链最前面
顺序错了,灰度就失效。比如日志、鉴权、限流中间件在前,它们拿到的 c.Request.Context() 里还没有灰度标识,后续逻辑就完全感知不到分流意图。
正确做法是:在 r.Use() 调用序列的第一位注册灰度中间件,且必须用 c.Request.WithContext() 注入新 context,不能只改 c.Keys 或局部变量。
- 错误示范:
r.Use(loggingMiddleware); r.Use(grayMiddleware)—— 日志里永远记录的是默认版本行为 - 正确写法:
r.Use(grayMiddleware); r.Use(authMiddleware); r.Use(loggingMiddleware) - Gin 用户注意:
c.GetString("gray_version")只在显式存入c.Set()后才可用;更推荐用context.WithValue(c.Request.Context(), grayKey{}, "v2"),避免 key 冲突
分流逻辑不能塞进 handler 或业务函数里
把 if c.GetHeader("X-Release-Phase") == "canary" 写在某个 HandleFunc 里,等于放弃整条请求链路的灰度一致性——下游服务、DB 连接池、缓存 client 都已按默认配置初始化完毕,再切版本毫无意义。
真正该做的事,是在灰度中间件里完成三件事:提取标识 → 查规则 → 存目标版本到 context(如 ctx.Value("target_service").(string))。
- 支持的标识源:Header(
X-Release-Stage)、Cookie(http.Request.Cookie("gray"),别手动解析Cookieheader)、Query(c.Query("version"),仅限测试) - 规则查本地内存,别每次调 Redis/etcd:启动时加载 JSON 配置,或监听 etcd 变更后更新
sync.Map - 下游 HTTP 调用前,必须显式设置头:
req.Header.Set("X-Gray-Version", version);net/http 不会自动透传 context 值
Cookie 作为灰度载体要格外小心
Cookie 易被 CDN、Nginx、浏览器 SameSite 策略拦截或删减,移动端 App 请求甚至根本不带 Cookie。若必须用,核心是「早解析、早透传、不依赖下游反向解析」。
常见失败现象:用户首次访问带 gray=on,第二次却走默认路由;gRPC 下游收不到灰度标识;日志里看到 Header 有值但 Cookie 值为空。
- 必须在最外层中间件用
c.Request.Cookie("gray")获取,而非c.Request.Header.Get("Cookie") - 提取后立即存入 context,key 类型别用字符串(推荐自定义空 struct:
type grayKey struct{}) - 下游是 gRPC 时,需用
metadata.MD{"canary-version": []string{grayID}}封装,并通过grpc.Dial(..., grpc.WithPerRPCCredentials())透传
灰度不是 Gin 的功能,而是你对请求生命周期的理解深度
框架只提供钩子,不提供决策。Gin 的 gin.Context 是一次性的,c.Next() 执行后,中间件就退出了;你在里面调 http.Redirect 或重复调 next.ServeHTTP,会导致 response header 丢失、body 已读取、defer 逻辑错乱。
真正的难点不在代码怎么写,而在于:谁负责解析标识?谁决定目标服务?谁保证下游也收到?谁兜底没匹配到规则的情况?这些必须在中间件链首统一收口,而不是靠每个 handler 自行判断。
最容易被忽略的一点:灰度标识注入后,必须贯穿整个链路打标——日志字段、metrics label、trace tag 都得带上 gray_version,否则出了问题根本没法定位是哪个版本出的错。











