Sentry 监控 - Distributed Tracing 分布式跟踪

Sentry 监控 - Distributed Tracing 分布式跟踪

系列

目录

  • 什么是跟踪?
    • 为什么要跟踪?
    • 跟踪(Traces)、事务(Transactions和跨度(Spans)
    • 示例:调查缓慢的页面加载
    • 更多示例
    • 跟踪数据模型
    • 更多信息
  • 数据采样
    • 跟踪中的一致性
公众号:黑客下午茶

分布式跟踪(Distributed tracing)通过捕获软件系统之间的交互来提供相关错误和事务的连接视图。通过跟踪,Sentry 可以跟踪您的软件性能并显示跨多个系统的错误影响。 通过服务追溯问题将您的前端连接到您的后端。

启用性能监控以扩充您现有的错误数据,跟踪从前端到后端的交互。通过跟踪,Sentry 可以跟踪您的软件性能,测量吞吐量和延迟等指标,并显示跨多个系统的错误影响。跟踪使 Sentry 成为更完整的监控解决方案,帮助您更快地诊断问题并衡量应用程序的整体健康状况。Sentry 中的跟踪提供了以下见解:

  • 特定错误事件或 issue 发生了什么
  • 导致应用程序出现瓶颈或延迟 issue 的条件
  • 消耗时间最多的端点或操作

什么是跟踪?

首先,请注意跟踪不是什么:跟踪不是分析。 尽管分析和跟踪的目标有相当多的重叠,虽然它们都可用于诊断应用程序中的问题,但它们在测量内容和数据记录方式方面有所不同。

profiler 可以测量应用程序操作的多个方面:执行的指令数、各种进程使用的内存量、给定函数调用所花费的时间量等等。生成的 profile 是这些测量值的统计汇总。

另一方面,tracing tool 关注发生了什么(以及何时),而不是发生了多少次或花费了多长时间。 结果跟踪(resulting trace)是在程序执行期间发生的事件日志,通常跨多个系统。 尽管跟踪最常见 - 或者,就 Sentry 的跟踪而言,总是 - 包括时间戳(timestamps),允许计算持续时间,但测量性能并不是它们的唯一目的。 它们还可以显示互连系统交互的方式,以及一个系统中的问题可能导致另一个系统出现问题的方式。

为什么要跟踪?

应用程序通常由互连的组件组成,这些组件也称为服务。 作为一个例子,让我们看一个现代 Web 应用程序,它由以下组件组成,由网络边界分隔:

  • Frontend (Single-Page Application) 前端
  • Backend (REST API) 后端
  • Task Queue 任务队列
  • Database Server 数据库服务器
  • Cron Job Scheduler 定时任务调度器

这些组件中的每一个都可以在不同的平台上用不同的语言编写。每个都可以使用 Sentry SDK 单独检测以捕获错误数据或崩溃报告,但该检测不能提供完整的图片,因为每个部分都是单独考虑的。跟踪允许您将所有数据联系在一起。

在我们的示例 Web 应用程序中,跟踪意味着能够跟踪从前端到后端和后端的请求,从请求创建的任何后台任务(background tasks)或通知作业(notification jobs)中提取数据。这不仅可以让您关联 Sentry 错误报告,查看一个服务中的错误如何传播到另一个服务,而且还可以让您更深入地了解哪些服务可能对应用程序的整体性能产生负面影响。

在学习如何在您的应用程序中启用跟踪之前,了解一些关键术语以及它们之间的关系会有所帮助。

跟踪(Traces)、事务(Transactions和跨度(Spans)

trace 表示您要测量或跟踪的整个操作的记录 - 例如页面加载、用户在应用程序中完成某些操作的实例或后端的 cron job。 当跟踪包括多个服务中的工作时,例如上面列出的服务,它被称为分布式跟踪,因为跟踪分布在这些服务中。

每个 trace 由一个或多个称为 transactions 的树状结构组成,其节点称为 spans。 在大多数情况下,每个 transaction 代表被调用服务的单个实例,并且该 transaction 中的每个 span 代表该服务执行单个工作单元,无论是调用该服务中的函数还是调用不同的服务。 这是一个示例跟踪,分解为事务(transactions)和跨度(spans):

Sentry 监控 - Distributed Tracing 分布式跟踪

由于事务(transaction)具有树结构,因此*跨度(top-level spans)本身可以分解为更小的跨度(smaller spans),这反映了一个函数可能调用许多其他更小的函数的方式; 这是使用父子隐喻来表达的,因此每个跨度都可能是多个其他子跨度的父跨度。 此外,由于所有树都必须有一个根,因此每个事务中的一个跨度始终代表事务本身,而事务中的所有其他跨度都从该根跨度下降。这是上图中事务之一的放大视图:

Sentry 监控 - Distributed Tracing 分布式跟踪

为了使所有这些更具体,让我们再次考虑我们的示例 Web 应用程序。

示例:调查缓慢的页面加载

假设您的 Web 应用程序加载缓慢,您想知道原因。 要使您的应用程序首先进入可用状态,必须发生很多事情:对后端的多个请求,可能是一些工作 - 包括对数据库或外部 API 的调用 - 在返回响应之前完成,并由浏览器处理以呈现所有 将返回的数据转化为对用户有意义的内容。那么这个过程的哪一部分会减慢速度?

假设在这个简化的示例中,当用户在浏览器中加载应用程序时,每个服务中都会发生以下情况:

  • Browser(浏览器)
    • HTMLCSSJavaScript1 个请求
    • 1 次渲染任务,触发 2JSON 数据请求 ^
  • Backend(后端)
    • 3 个提供静态文件(HTMLCSSJS)的请求
    • 2JSON 数据请求 - 1 个需要调用数据库 - 1 个需要调用外部 API 并在将结果返回到前端之前处理结果^
  • Database Server(数据库服务器)
    • 1 个请求需要 2 次查询
      • 1 查询以检查身份验证
      • 1 查询获取数据

注意:外部 API 并未准确列出,因为它是外部的,因此您看不到它的内部。

在此示例中,整个页面加载过程(包括上述所有过程)由单个 trace 表示。 该跟踪将由以下事务(transactions)组成:

  • 1 个浏览器事务(用于页面加载)
  • 5 个后端事务(每个请求一个)
  • 1 个数据库服务器事务(用于单个 DB 请求)

每个事务将被分解为跨度(spans)如下:

  • 浏览器页面加载事务:7span
    • 1 个根 span 代表整个页面加载
    • HTMLCSSJS 请求各 1 个(共 3 个)
    • 渲染任务的 1span,它本身包含
      • 2 个子 span,每个 JSON 请求一个

让我们在这里暂停一下以说明一个重点:此处列出的浏览器事务中的一些(尽管不是全部)跨度与前面列出的后端事务有直接对应关系。 具体来说,浏览器事务中的每个请求跨度对应于后端中的一个单独的请求事务。 在这种情况下,当一个服务中的跨度引起后续服务中的事务时,我们将原始跨度称为事务及其根跨度的父跨度。在下图中,波浪线代表这种父子关系。

Sentry 监控 - Distributed Tracing 分布式跟踪

在我们的示例中,除了初始浏览器页面加载事务之外的每个事务都是另一个服务中一个跨度的子项,这意味着除了浏览器事务根之外的每个根跨度都有一个父跨度(尽管在不同的服务中)。

fully-instrumented 的系统(其中每个服务都启用了跟踪的系统)中,这种模式将始终适用。 唯一的无父 span 将是初始 transaction 的根;每隔一个 span 都会有一个父级。 此外,parentschildren 将始终生活在同一个服务中,除非在子 span 是子 transaction 的根的情况下,在这种情况下,父 span 将在调用服务中,而子 transaction/childspan 将在被调用服务中。

换句话说,一个 fully-instrumented 的系统创建一个跟踪,它本身就是一个连接的树——每个事务都是一个子树——在这棵树中,子树/事务之间的边界正是服务之间的边界。上图显示了我们示例的完整跟踪树的一个分支。

现在,为了完整起见,回到我们的 spans

  • 后端 HTML/CSS/JS 请求事务:每个 1 个 span
    • 代表整个请求的 1 个根跨度(浏览器跨度的子项)^
  • 带有数据库调用事务的后端请求:2 个 span
    • 1 个表示整个请求的根跨度(浏览器跨度的子项)
    • 1 个跨度用于查询数据库(数据库服务器事务的父级)^
  • 带有 API 调用事务的后端请求:3 个 span
    • 1 个表示整个请求的根跨度(浏览器跨度的子项)
    • API 请求的 1 个跨度(与数据库调用不同,不是父跨度,因为 API 是外部的)
    • 1 个跨度用于处理 API 数据^
  • 数据库服务器请求事务:3 个 span
    • 1 个代表整个请求的根跨度(上面后端跨度的子项)
    • 1 跨度用于身份验证查询
    • 1 个跨度用于查询检索数据的

总结一下这个例子:在检测了所有服务之后,您可能会发现——出于某种原因——是数据库服务器中的身份验证查询(auth query)导致了速度变慢,占了完成整个页面加载过程所需时间的一半以上。跟踪无法告诉你为什么会发生这种情况,但至少现在你知道该去哪里找了!

更多示例

本节包含更多跟踪示例,分为事务(transaction)和跨度(span)。

衡量特定的用户动作

如果您的应用程序涉及电子商务,您可能希望测量从用户单击“提交订单(Submit Order)”到订单确认出现之间的时间,包括跟踪向支付处理器提交费用和发送订单确认电子邮件。 整个过程是一个跟踪,通常您会有事务 (T) 和跨度 (S) 用于:

  • 浏览器全过程(T 和根跨度 S
    • 对后端的 XHR 请求* (S)
    • 渲染确认 screenS)^
  • 您的后端对该请求的处理(T 和根跨度 S
    • 计算总数的函数(Function)调用 (S)
    • 存储订单数据库(DB)调用* (S)
    • 对支付处理器的 API 调用 (S)
    • 电子邮件确认排队* (S) ^
  • 您的数据库更新客户订单历史的工作(T 和根跨度 S
    • 单个 SQL 查询 (S) ^
  • 发送电子邮件的排队任务(T 和根跨度 S
    • 用于填充电子邮件模板的函数调用 (S)
    • 对电子邮件发送服务的 API 调用 (S)

注意:带星号的跨度表示作为后续事务(及其根跨度)的父跨度。

监控后台进程

如果您的后端定期轮询外部服务的数据,对其进行处理、缓存,然后将其转发给内部服务,则发生这种情况的每个实例都是一个跟踪,您通常会有以下事务 (T) 和跨度 (S)

  • 完成整个过程的 cron jobT 和根跨度 S
    • API 调用外部服务 (S)
    • Processing 函数 (S)
    • 调用缓存服务* (S)
    • API 调用内部服务* (S) ^
  • 在您的缓存服务中完成的工作(T 和根跨度 S
    • 检查现有数据的缓存 (S)
    • 在缓存中存储新数据 (S) ^
  • 您的内部服务对请求的处理(T 和根跨度 S
    • 服务可能为处理请求而做的任何事情 (S)

注意:带星号的跨度表示作为后续事务(及其根跨度)的父跨度。

跟踪数据模型

“给我看你的流程图而隐藏你的表,我仍然莫名其妙。如果给我看你的表,那么我将不再需要你的流程图,因为它们太明显了。”

Fred *s, 《The Mythical Man-Month》(人月神话)

虽然这个理论很有趣,但最终任何数据结构都是由它包含的数据类型定义的,数据结构之间的关系由它们之间的链接如何记录来定义。跟踪、事务和跨度也不例外。

Traces(跟踪)

Traces 本身并不是一个实体。相反,跟踪被定义为共享一个 trace_id 值的所有事务的集合。

Transactions(事务)

Transactions 与其根跨度共享其大部分属性(开始和结束时间、标签等),因此下面描述的跨度的相同选项在事务中可用,并且在任一位置设置它们是等效的。

Transactions 还有一个不包含在跨度中的附加属性,称为 transaction_name,它在 UI 中用于标识 transactiontransaction_name 值的常见示例包括后端请求事务的端点路径(如 /store/checkout/api/v2/users/<user_id>/)、cron job 事务的任务名称(如 data.cleanup.delete_inactive_users)和 URL( 像 https://docs.sentry.io/performance-monitoring/distributed-tracing/) 用于页面加载事务。

Spans(跨度)

transaction 中的大部分数据驻留在事务包含的单个 span 中。span 数据包括:

  • parent_span_id: 将 span 与其父 span 联系起来
  • op: 标识跨度正在测量的操作类型或类别的短字符串
  • start_timestamp: span 打开时
  • end_timestamp: span 关闭时
  • description: span 操作的较长描述,唯一标识 span,但跨 span 实例保持一致(可选)
  • status: 指示操作状态的短 code(可选)
  • tags: key-value 对保存有关跨度的附加数据(可选)
  • data: 关于 span 的任意结构的附加数据(可选)

opdescription 属性一起使用的示例是 op: sql.querydescription: SELECT * FROM users WHERE last_active < %sstatus 属性通常用于指示 span 操作的成功或失败,或者在 HTTP 请求的情况下用于 response code。 最后,tagsdata 允许您将更多上下文信息附加到 span,例如 function: middleware.auth.is_authenticated 用于函数调用或 request: {url: ..., headers: ... , body: ...} 用于 HTTP 请求。

更多信息

关于跟踪、事务和跨度以及它们相互关联的方式的一些更重要的点:

Trace Duration(跟踪持续时间)

因为 trace 只是 transaction 的集合,所以 trace 没有自己的开始和结束时间。 相反,trace 在其最早的 transaction 开始时开始,并在其最新的 transaction 结束时结束。 因此,您无法直接明确地开始或结束 trace。 相反,您通过在该 trace 中创建第一个 transaction 来创建 trace,并通过完成它包含的所有 transaction 来完成 trace

Async Transactions(异步事务)

由于异步进程的可能性,子事务(child transaction)可能比包含其父跨度(parent span)的事务的寿命长很多数量级。例如,如果后端 API 调用启动了一个长时间运行的处理任务,然后立即返回响应,则后端事务将在异步任务事务完成之前很久完成(并且其数据将被发送到 Sentry)。异步性还意味着 transaction 发送到(和接收)Sentry 的顺序与创建它们的顺序没有任何关系。 (相比之下,同一 tracetransaction 的接收顺序与完成顺序相关,但由于传输时间的可变性等因素,相关性远非完美。)

Orphan Transactions(孤儿事务)

理论上,在一个 fully instrumented 的系统中,每个 trace 应该只包含一个 transaction 和一个 spantransaction的根),没有父项,即原始服务中的 transaction。但是,在实践中,您可能不会在每一项服务中都启用 trace,或者检测的服务可能由于网络中断或其他不可预见的情况而无法报告 transaction。发生这种情况时,您可能会在跟踪层次结构中看到间隙。 具体来说,您可能会在 span 的中途看到其父 span 尚未记录为任何已知 transaction 的一部分的 transaction。这种非发起、无父 transaction 被称为孤儿事务。

Nested Spans(嵌套跨度)

尽管我们上面的示例在其层次结构中有四个级别(跟踪trace、事务transaction、跨度span、子跨度child span),但跨度嵌套的深度没有设置限制。 但是,存在实际限制:发送到 Sentry 的事务有效负载具有最大允许大小,并且与任何类型的日志记录一样,需要在数据的粒度与其可用性之间取得平衡。

Zero-duration Spans(零持续时间跨度)

跨度可能具有相同的开始时间和结束时间,因此被记录为不占用时间。这可能是因为 span 被用作标记(例如在浏览器的 Performance API 中完成的),或者因为操作花费的时间少于测量分辨率(这将因服务而异)。

Clock Skew(时钟偏移)

如果您从多台机器收集 transaction,您可能会遇到 clock skew,其中一个 transaction 中的时间戳与另一个 transaction 中的时间戳不一致。 例如,如果您的后端进行数据库调用,则后端事务在逻辑上应该在数据库事务之前开始。 但是,如果每台机器(分别托管后端和数据库的机器)上的系统时间未同步到通用标准,则情况可能并非如此。 排序也有可能是正确的,但是两个记录的时间范围没有以准确反映实际发生的方式排列。为了减少这种可能性,我们建议使用网络时间协议 (NTP) 或您的云提供商的时钟同步服务。

如何发送数据

单个 span 不会发送到 Sentry; 相反,整个 transaction 作为一个单位发送。 这意味着 Sentry 的服务器不会记录任何 span 数据,直到它们所属的 transaction 被关闭和分派。 然而,反过来就不是这样了——尽管没有 transaction 就不能发送 span,但 transaction 仍然有效,并且会被发送,即使它们包含的唯一 span 是它们的根 span

数据采样

当您在跟踪设置中启用采样时,您可以选择要发送到 Sentry 的已收集交易的百分比。例如,如果您有一个每分钟接收 1000 个请求的端点,0.25 的采样率将导致每分钟大约 250 个事务 (25%) 被发送到 Sentry。(这个数字是近似的,因为每个请求要么被跟踪,要么被独立和伪随机地跟踪,概率为 25%。因此,以同样的方式,100 个公平硬币,在翻转时会导致大约 50 个正面,SDK 将“决定” 在大约 250 个案例中收集跟踪。)因为您知道采样百分比,所以您可以推断您的总流量。

在收集跟踪时,我们建议对您的数据进行采样,原因有两个。 首先,虽然捕获单个跟踪的开销最小,但捕获每个页面加载或每个 API 请求的跟踪可能会给您的系统增加不希望的负载量。 其次,启用采样可以让您更好地管理发送到 Sentry 的事件数量,以便您可以根据组织的需求对其进行定制。

选择采样率时,目标是不要收集太多数据(鉴于上述原因),而是收集足够的数据,以便得出有意义的结论。如果您不确定要选择什么速率,我们建议从一个较低的值开始,并随着您对流量模式和流量的了解逐渐增加,直到找到一个速率,使您能够平衡性能和流量与数据准确性之间的关系。

跟踪中的一致性

对于涉及多个事务的跟踪,Sentry 使用 “基于头部(head-based)” 的方法:在原始服务中做出采样决策,然后将该决策传递给所有后续服务。要了解这是如何工作的,让我们回到上面的 webapp示例。考虑两个用户 AB,他们都在各自的浏览器中加载应用程序。当 A 加载应用程序时,SDK 伪随机“决定”收集跟踪,而当 B 加载应用程序时,SDK “决定”不收集跟踪。当每个浏览器向您的后端发出请求时,它会在这些请求的标题中包含“yes, please collect transactions)”“no, don't collect transactions this time”的决定。

当您的后端处理来自 A 浏览器的请求时,它会看到 “yes” 的决定,收集事务和跨度数据,并将其发送给 Sentry。此外,它在向后续服务(如您的数据库服务器)发出的任何请求中都包含“yes”决定,这些服务同样会收集数据,将数据发送给 Sentry,并将决定传递给它们调用的任何服务。通过这个过程,A的跟踪中的所有相关事务都被收集并发送到 Sentry

另一方面,当您的后端处理来自 B 浏览器的请求时,它会看到 “no” 决定,因此它不会收集和发送事务和跨度数据到 Sentry。然而,它在将决策传播到后续服务方面做与在 A 的情况下所做的相同的事情,告诉他们也不要收集或发送数据。 然后他们又告诉他们调用的任何服务不要发送数据,这样就不会收集到来自 B 跟踪的事务。

简而言之:这种 head-based 的方法的结果是,决策在原始服务中作出一次,并传递给所有后续服务,要么收集给定跟踪的所有事务,要么不收集任何事务,因此不应存在任何不完整的跟踪。

公众号:黑客下午茶
上一篇:Sentry 监控 - 全栈开发人员的分布式跟踪 101 系列教程(第一部分)


下一篇:Sentry 监控 - Dashboards 事件数据可视化大屏