欢迎来到全栈开发人员分布式跟踪(Distributed Tracing)该系列的第 1 部分。在本系列中,我们将学习分布式跟踪的细节,以及如何帮助您监控整个堆栈应用程序日益复杂的需求。
在 Web 早期,写 Web 应用程序非常简单。开发人员使用 PHP 等语言在服务器上HTML,与 MySQL 等单一关系数据库通信,大部分互动是静态 HTML 表单组件驱动。尽管调试工具非常原始,但理解代码的执行过程非常简单。
今天的现代 web 在栈里,什么都不是。全栈开发人员需要在浏览器中编写 JavaScript,在不同的服务器架构(如:serverless)上部署服务器端代码。如果没有合适的工具,了解浏览器中的用户交互如何与服务器堆栈深处的 500 相关server error 几乎不可能。Enter:分布式跟踪。
我试图解释 2021年我的 web 堆栈中的瓶颈。
分布式跟踪(Distributed tracing)它是一种将多个服务之间的操作和请求联系起来的监控技术。这允许开发人员从一个服务移动到另一个服务“跟踪(trace)”它的路径允许他们在单个服务中发现错误或性能瓶颈,对整个系统产生负面影响。
本文将了解更多关于分布式跟踪概念的信息,并在代码中查看端到端(end-to-end)跟踪示例,了解如何使用跟踪元数据为您的日志记录和监控工具添加有价值的上下文。完成后,您不仅将了解分布式跟踪的基本知识,还将了解如何应用跟踪技术来更有效地调试整堆 Web 应用程序。
但首先,让我们回到开始:什么是分布式跟踪?
分布式追踪基础
分布式跟踪是记录多个服务的连接操作方法。通常,这些操作是由从一个服务到另一个服务的请求发起的“请求(request)”可以是实际 HTTP 请求也可以通过任务队列或其他异步调用。
跟踪由两个基本组件组成:
- Span 描述发生在服务上的操作或 “work”。Span 可以描述广泛的操作——例如,响应 HTTP 请求的 web 操作服务器——还可以描述单个函数的调用。
- trace 描述一个或多个连接 span 的端到端(end-to-end)旅程。如果 trace 在多个服务上连接 span(“work”),则该 trace 被认为是分布式跟踪。
让我们看看假设的分布式跟踪示例。
上图显示了 trace 如何从一个服务(一个在浏览器上运行的 React 应用程序)开始并调用 API Web Server 继续,甚至进一步调用后台任务 worker。此图中的 span 是在每项服务中执行的 work,每个 span 都可以“追溯到(traced)”初始工作由浏览器应用程序启动(initial work)。最后,跟踪被认为是分布式的,因为这些操作发生在不同的服务上。
描述广泛操作的跨度(例如:响应 HTTP request 的 Web server 完整的生命周期)有时被称为事务跨度(transaction spans),甚至只是事务。我们将在本系列的第 2 部分中更多地讨论事务与跨度(transactions vs. spans)。
跟踪和跨度标识符
到目前为止,我们已经确定了跟踪组件,但我们还没有描述它们是如何连接在一起的。
首先,每个跟踪都使用跟踪标志符(trace identifier)唯一的标志。这是通过根跨度(root span)创建唯一的随机生成值(即 UUID)来完成的——这是启动整个跟踪的初始操作。在我们上面的示例中,根跨度出现在浏览器应用程序中。
第二,每个 span 首先,它需要被唯一的识别。这创建了唯一的跨度标识符(或 span_id)来完成。这个 span_id 创建应该发生在 trace 发生在每个 span(或操作)处进行。
让我们重新审视我们假设的跟踪示例。在上图中,您会注意到跟踪标志符是唯一的跟踪标志,跟踪中的每个跨度也有唯一的跨度标志符。
然而,生成 trace_id 和 span_id 是不够的。为了实际连接这些服务,您的应用程序必须在从一个服务到另一个服务的请求中传播所谓的跟踪(trace context)。
跟踪上下文
跟踪上下文(trace context)通常仅由两个值组成:
- 跟踪标识符(或 trace_id):根跨度生成的唯一标识符用于标识整个跟踪。这与我们上一节介绍的跟踪标识符相同;它以不变的方式传播到每个下游服务。
- 父标符(或 parent_id):产生当前操作“父”跨度的 span_id。
下图显示了如何务中启动的请求如何将跟踪上下文传播到下游的下一个服务。你会注意到 trace_id 保持不变, parent_id 在请求之间发生变化,指向启动最新操作的父跨度。
有了这两个值,可以确定任何给定操作的原始值(root)服务,按照导致当前操作的顺序重建所有父亲/祖先(parent/ancestor)服务。
工作示例(代码演示)
示例源码:
- https://github.com/getsentry/distributed-tracing-examples
为了更好地理解这一点,让我们实现一个基本的跟踪实现,其中浏览器应用程序是一系列由跟踪上下文连接的分布式操作的发起者。
首先,浏览器应用程序呈现一个表单:就这个例子而言,它是一个表单“邀请用户(invite user)”表格。表格中有一个提交事件处理程序,在表格提交时触发。让我们把这个提交程序视为我们的根跨度(root span),这意味着调用处理程序时会产生 trace_id 和 span_id。
接下来,完成一些工作,从表单中收集用户输入的值,然后最我们 Web 服务器发送到 /inviteUser API 端点的 fetch 请求。作为这个 fetch 请求的一部分,跟踪上下文作为两个自定义 HTTP header 传递:trace-id 和 parent-id(即当前 span 的 span_id)。
请注意,这些都是用来解释目的的非标准 HTTP header。作为 W3C traceparent 标准化的一部分正在积极努力标准化 tracing HTTP header,该规范仍在 “Recommendation” 阶段。
- https://www.w3.org/TR/trace-context/
在接收端,API web server 处理请求请求HTTP 请求中提取跟踪元数据(tracing metadata)。然后它会排队 job 向用户发送电子邮件,并将跟踪上下文作为 job 描述中“meta”附加了字段的一部分。最后,它回到了 200 状态 code 的反应表明该方法成功。
请注意,尽管服务器已经回到了成功的响应,但实际上“工作”直到后台任务 worker 拿起新排队的 job 并实际发送电子邮件。
在某一点上,队列处理器开始处理排队的电子邮件操作。再次跟踪(trace)和父标识符(parent identifier)就像它们在 web server 中早些时候一样。
分布式系统 Logging
你会注意到 将用于我们示例的每个阶段console.log 进行 logging 调用还发出了目前的 调用trace、span 和 parent 标识符。在完美的同步世界——每个服务都可以登录到同一个集中式 logging 工具——这些日志语句中的每一个都会依次出现:
如果在这些操作过程中出现异常或错误行为,使用这些或额外的日志语句来找出来源将相对简单。但不幸的是,这些都是分布式服务,这意味着:
Web 服务器通常处理许多并发请求。Web 服务器可能正在执行归因于其他语句),服务器可能正在执行工作。
网络延迟会影响操作顺序。从上游服务发出的请求可能不会按照它们被触发的顺序到达目的地。
后台 worker 可能有排队的 job。确切排队 job 之前,worker 可能必须先完成以前的排队 job。
在一个更现实的例子中,我们的日志调用可能看起来像这样,它反映了同时发生的多个操作:
若不跟踪 metadata,不可能知道哪个动作调用哪个动作的拓扑结构。但每次 logging 调用时发出跟踪 meta通过过滤 信息traceId 快速过滤跟踪中的所有 logging 调用,检查 spanId 和 parentId 关系重建的确切顺序。
这就是分布式跟踪的力量:附加描述当前操作(span id)、产生它的父操作(parent id)跟踪标识符(trace id)对于元数据,我们可以添加日志记录和遥测数据,以更好地理解 分布式服务中事件的确切顺序。
在真实的分布式跟踪环境中
在这篇文章的过程中,我们一直在使用一个有点人为的例子。在真实的分布式跟踪环境中,您不会手动生成和传输所有的跨度和跟踪符号。你不会依赖 console.log(或其他日志记录)调用自己发送跟踪元数据。您将使用适当的跟踪库来处理检测和发送跟踪数据。
OpenTelemetry
OpenTelemetry 是一组开源工具,API 和 SDK,用于检测、生成和导出正在运行的软件中的遥测数据。它为大多数流行的编程语言提供了特定于语言的实现,包括浏览器 JavaScript 和 Node.js。
- https://opentelemetry.io/
- https://github.com/open-telemetry/opentelemetry-js
Sentry
Sentry 以多种方式使用这种遥测。Sentry 性能监控功能集使用跟踪数据生成瀑布图,说明跟踪中分布式服务操作的端到端延迟。
Sentry 还使用跟踪元数据来增强其错误监控功能,以了解服务(如服务器后端)中触发的错误如何传输到另一个服务(如前端)。