-
Notifications
You must be signed in to change notification settings - Fork 991
Description
✅ 验证清单
- 🔍 我已经搜索过 现有 Issues,确信这不是重复问题
🚀 Go 版本
1.25.5
📦 Dubbo-go 版本
v3.3.0
🖥️ 服务端配置
Dubbo-go, v3.3.0
💻 客户端配置
Dubbo-go, v3.3.0
🌐 协议配置
Triple
📋 注册中心配置
Nacos
💾 操作系统
🐧 Linux
📝 Bug 描述
问题描述
使用内置 otelClientTrace filter + Triple 协议时,客户端发出的 traceparent 会被破坏,导致下游 span 的 parent 指向错误。
根因分析
根因 1 — parseAttachments 覆盖了 OTEL 注入的 traceparent
otelClientTrace filter 在 filter chain 中正确创建 span 并将 traceparent 注入到 invocation.Attachments()。但随后请求进入 protocol invoker,triple_invoker 内部的 parseAttachments 把 ctx.Value(constant.AttachmentKey) 中的 attachments 再次复制到 invocation。这个 map 里仍然保留着入站请求带来的上游 traceparent,直接覆盖了 OTEL 刚注入的正确值。
相关源码:protocol/triple/triple_invoker.go → parseAttachments 方法
根因 2 — AppendToOutgoingContext 修改共享 header map,导致 traceparent 累积
同文件中的 mergeAttachmentToOutgoing 方法调用 tri.AppendToOutgoingContext(位于 protocol/triple/triple_protocol/header.go),该函数通过 header.Add 操作的是 ctx chain 中共享的同一个 http.Header 指针(extraDataKey)。当同一个请求内串行发起多个下游 RPC 时:
- 第 1 次调用将
traceparent写入共享 header - 第 2 次调用通过
header.Add追加新的traceparent - 接收方的
OTEL propagator通过http.Header.Get("traceparent")提取trace context,该方法返回header中的第一个值,即第 1 次 RPC 残留的旧traceparent,而非当前调用注入的正确值。
导致 span parent 指向错误的调用。
相关源码:
protocol/triple/triple_invoker.go→mergeAttachmentToOutgoing方法protocol/triple/triple_protocol/header.go→AppendToOutgoingContext函数
🔄 重现步骤
- 服务 A 收到请求(入站 header 包含
traceparent) - 服务 A 串行发起两个下游 Triple RPC,分别调用服务 B 和服务 C
- 在 Jaeger 等追踪系统中观察:
- 服务 C 的调用 span 错误地显示服务 B 的 client span 为其 parent(根因 2)
- 或者 span 引用的是上游调用方的 span,而非服务 A 的 client span(根因 1)
✅ 预期行为
每个出站 RPC 应携带引用当次调用对应 span 的 traceparent,而非上游的旧值或前一次调用的残留值。
❌ 实际行为
- 服务 A 收到请求(入站 header 包含
traceparent) - 服务 A 串行发起两个下游 Triple RPC,分别调用服务 B 和服务 C
- 在 Jaeger 等追踪系统中观察:
- 服务 C 的调用 span 错误地显示服务 B 的 client span 为其 parent(根因 2)
- 或者 span 引用的是上游调用方的 span,而非服务 A 的 client span(根因 1)
💡 可能的解决方案
修复建议
在 triple_invoker.go 中:
parseAttachments不应将traceparent/tracestate从 ctx attachments 复制到 invocation(或让 OTEL filter 在 parseAttachments 之后执行)mergeAttachmentToOutgoing应使用tri.NewOutgoingContext(ctx, http.Header{})替代tri.AppendToOutgoingContext,避免 mutate 共享的 header map;或在写入前 clone header
临时规避方案
通过 extension.SetFilter 覆盖 otelClientTrace filter,在 span 创建之前清理 ctx 中的 trace headers 并重置出站 header:
规避方案代码
// filter/otel/trace/filter.go
func (f *otelClientFilter) Invoke(ctx context.Context, invoker protocol.Invoker, invocation protocol.Invocation) protocol.Result {
// 修复1:清理 ctx 中的 trace headers,防止 parseAttachments 覆写 OTEL 注入的正确值
attachments := make(map[string]any)
if src, ok := ctx.Value(constant.AttachmentKey).(map[string]any); ok {
for k, v := range src {
attachments[k] = v
}
}
delete(attachments, "traceparent")
delete(attachments, "tracestate")
ctx = context.WithValue(ctx, constant.AttachmentKey, attachments)
// 修复2:重置出站 triple header,防止 AppendToOutgoingContext 在共享 map 上累积
// 前一次 RPC 调用的 traceparent 到当前调用中
ctx = tri.NewOutgoingContext(ctx, http.Header{})
// 下面是原先逻辑
tracer := f.TracerProvider.Tracer(
constant.OtelPackageName,
oteltrace.WithInstrumentationVersion(constant.OtelPackageVersion),
)
// ...
}