灰度路由逻辑必须由http.Handler层接管,不能依赖后端服务判断;需通过自定义Handler在ServeHTTP入口按Header→Query→权重优先级分流,支持上下文透传、原子热更新与精准特征匹配。

灰度路由逻辑必须由 http.Handler 层接管,不能只靠后端服务判断
权重灰度本质是请求分流,如果把分流逻辑放在业务 handler 里(比如在某个 HandleFunc 中做 rand + 权重计算),会丢失中间件链路的统一控制权,也难以兼容 gRPC、WebSocket 等非 HTTP 场景。真正可控的方式是用自定义 http.Handler 包裹下游,例如:
type WeightedRouter struct {
routes []struct {
handler http.Handler
weight int // 0–100
}
}
func (r *WeightedRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
total := 0
for _, rt := range r.routes {
total += rt.weight
}
if total == 0 {
http.Error(w, "no active route", http.StatusServiceUnavailable)
return
}
randVal := rand.Intn(total)
sum := 0
for _, rt := range r.routes {
sum += rt.weight
if randVal < sum {
rt.handler.ServeHTTP(w, req)
return
}
}
}注意:这里用 rand.Intn 前必须调用 rand.Seed(time.Now().UnixNano()),否则所有进程启动后生成的随机序列完全一致——这是线上灰度失效最常见的坑。
weight 字段建议用整数百分比而非浮点,且需做归一化校验
用浮点数表示权重(如 0.3、0.7)看似直观,但会引入浮点误差和 JSON 解析歧义(Go 的 json.Unmarshal 对 float64 默认精度可能丢失)。更稳妥的做法是强制使用整数百分比(0–100),并在初始化时校验总和是否 ≤ 100:
- 若总和
- 若总和 > 100,直接 panic 或 log.Fatal —— 权重配置错误必须阻断启动,不能静默截断
- 避免用 map 存储路由,因为 map 遍历顺序不确定,会导致相同随机数下分流结果不一致
type RouteConfig struct {
Version string `json:"version"`
Weight int `json:"weight"` // 必须为 0–100 整数
Addr string `json:"addr"` // 如 "http://v1.svc:8080"
}
真实灰度必须支持请求级上下文透传,不能只依赖 IP 或 Header 静态匹配
纯权重轮询只是“粗粒度灰度”,实际中常需结合用户 ID、traceID、Header 标识做精准放量。这时要在 ServeHTTP 中提取关键字段,并允许路由规则动态生效:
- 优先检查
req.Header.Get("X-Gray-Id")是否命中白名单(如"user_123")→ 直接路由到灰度版 - 再检查
req.URL.Query().Get("debug") == "v2"→ 临时调试开关 - 最后才走权重随机分流
- 所有决策过程要写入
req.Context(),比如context.WithValue(req.Context(), grayKey, "v2"),供下游日志和 metric 区分
热更新权重配置时,必须用原子替换+双检锁,避免路由表瞬时错乱
通过文件监听或 etcd watch 更新权重后,不能直接修改原 routes 切片——Go 中切片是引用类型,正在处理的请求可能读到部分更新的中间状态。正确做法是:
- 新建完整
routes切片,填充好新配置 - 用
sync/atomic替换指向该切片的指针(例如atomic.StorePointer(&r.routesPtr, unsafe.Pointer(&newRoutes))) - 读取时用
atomic.LoadPointer并转回切片,确保看到的是一致快照 - 或者更简单:用
sync.RWMutex保护整个路由表,读多写少场景下性能影响极小
connection refused。
权重灰度不是加个随机数就完事,关键是把“谁来决定”“怎么生效”“出错了能否追溯”这三个环节钉死在 HTTP 入口层。配置热更新和上下文透传这两块,最容易在线上悄悄失效。


















