追踪是一种强大的工具,可让您分析应用程序的性能并识别瓶颈。本教程将教您追踪的核心概念,以及如何使用 OpenTelemetry 和 Prisma 的追踪功能将追踪集成到您的应用程序中。
目录
简介
在本教程中,您将学习如何将追踪集成到使用 Prisma 和 Express 构建的现有 Web 应用程序中。您将使用 OpenTelemetry 实现追踪,这是一个用于收集追踪和其他遥测数据(例如日志、指标等)的厂商中立标准。
首先,您将为 HTTP 端点创建手动追踪并将其打印到控制台。然后,您将学习如何使用 Jaeger 可视化您的追踪。您还将学习如何使用 Prisma 的追踪功能自动为您的数据库查询生成追踪。最后,您将了解自动插桩以及使用追踪时的性能考量。
什么是追踪?
追踪是一种*可观察性工具*,它记录请求在应用程序中传播时所经过的路径。追踪可帮助您将系统为响应任何特定请求而执行的活动关联起来。追踪还提供有关这些活动的时间信息(例如,开始时间、持续时间等)。
一个单独的*追踪*会为您提供有关用户或应用程序发出请求时发生的情况的信息。每个追踪由一个或多个*跨度*组成,其中包含有关请求期间发生的单个步骤或任务的信息。
使用像 Jaeger 这样的追踪工具,追踪可以可视化为以下图表
一个单独的跨度可以有多个子跨度,它们表示父跨度期间发生的子任务。例如,在上面的图中,PRISMA QUERY 跨度有一个名为 PRISMA ENGINE 的子跨度。最顶层的跨度称为*根跨度*,表示从开始到结束的整个追踪。在上面的图中,GET /ENDPOINT 是根跨度。
追踪是深入了解和查看系统的一种绝佳方式。它允许您精确识别影响应用程序的错误和性能瓶颈。追踪对于调试分布式系统特别有用,在分布式系统中,每个请求可能涉及多个服务,并且特定问题可能难以在本地重现。
您将使用的技术
在本教程中,您将使用以下工具
- OpenTelemetry 作为追踪库/API
- Prisma 作为对象关系映射器 (ORM)
- SQLite 作为数据库
- Jaeger 作为追踪可视化工具
- Express 作为 Web 框架
- TypeScript 作为编程语言
先决条件
假设的知识
这是一个适合初学者的教程。但是,本教程假设您
- 对 JavaScript 或 TypeScript(首选)有基本了解
- 对后端 Web 开发有基本了解
注意:本教程假定您没有追踪和可观察性方面的预备知识。
开发环境
要跟随本教程,您需要
- ... 安装 Node.js。
- ... 安装 Docker 和 Docker Compose。
- ... *可选地*安装 Prisma VS Code 扩展。Prisma VS Code 扩展为 Prisma 添加了非常棒的智能感知和语法高亮。
- ... *可选地*拥有 Unix Shell 访问权限(例如 Linux 和 macOS 中的终端/Shell),以运行本系列中提供的命令。
如果您没有 Unix Shell(例如,您使用的是 Windows 机器),您仍然可以继续操作,但 Shell 命令可能需要根据您的机器进行修改。
克隆存储库
您将需要一个 Web 应用程序来演示追踪。您可以使用我们为本教程构建的现有 Express Web 应用程序。
首先,执行以下操作
- 克隆 存储库
- 导航到克隆的目录
- 安装依赖项
- 从
prisma/migrations
目录应用数据库迁移
注意:此命令还将生成 Prisma Client 并为数据库填充数据。
- 启动项目
注意:您在开发应用程序时应保持服务器运行。
dev
脚本应在代码发生任何更改时重新启动服务器。
该应用程序只有一个端点:http://localhost:4000/users/random。此端点将从数据库中返回 10 个用户的随机样本。通过访问上述 URL 或运行以下命令来测试该端点
项目结构和文件
您克隆的存储库具有以下结构
此存储库中值得注意的文件和目录有
prisma
schema.prisma
: 定义数据库 schema。migrations
: 包含数据库迁移历史记录。seed.ts
: 包含用于为开发数据库填充模拟数据的脚本。dev.db
: 存储 SQLite 数据库的状态。
server.ts
: 带有GET /users/random
端点的 Express 服务器。tsconfig.json
&package.json
: 配置文件。
将追踪集成到您的应用程序中
您的 Express 应用程序已经实现了所有核心“业务逻辑”(即返回 10 个随机用户)。为了衡量应用程序的性能并提高其可观察性,您将集成追踪。
在本节中,您将学习如何初始化追踪并手动创建追踪。
初始化追踪
您将使用 OpenTelemetry 追踪来实现追踪。OpenTelemetry 提供了一个开源实现,它兼容各种平台和语言。此外,它还附带用于实现追踪的库和 SDK。
通过安装以下 OpenTelemetry 包来开始追踪
这些包包含 OpenTelemetry 追踪的 Node.js 实现。
现在,创建一个新的 tracing.ts
文件来初始化追踪
在 tracing.ts
内部,按如下方式初始化追踪
initializeTracing
函数执行以下操作
- 它初始化一个追踪器提供程序,用于创建追踪器。追踪器在您的应用程序中创建追踪/跨度。
- 它定义一个追踪导出器并将其添加到您的提供程序中。追踪导出器将追踪发送到各种目标。在这种情况下,
ConsoleSpanExporter
将追踪打印到控制台。 - 它通过调用
.register()
函数来注册提供程序以与 OpenTelemetry API 一起使用。 - 最后,它创建一个追踪器并将其返回,追踪器名称作为参数传递给函数。
现在,导入并在现有的 server.ts
中调用 initializeTracing
现在您已准备好创建您的第一个追踪!
创建您的第一个追踪
在上一节中,您初始化了追踪并将追踪器导入到您的服务器。现在您可以使用 tracer
对象在服务器中创建跨度。首先,您将创建一个封装 GET /users/random
请求的追踪。按如下方式更新请求处理程序定义
在这里,您使用 startActiveSpan()
创建一个新的跨度,并将所有请求处理程序逻辑包含在它提供的回调函数中。回调函数带有一个对 span
对象的引用,您已将其命名为 requestSpan
。您可以使用它来修改或向跨度添加属性。在此代码中,您根据请求的结果将一个名为 http.status
的属性设置为跨度。最后,一旦请求得到服务,您就结束该跨度。
要查看新创建的跨度,请转到 http://localhost:4000/users/random。或者,您可以在终端中运行以下命令
转到运行 Express 服务器的终端窗口。您应该会看到一个*类似*以下内容的打印到控制台的对象
此对象代表您刚刚创建的跨度。其中一些值得注意的属性是
id
表示此特定跨度的唯一标识符。traceId
表示特定追踪的唯一标识符。某个追踪中的所有跨度都将具有相同的traceId
。目前,您的追踪仅包含一个跨度。parentId
是父跨度的id
。在本例中,它是undefined
,因为根跨度没有父跨度。name
表示跨度的名称。您在创建跨度时指定了它。timestamp
是表示跨度创建时间的 UNIX 时间戳。duration
是跨度的持续时间,以微秒为单位。
使用 Jaeger 可视化追踪
目前,您正在控制台中查看追踪。虽然这对于单个追踪来说是可管理的,但对于大量追踪来说并不十分有用。为了更好地理解您的追踪,您需要一些可以可视化追踪的追踪解决方案。在本教程中,您将为此目的使用 Jaeger。
设置 Jaeger
您可以通过两种方式设置 Jaeger
在本教程中,您将使用 Docker Compose 来运行 Jaeger 的 Docker 镜像。首先,创建一个新的 docker-compose.yml
文件
在文件中定义以下服务
运行此镜像将在 Docker 容器内设置并初始化 Jaeger 的所有必要组件。要运行 Jaeger,请打开一个*新的*终端窗口,并在项目的主文件夹中运行以下命令
注意:如果您关闭运行 docker 容器的终端窗口,它也将停止容器。您可以通过在命令末尾添加
-d
选项来避免这种情况,例如:docker-compose up -d
。
如果一切顺利,您应该能够通过 http://localhost:16686 访问 Jaeger。
由于您的应用程序尚未向 Jaeger 发送追踪,Jaeger UI 将为空。
添加 Jaeger 追踪导出器
要在 Jaeger 中查看您的追踪,您需要设置一个新的追踪导出器,它将把追踪从您的应用程序发送到 Jaeger(而不是仅仅将它们打印到控制台)。
首先,在您的项目中安装导出器包
现在将导出器添加到 tracing.ts
在这里,您初始化了一个新的 JaegerExporter
并将其添加到您的追踪器提供程序中。JaegerExporter
构造函数中的 endpoint
属性指向 Jaeger 监听追踪数据的位置。您还删除了控制台导出器,因为它不再需要。
您现在应该能够在 Jaeger 中看到您的追踪。要查看您的第一个追踪
- 再次查询
GET /users/random
端点 (curl http://localhost:4000/users/random
)。 - 前往 http://localhost:16686。
- 在左侧的 Search(搜索)选项卡中,在 Service(服务)下拉菜单中,选择 express-server。
- 在 Search 选项卡底部附近,单击 Find Traces(查找追踪)。
- 您现在应该看到一个追踪列表。单击列表中的第一个追踪。
- 您将看到追踪的详细视图。应该有一个名为 GET /users/random 的单个跨度。单击该跨度以获取更多信息。
- 您应该能够看到有关追踪的各种信息,例如持续时间和开始时间。您还应该看到多个标签,其中一个您已手动设置(
http.status
)。
为您的 Prisma 查询添加追踪
在本节中,您将学习如何追踪您的数据库查询。最初,您将通过自行创建跨度来手动执行此操作。尽管 Prisma 现在不再需要手动追踪,但实现手动追踪将让您更好地了解追踪的工作原理。
然后,您将使用 Prisma 的新 追踪功能自动执行相同的操作。
手动追踪您的 Prisma 查询
要手动追踪您的 Prisma 查询,您必须将每个查询包装在一个跨度中。您可以通过将以下代码添加到 server.ts
文件来完成此操作
您已经为 Prisma 查询创建了一个名为 prisma.user.findmany
的新跨度。您还对 users
变量的声明方式进行了一些更改,以使其与您代码的其余部分保持一致。
通过再次查询 GET /users/random
端点 (curl http://localhost:4000/users/random
) 并在 Jaeger 中查看新生成的追踪来测试新跨度。
您应该会看到生成的追踪有一个名为 prisma.user.findmany
的新子跨度,嵌套在父 GET /users/random
跨度之下。现在您可以看到请求中执行 Prisma 查询所花费的时间。
手动与自动插桩
到目前为止,您已经学习了如何设置追踪以及如何手动为应用程序生成追踪和跨度。像这样手动定义跨度称为*手动插桩*。手动插桩使您能够完全控制应用程序如何被追踪,但是,它有一些缺点
- 手动追踪应用程序非常耗时,特别是当您的应用程序很大时。
- 并非总是能够正确地手动插桩第三方库。例如,无法使用手动插桩来追踪 Prisma 内部组件的执行。
- 它可能导致错误和缺陷(例如,错误处理不当、损坏的跨度等),因为它涉及手动编写大量代码。
幸运的是,许多框架和库提供*自动插桩*,允许您自动为这些组件生成追踪。自动插桩几乎不需要代码更改,设置非常快速,并且可以为您提供开箱即用的基本遥测数据。
值得注意的是,自动插桩和手动插桩并非相互排斥。同时使用这两种技术可能很有益。自动插桩可以提供良好的基线遥测,覆盖所有端点。然后可以添加手动插桩,以实现特定的细粒度追踪和自定义指标/元数据。
为 Prisma 设置自动插桩
本节将教您如何使用新的追踪功能为 Prisma 设置自动插桩。首先,在您的 schema.prisma
文件的 generator 块中启用追踪功能标志
注意:追踪目前是一个预览功能。这就是您必须在可以使用追踪之前添加
tracing
功能标志的原因。
现在,重新生成 Prisma Client
要执行自动插桩,您还需要使用 npm
安装两个新包
需要这些包的原因是
@opentelemetry/instrumentation
是设置自动插桩所必需的。@prisma/instrumentation
为 Prisma Client 提供自动插桩。
根据 OpenTelemetry 术语,*被插桩库*是为其收集追踪的库或包。另一方面,*插桩库*是为特定被插桩库生成追踪的库。在这种情况下,Prisma Client 是被插桩库,而 @prisma/instrumentation
是插桩库。
现在您需要向 OpenTelemetry 注册 Prisma 插桩。为此,请将以下代码添加到您的 tracing.ts
文件中
registerInstrumentations
调用接受两个参数
instrumentations
接受您要注册的所有插桩库的数组。tracerProvider
接受您的追踪器提供程序。
由于您正在设置自动插桩,因此不再需要手动为 Prisma 查询创建跨度。通过删除 Prisma 查询的手动跨度来更新 server.ts
使用自动插桩时,初始化追踪的顺序很重要。您需要在导入被插桩库之前设置追踪并注册插桩。在这种情况下,initializeTracing
调用必须在 import
PrismaClient
语句之前。
再次,向 GET /users/random
端点发出请求,并在 Jaeger 中查看生成的追踪。
这次,同一个 Prisma 查询生成了多个跨度,提供了更多关于查询的细粒度信息。启用自动插桩后,您添加到应用程序的任何其他查询也将自动生成追踪。
注意:要了解有关 Prisma 生成的跨度的更多信息,请参阅追踪文档的追踪输出部分。
为 Express 设置自动插桩
目前,您正在通过手动创建跨度来追踪您的端点。就像 Prisma 查询一样,随着端点数量的增加,手动追踪将变得难以管理。为了解决这个问题,您也可以为 Express 设置自动插桩。
首先安装以下插桩库
在 tracing.ts
内部注册这两个新的插桩库
最后,移除 server.ts
中 GET /users/random
端点的手动跨度
向 GET /users/random
端点发出请求,并在 Jaeger 中查看生成的追踪。
您应该会看到更细粒度的跨度,显示请求通过您的代码时不同步骤。特别是,您应该会看到由 ExpressInstrumentation
库生成的新跨度,这些跨度显示请求通过各种 Express 中间件和 GET /users/random
请求处理程序。
注意:有关可用插桩库的列表,请查看 OpenTelemetry 注册表。
减少追踪的性能影响
如果您的应用程序向收集器(如 Jaeger)发送大量跨度,这可能会对应用程序的性能产生显著影响。这在您的开发环境中通常不是问题,但在生产环境中可能会成为问题。您可以采取一些措施来缓解此问题。
批量发送追踪
目前,您正在使用 SimpleSpanProcessor
发送追踪。这种方式效率低下,因为它一次发送一个跨度。您可以改为使用 BatchSpanProcessor
批量发送跨度。
在您的 tracing.ts
文件中进行以下更改,以便在生产环境中使用 BatchSpanProcessor
请注意,在开发环境中,您仍然使用 SimpleSpanProcessor
,因为在此环境中优化性能并不是主要考虑因素。这确保了在开发过程中,追踪一经生成便立即显示。
通过采样减少跨度发送
概率采样是一种技术,允许 OpenTelemetry 追踪用户通过使用随机采样技术来降低跨度收集的性能成本。使用此技术,您可以减少发送到收集器的跨度数量,同时仍能很好地表示应用程序中发生的情况。
更新 tracing.ts
以使用概率采样
与批量处理一样,您仅在生产环境中引入概率采样。
总结与最终说明
恭喜!🎉
在本教程中,您学习了
- 什么是追踪,以及为什么要使用它。
- 什么是 OpenTelemetry,以及它与追踪的关系。
- 如何使用 Jaeger 可视化追踪。
- 如何将追踪集成到现有 Web 应用程序中。
- 如何使用自动插桩库来提高代码可观察性。
- 如何减少追踪在生产环境中的性能影响。
您可以在 GitHub 上找到此项目的源代码。如果您发现问题,请随时在存储库中提出问题或提交拉取请求。您也可以直接在 Twitter 上与我联系。
不要错过下一篇文章!
订阅 Prisma 新闻通讯