Skip to content

[BUG] OtelClientTrace filter 的 traceparent 被 triple_invoker 破坏,导致链路追踪 parent 错乱 #3240

@flyu518

Description

@flyu518

✅ 验证清单

  • 🔍 我已经搜索过 现有 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 内部的 parseAttachmentsctx.Value(constant.AttachmentKey) 中的 attachments 再次复制到 invocation。这个 map 里仍然保留着入站请求带来的上游 traceparent,直接覆盖了 OTEL 刚注入的正确值。

相关源码:protocol/triple/triple_invoker.goparseAttachments 方法

根因 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.gomergeAttachmentToOutgoing 方法
  • protocol/triple/triple_protocol/header.goAppendToOutgoingContext 函数

🔄 重现步骤

  1. 服务 A 收到请求(入站 header 包含 traceparent
  2. 服务 A 串行发起两个下游 Triple RPC,分别调用服务 B 和服务 C
  3. 在 Jaeger 等追踪系统中观察:
    • 服务 C 的调用 span 错误地显示服务 B 的 client span 为其 parent(根因 2)
    • 或者 span 引用的是上游调用方的 span,而非服务 A 的 client span(根因 1)

✅ 预期行为

每个出站 RPC 应携带引用当次调用对应 span 的 traceparent,而非上游的旧值或前一次调用的残留值。

❌ 实际行为

  1. 服务 A 收到请求(入站 header 包含 traceparent
  2. 服务 A 串行发起两个下游 Triple RPC,分别调用服务 B 和服务 C
  3. 在 Jaeger 等追踪系统中观察:
    • 服务 C 的调用 span 错误地显示服务 B 的 client span 为其 parent(根因 2)
    • 或者 span 引用的是上游调用方的 span,而非服务 A 的 client span(根因 1)

💡 可能的解决方案

修复建议

triple_invoker.go 中:

  1. parseAttachments 不应将 traceparent / tracestate 从 ctx attachments 复制到 invocation(或让 OTEL filter 在 parseAttachments 之后执行)
  2. 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),
	)
   // ...
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions