.NET 分布式跟踪概念

分布式跟踪是一种诊断技术,可帮助工程师本地化应用程序中的故障和性能问题,尤其是可能分布在多台计算机或进程中的故障和性能问题。 有关分布式跟踪的用途的一般信息,请参阅 分布式跟踪概述

跟踪和活动

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

注意

分布式跟踪中工作单元的另一个常见的行业名称为“Span”。 .NET 在许多年前采用了“Activity”一词,那时“Span”这个名称在此概念中尚未得到广泛认可。

活动 ID

Parent-Child 分布式跟踪树中的活动之间的关系是使用唯一 ID 建立的。 .NET 的分布式跟踪实现支持两种 ID 方案:W3C 标准 TraceContext,这是 .NET 5+ 中的默认值,以及一个名为“分层”的较旧的 .NET 约定,可用于向后兼容。 Activity.DefaultIdFormat 用于控制所使用的 ID 方案。 在 W3C TraceContext 标准中,为每个跟踪分配一个全局唯一的 16 字节跟踪 ID(Activity.TraceId),并且跟踪中的每个活动都分配有唯一的 8 字节跨度 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.Current访问。 当前活动会自动流过线程上的所有同步调用,然后是在不同线程上处理的异步调用。 如果活动 A 是线程上的当前活动,并且代码启动新的活动 B,则 B 将成为该线程上的新当前活动。 默认情况下,Activity B 还会将 Activity A 视为其父项。 当活动 B 稍后停止时,活动 A 将还原为线程上的当前活动。 Activity 启动时,它会将当前时间捕获为 Activity.StartTimeUtc。 当停止时,Activity.Duration 被计算为当前时间与开始时间的时间差。

跨进程边界进行协调

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

收集跟踪

检测的代码可以创建 Activity 对象作为分布式跟踪的一部分,但这些对象中的信息需要在集中持久存储中传输和序列化,以便以后可以有效地查看整个跟踪。 有几个遥测收集库可以执行此任务,例如 Application InsightsOpenTelemetry,或第三方遥测或 APM 供应商提供的库。 或者,开发人员可以使用 System.Diagnostics.ActivityListenerSystem.Diagnostics.DiagnosticListener创作自己的自定义活动遥测集合。 ActivityListener 支持观察任何活动,而不考虑开发人员是否对活动有任何事先了解。 这使得 ActivityListener 成为一种简单灵活的常规用途解决方案。 相比之下,使用 DiagnosticListener 是一个更为复杂的场景,它要求被检测的代码通过调用 DiagnosticSource.StartActivity 来主动选择加入,并且集合库需要知道被检测代码在启动时使用的确切命名信息。 使用 DiagnosticSource 和 DiagnosticListener,创建者和侦听器可以交换任意 .NET 对象并建立自定义信息传递约定。

采样

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

后续步骤

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

有关 .NET 原生发出的活动列表,请参阅 .NET 中的内置活动