流量染色与链路跟踪在Go微服务中相互独立:染色决定灰度路由(走哪条路),跟踪记录请求路径(走过哪些路);两者均需透传但机制不同,不可混用X-Gray-ID与Traceparent,HTTP/gRPC需分别显式提取、注入与传递。

HTTP 中间件必须每层重新提取 X-Gray-ID
常见错误是网关解析一次 X-Gray-ID 写进 context.WithValue,后续 handler 直接 ctx.Value(grayKey) 取值——这会失效。任何中间件(日志、panic 恢复、auth)都可能新建 context,覆盖上游值。
- 所有 HTTP handler 前必须加统一中间件,调用
r.Header.Get("X-Gray-ID")显式提取 - 立即用
context.WithValue(ctx, grayKey, val)构建新 context,不信任上游传来的 ctx - 若用 gin,推荐
c.Set("gray-id", val)+c.MustGet("gray-id").(string),比原生 context 更可控 - key 必须类型安全:定义
type grayCtxKey string,避免字符串字面量冲突
下游 HTTP 请求必须手动 set X-Gray-ID
net/http 不会自动把 incoming headers 复制到 outgoing request,这是设计使然,不是 bug。A 服务收到带 X-Gray-ID: v2 的请求,调 B 时 B 收不到,90% 是因为没手动设头。
- 在
http.DefaultClient.Do(req)之前,且不能晚于req = req.WithContext() - 必须写
req.Header.Set("X-Gray-ID", grayID),注意大小写:是X-Gray-ID,不是x-gray-id或X_gray_ID - 别改
gin.Context.Request.Header,它只影响当前 request 生命周期,不会带到下游 - 封装通用 client 工具函数时,参数必须显式接收
grayID string,禁止隐式从 context 取
gRPC 必须用 metadata.MD 显式读写
context.WithValue 里的灰度字段不会自动变成 wire 上的 metadata,server 端也收不到。更关键的是,metadata 不会自动向下一级透传——A→B→C,B 不主动提取再注入,C 就收不到。
- client 发起调用前:构造
md := metadata.Pairs("x-gray-id", grayID),再用grpc.Header(&md)或metadata.NewOutgoingContext(ctx, md) - server 端拦截器中:用
metadata.FromIncomingContext(ctx)提取,再调md.Get("x-gray-id")(注意 key 是小写"x-gray-id") - B 调 C 时,必须重复提取 → 构造 md → 注入 outgoing context,不能依赖“上一层已传过”
- 别指望 gRPC 拦截器默认转发自定义 metadata;必须显式读、显式写
链路跟踪用 Traceparent,别和 X-Gray-ID 混用
染色用 X-Gray-ID,跟踪用 Traceparent(W3C 标准)。二者语义不同、生命周期不同、透传方式也不同。强行复用同一个 header 会导致采样混乱、span 关联失败、灰度策略污染 trace 数据。
立即学习“go语言免费学习笔记(深入)”;
- OpenTelemetry 初始化后,HTTP 中间件自动从
req.Header.Get("Traceparent")提取并创建 root span - 下游 HTTP 请求需调用
propagator.Inject(ctx, propagation.HeaderCarrier{req.Header})注入Traceparent - gRPC 场景下用
otelgrpc.UnaryClientInterceptor和otelgrpc.UnaryServerInterceptor,它们内部处理 metadata 透传 - 日志、DB、缓存等组件接入 OTel 时,要确保其上下文携带 trace context,否则 span 会断链
user-service-gray 还是 user-service-prod,这个判断必须发生在 HTTP client dial 或 gRPC conn 创建之前,否则连接池、DNS 缓存、长连接都会按默认配置初始化,染色就形同虚设。


















