.NET 分布式跟踪概念

分布式跟踪是一种诊断技术,可帮助工程师找出应用程序中的故障和性能问题,尤其是那些可能跨多个计算机或进程分布的问题。 如需深入了解分布式跟踪的使用场景以及入门示例代码,请参阅分布式跟踪概述

跟踪和活动

每次应用程序收到新请求时,该请求都可以与跟踪相关联。 在用 .NET 编写的应用程序组件中,跟踪中的工作单元由 System.Diagnostics.Activity 实例表示,并且跟踪整体上构成了这些活动的树,可能跨越了许多不同的进程。 为新请求创建的第一个活动形成跟踪树的根,并跟踪处理请求的总体持续时间和成功/失败。 可以选择创建子 Activity,以将工作细分为可单独跟踪的不同步骤。 例如,假设某个 Activity 跟踪了 Web 服务器中的特定入站 HTTP 请求,则可以创建子 Activity 来跟踪完成请求所需的每个数据库查询。 这样做可以单独记录每个查询的持续时间和成功情况。 活动可以记录每个工作单元的其他信息,例如 OperationName、称为 Tags 的名称-值对和 Events。 名称标识所执行的工作类型;标记可以记录工作的描述性参数;事件是一种简单的日志记录机制,用于记录带时间戳的诊断消息。

注意

分布式跟踪中工作单元的另一个常见的行业名称为“Span”。 .NET 在很多年前就采用了“Activity(活动)”这个术语,当时“Span”这个名称的概念还不为人们所了解。

活动 ID

使用唯一 ID 建立分布式跟踪树中活动之间的父-子关系。 .NET 分布式跟踪的实现支持两种 ID 方案:W3C 标准 TraceContext(这是 .NET 5+ 中的默认设置)和一个较早的名为“分层”的 .NET 约定(可用于向后兼容)。 Activity.DefaultIdFormat 控制使用的 ID 方案。 在 W3C TraceContext 标准中,每个跟踪都分配有一个全局唯一的 16 字节 trace-id (Activity.TraceId),且该跟踪内的每个 Activity 都分配有唯一的 8 字节 span-id (Activity.SpanId)。 每个活动都记录跟踪 ID、其自身的 Span ID 以及其父 (Activity.ParentSpanId) 的 Span ID。 由于分布式跟踪可以跟踪跨进程边界的工作,因此父 Activity 和子 Activity 可能不在同一进程中。 跟踪 ID 和父 Span ID 的组合可以在全局范围内仅标识父活动,无论该活动驻留在哪个进程中。

Activity.DefaultIdFormat 控制使用哪种 ID 格式来启动新的跟踪,但默认情况下,如果将新活动添加到现有跟踪,则会使用任何父活动所使用的格式。 将 Activity.ForceDefaultIdFormat 设置为 true 会覆盖此行为,并使用 DefaultIdFormat 创建所有新的活动(即使父级使用其他 ID 格式也是如此)。

启动和停止 Activity

进程中的每个线程都有一个对应的 Activity 对象,该对象跟踪该线程上发生的工作,可通过 Activity.Current 进行访问。 当前活动会自动流过线程上的所有同步调用,以及在不同线程上处理的异步调用。 如果 Activity A 是线程上的当前 Activity,并且代码启动新的 Activity B,则 B 将成为该线程上新的当前 Activity。 默认情况下,Activity B 还会将 Activity A 视为其父项。 之后,当 Activity B 停止时,Activity A 将还原为该线程上的当前 Activity。 Activity 启动时,它会将当前时间捕获为 Activity.StartTimeUtc。 停止时,会将 Activity.Duration 计算为当前时间与开始时间的差值。

跨进程边界进行协调

为了跟踪跨进程边界的工作,需要在网络中传输 Activity 的父 ID,以便接收进程可以创建引用它们的 Activity。 使用 W3C TraceContext ID 格式时,.NET 还将使用标准推荐的 HTTP 标头来传输此信息。 使用 Hierarchical ID 格式时,.NET 会使用自定义 request-id HTTP 标头来传输该 ID。 与许多其他语言运行时不同,.NET 内置库(如 ASP.NET Web 服务器和 System.Net.Http)本身理解如何对 HTTP 消息上的 Activity ID 进行解码和编码。 运行时还理解如何通过同步和异步调用流式传输 ID。 这意味着,接收和发出 HTTP 消息的 .NET 应用程序会自动参与分布式跟踪 ID 的流式传输工作,应用开发人员无需进行特殊编码,也无需依赖第三方库。 第三方库可以添加对通过非 HTTP 消息协议传输 ID 的支持,或者支持 HTTP 的自定义编码约定。

收集跟踪

在分布式跟踪过程中,检测代码可以创建 Activity 对象,但需要将这些对象中的信息传输到集中的持久性存储中,并在其中进行序列化,这样以后就可以对整个跟踪进行有效地审查。 有几个遥测收集库可以执行此任务,例如 Application InsightsOpenTelemetry 或第三方遥测或 APM 供应商提供的库。 或者,开发人员可以使用 System.Diagnostics.ActivityListenerSystem.Diagnostics.DiagnosticListener 创作自己的自定义活动遥测收集逻辑。 ActivityListener 支持观察任何 Activity,无论开发人员是否对此有先验知识。 因此,ActivityListener 是一种简单而灵活的常规用途解决方案。 与此相反,使用 DiagnosticListener 是一个更复杂的方案,该方案需要检测代码通过调用 DiagnosticSource.StartActivity 来选择加入,而且收集库需要知道检测代码在启动时所使用的确切命名信息。 使用 DiagnosticSource 和 DiagnosticListener,创建者和侦听器可以交换任意 .NET 对象并建立自定义的信息传递约定。

采样

为了改进高吞吐量应用程序的性能,.NET 上的分布式跟踪支持仅采样跟踪的一部分,而不是记录所有跟踪。 对于使用推荐的 ActivitySource.StartActivity API 创建的 Activity,遥测收集库可以使用 ActivityListener.Sample 回调来控制采样。 日志记录库可以选择完全不创建活动,使用传播分布式跟踪 ID 所需的最小信息创建活动,或者用完整的诊断信息填充活动。 这些选择进行了权衡,增加了性能开销以提高诊断效用。 使用较旧的调用方式 Activity.ActivityDiagnosticSource.StartActivity 启动的活动,可以通过首先调用 DiagnosticSource.IsEnabled 来支持 DiagnosticListener 采样。 即使在捕获完整的诊断信息时,.NET 实现也可以快速与高效的收集器耦合,同时,活动可以在新式硬件上以大约一微秒的时间创建、填充和传输。 采样可以将每个未记录的活动的检测成本降低到小于 100 毫微秒。

后续步骤

有关在 .NET 应用程序中开始使用分布式跟踪的示例代码,请参阅分布式跟踪检测