文章介绍了 Dapr 团队如何通过增强 OpenTelemetry 集成,解决异步工作流跟踪上下文传播难题,实现 Jaeger 连贯跟踪,并探索语义对齐与异步行为建模,为云原生项目提供协作范例。
译自:Improving Async Workflow Observability in Dapr
作者:Mauricio “Salaboy” Salatino (Diagrid), Kasper Borg Nissen (Dash0)
本文讲述了云原生社区的贡献者如何协同工作,以增强 Dapr 的 OpenTelemetry 集成,特别是在异步工作流方面。它还强调了使用 OpenTelemetry Weaver 使 Dapr 与 OpenTelemetry 语义约定保持一致的持续努力,并探讨了这种协作如何为其他 CNCF 项目提供一个有益的范例。这项工作都不是作为正式倡议开始的。它是在讨论、实验和共同的目标中出现的,即让遥测数据更易理解且在生态系统中更一致。
在复杂编排中传播跟踪的挑战
Dapr 的工作流引擎 提供了一种直接的方式来实现长时间运行的同步和异步编排。工作流编排在 Dapr sidecar 内部运行,而工作流和活动代码则在使用 Dapr SDK 的应用程序内部运行。它们之间的通信通过一个长寿命的 gRPC 流进行。
这很高效,但却使 W3C 跟踪上下文传播变得困难。HTTP 和一元 gRPC 调用自然会随每个请求携带 traceparent 和 tracestate 头部。而长寿命的流则不然。一旦流打开,工作流步骤就无法附加新的元数据。这意味着工作流引擎可能在 sidecar 内部创建具有正确上下文的 Span,但活动消息到达应用程序时却没有父上下文。活动中的出站调用随后会创建自己的跟踪。
结果是碎片化:工作流 Span、活动 Span 和用户级别 Span 出现在跟踪后端,但它们并未形成连贯的层次结构。

此图说明了问题:尽管工作流引擎正在生成 Span,但上下文在 gRPC 流边界处中断,因此活动代码无法附加到工作流的跟踪中。
典型工作流编排的复杂性
工作流表面上看起来很简单,但一旦我们开始跟踪它们,其运动部件的数量就会变得显而易见。工作流不是一个单一的请求。它是一个长时间运行的决策和状态转换序列,其中每个步骤都可能与不同的下游系统交互。其中一些交互是同步的,例如当一个活动调用外部服务并等待结果时。另一些是异步的,例如调度工作、等待外部信号或暂停直到计时器触发。这些差异很重要,因为它们改变了请求在系统中的流向,并塑造了跟踪应有的样子。
每个工作流也携带其自己的身份。工作流实例 ID 在概念上将所有步骤联系在一起,即使它们在时间上是分离的。一个工作流可能运行几秒钟或几小时。它可能在故障后重新激活或在服务器重启后恢复。持久化执行意味着工作流引擎持久化状态、重放事件,并在外部条件变化时推进工作流。从跟踪的角度来看,这与普通的请求或后台作业非常不同。单个工作流执行可以跨越许多网络调用、等待周期、重试和部分进度。
因此,跟踪工作流意味着捕获立即执行的同步调用,以及工作流引擎调度或恢复工作的异步边界。它意味着将一个进程中运行的用户代码与另一个进程中运行的编排逻辑关联起来。它还意味着在持久化边界上保持上下文完整,以便跟踪反映工作流的实际生命周期,而不是一系列孤立的操作。
这些特性使得工作流跟踪具有挑战性。它们也使其变得有价值。当跟踪准确地表示编排时,它就成为一个强大的工具,用于理解工作流随时间的行为及其步骤如何相互影响。
恢复跨工作流边界的上下文
解决这个问题需要跨多个代码库进行协调更改。第一步是启用 durabletask-go(一个用于跟踪工作流编排的库),使其在通过流发送之前,将 W3C 上下文序列化到工作流活动消息中。
通过将 traceparent 和 tracestate 嵌入 到活动消息内部,工作流引擎无需依赖每消息 gRPC 元数据即可传播上下文。
在应用程序侧,durabletask-java SDK 进行了更新,以在运行活动代码之前读取并恢复此上下文。可以在 探索性分支 中看到其一个版本。
随着应用程序中上下文的恢复,OpenTelemetry 的 Java 自动插桩变得有效。由于 OpenTelemetry Java 代理看到了有效的父上下文,它能自动插桩出站 HTTP 调用和其他操作。无需修改任何应用程序代码。
OpenTelemetry Operator 在这些实验中发挥了重要作用,它自动将 Java 代理注入到工作流应用程序容器中。这使我们能够专注于理解和改进上下文传播,而无需手动配置插桩。
最后,Dapr 运行时得到了改进,以优先处理工作流 Span 之间清晰的父子关系,目标是提高开发者的清晰度。仍需要进一步的工作来评估 Span 链接在何处能更好地表示某些异步关系。
Jaeger 中的新视图
一旦这些部分到位,在 Jaeger 中查看时,改进就非常明显。以前,入站请求、工作流编排器以及每个活动的出站调用都显示为单独的跟踪。
更改后,Jaeger 瀑布图显示了一个单一、连续的跟踪。入站请求 Span 位于顶部。工作流编排器 Span 出现在其下方。每个活动都嵌套在工作流 Span 之下。出站调用出现在相应的活动之下。
工作流最终呈现为一个连贯的生命周期。

