consul注册时必须在agentserviceregistration中显式设置meta字段(如weight="10"、version="v2"),否则灰度标签和权重无处落脚;tags仅适用于开关型灰度,而meta是加权路由基础,需数字字符串且同一服务不同版本须用不同id以防元数据覆盖。

Consul 注册时必须带灰度元数据,否则权重/标签无处落脚
单纯在 Consul UI 里手动加 Meta 或改 Tags 没用——Gin 服务启动时若没主动上报,Consul 存储的实例元信息就是空的。注册代码里漏掉 Meta 字段,后续所有灰度逻辑都会失效。
正确做法是在 consulapi.AgentServiceRegistration 中显式填入灰度标识:
registration := &consulapi.AgentServiceRegistration{
ID: "user-service-v2-01",
Name: "user-service",
Port: 8080,
Tags: []string{"gray"}, // Label 方式:全量切流
Meta: map[string]string{
"weight": "10", // Weight 方式:10% 流量进 v2
"version": "v2",
},
Check: &consulapi.AgentServiceCheck{
HTTP: "http://localhost:8080/health",
Timeout: "5s",
Interval: "10s",
},
}
-
Tags适合做「开关型」灰度(比如只对内部测试账号开放) -
Meta里的weight是加权路由的基础,必须是数字字符串,不能是"10%"或10(int) - 同一服务不同版本建议用不同
ID,避免 Consul 认为是实例重启覆盖旧元数据
Gin 中间件读 Consul 实例列表时,要过滤健康 + 解析 Meta
调用 client.Health().Service() 拿到的是原始服务节点列表,里面混着不健康、未通过检查的实例。直接拿去路由会把请求打到挂掉的 v2 实例上。
关键步骤是两层过滤:
- 先用
passingOnly=true参数只取健康实例:client.Health().Service("user-service", "", true, nil) - 再遍历每个
ServiceEntry的Service.Meta字段,提取weight和version
常见错误是忽略健康状态判断,或把 Meta 当成顶层字段直接读——它实际嵌套在 entry.Service.Meta 里。
按权重选实例不能靠简单随机,得用加权轮询或区间匹配
用 rand.Intn(100) 然后 if-else 判断 0–9 走 v2、10–99 走 v1,短期看没问题,但流量分布偏差大,尤其当权重不是整百数时(比如 v2 权重 7,v1 权重 93)。
更稳的做法是预计算权重区间,用一次随机命中:
// 假设拿到实例列表:[{v1, weight:"93"}, {v2, weight:"7"}]
total := 0
for _, inst := range instances {
w, _ := strconv.Atoi(inst.Service.Meta["weight"])
total += w
}
r := rand.Intn(total)
cursor := 0
for _, inst := range instances {
w, _ := strconv.Atoi(inst.Service.Meta["weight"])
if r
- 每次请求都重新算 total + 遍历,性能敏感场景建议缓存实例列表+权重快照,定时刷新(比如 30s)
- 别在 HTTP handler 里同步调用 Consul API——超时会拖慢整个请求,应提前拉取并本地缓存
- 如果用 gRPC,可把这层逻辑封装进自定义
balancer.Builder,比 Gin 中间件更透明
灰度标识从哪来?Header/Cookie/Query 都要统一提取入口
Consul 只管服务端注册和发现,但“谁该走灰度”这个决策点必须在 Gin 入口处收口。硬编码在中间件里写 c.GetHeader("X-Release") == "gray" 是最常见也最脆弱的做法。
实际要支持多维度识别:
- 按 Header:
X-Release: gray(适合压测或内部人员) - 按 Cookie:
user_id=abc123→ 取 hash % 100 - 按 Query:
?canary=1(适合运营临时开灰度链接)
这些逻辑必须收敛在一个函数里,比如 shouldRouteToGray(c *gin.Context) bool,避免多个中间件重复解析。否则某天加了个新灰度策略,漏改一个地方就导致部分流量错乱。
真正容易被忽略的是:灰度标识一旦确定,要透传给下游 gRPC 调用。Gin handler 里用 metadata.MD{...} 把 canary: true 塞进 context,下游服务才能继续做二级灰度路由——否则灰度只停留在网关层,服务间调用还是走默认策略。