连续的瀑布图不仅使工作流的结构,而且使其节奏也易于理解。等待周期、重试、状态转换和长时间运行的操作都准确地出现在它们应有的位置。
从跟踪连续性到语义对齐
一旦 Dapr 工作流生成了完整的跟踪,新的问题就出现了:这些 Span 应该代表什么?它们应该如何命名?Dapr 与许多组件交互,例如计时器、状态存储、发布/订阅代理、绑定、密钥和配置 API。如果没有稳定的语义,不同的 SDK 或运行时组件可能会以不同的方式表示类似的操作。
为解决这个问题,Dapr 正在采用 OpenTelemetry Weaver,它允许项目以机器可读格式存储语义约定。 这个初步 PR 将 Weaver 引入 Dapr。
Weaver 为 Dapr 提供了一个单一的地方来定义工作流、状态交互、组件调用、发布/订阅交付等方面的遥测属性。这有助于统一跨 SDK 的行为,并使 Dapr 与更广泛的 OpenTelemetry 语义模型保持一致。这仅仅是采用的开始,Dapr 其余交互的对齐工作仍在进行中。
更好地建模异步行为
当我们审查改进后的工作流跟踪时,我们也开始探索 Dapr 如何更好地使用 Span 类型表示异步执行。历史上,工作流引擎几乎将所有与工作流相关的 Span 都作为客户端 Span 发出。这反映了引擎正在调用操作的事实,但它没有清楚地表达调度工作和实际运行工作之间的区别。一旦工作流跟踪形成连续、可读的层次结构,这种区别就变得更加重要。
在异步系统中,生产者语义通常在调度工作时提供更清晰的信号,而消费者语义可以表示执行工作的点。在活动代码内部发出的出站调用仍将是客户端 Span,内部工作流转换仍将是内部 Span。这种方法可以帮助跟踪工具更自然地传达工作流行为。
这些更改目前都不是 Dapr 的一部分。这是由跟踪改进所激发的早期探索,也是我们已开始与 Dapr 维护者讨论的方向。目标不是重新设计 Dapr 的遥测,而是开始塑造一个更准确反映异步行为并与 OpenTelemetry 语义指南保持一致的模型。我们期望这项工作随着更多贡献者的参与而协作发展。
为什么这项工作在云原生生态系统中很重要
尽管这项协作专注于 Dapr,但其基本挑战存在于许多云原生项目中。代理、服务网格、事件路由器、工作流引擎和控制器经常面临类似的问题:如何通过 sidecar 或非 HTTP 路径传播 W3C 上下文,如何定义语义约定,如何建模异步行为,以及如何确保 SDK 保持一致。
跨项目协作有助于解决这些问题。Linkerd 和 Traefik 也发生过类似的改进工作。每个与 OpenTelemetry 语义保持一致的新项目都为依赖来自多个组件信号的运营商改进了整体体验。
展望
随着 OpenTelemetry 在 CNCF 生态系统中的日益普及,现在也许是时候在 OpenTelemetry 社区中进行更广泛的讨论,探讨如何最好地支持项目维护者。许多人希望改进他们的遥测,但不知道从何开始。其他人则需要关于 Span 类型、采用 Weaver 或建模异步操作等主题的指导。
这次讨论不需要以正式工作组的形式开始。即使是一个非正式空间也可以帮助项目比较方法、分享最佳实践并避免重复发明已在其他地方解决的模式。Dapr 和 OpenTelemetry 贡献者之间的工作展示了社区协作能走多远。
如果您维护或贡献于一个 CNCF 项目并希望加强其 OpenTelemetry 集成,我们邀请您加入讨论。共享遥测有助于创建更可预测、更易解释、更易操作的分布式系统。