本文是四篇教程之一,旨在帮助您实践2023年3月微服务专题:开始交付微服务中的概念:
- 如何部署和配置微服务
- 如何安全管理容器中的密钥
- 如何使用GitHub Actions自动执行微服务金丝雀部署
- 如何使用OpenTelemetry追踪来了解微服务(本文)
微服务架构具有诸多优势,包括提高团队自主性,以及增强扩展和部署的灵活性。但另一方面,系统中的服务越多(一个微服务应用可能包含数十甚至数百个服务),就越难清晰掌握系统的整体运行状况。作为复杂软件系统的开发者和维护者,我们深知清晰掌握整体运行状况至关重要。可观测性工具让我们能够全面了解众多服务及其支撑基础设施的运行状况。
本教程将重点介绍微服务应用中一种非常重要的可观测性类型:追踪。在开始之前,先来定义一些讨论可观测性时常用的术语:
- 可观测性:仅根据复杂系统(如微服务应用)的外部输出(如跟踪、日志和指标),了解其内部状态或状况的能力。
- 监控:在一段时间内观察和检查对象的进展或状态。例如,可以监控高峰时段进入应用的流量,并根据这些信息适当扩缩容。
- 遥测:收集指标、跟踪信息和日志,并将其从源位置传输到另一个系统进行存储和分析的过程。也指这些数据本身。
- 追踪/追踪记录:记录请求或操作在分布式系统的所有节点间流转的完整过程。
- 跨度:跟踪中记录某项操作及其相关元数据的条目。跟踪由多个嵌套的跨度组成。
- 事件日志记录/日志:带有时间戳和元数据的文本记录。
- 指标:运行时采集的测量值。例如,应用程序在某一时刻的内存使用量。
我们可以运用上述所有概念来深入了解微服务的性能。追踪是可观测性策略中尤为实用的一环,因为在发出请求时,追踪信息可以呈现多个组件(通常为松耦合组件)之间所发生情况的“全貌”。追踪也是识别性能瓶颈的一种尤为有效的方法。
本教程使用OpenTelemetry(OTel)提供的追踪工具包。OTel是一项开源且不依赖特定供应商的标准,用于收集、处理和导出遥测数据,目前正迅速普及。在OTel的概念中,追踪会将可能涉及多个服务的数据流划分为一系列按时间顺序排列的“片段”,帮助您轻松了解:
- 一个“片段”中发生的所有步骤
- 所有这些步骤共耗时多久
- 每个步骤的元数据
如果您不熟悉OTel,请参阅什么是OpenTelemetry?,全面了解该标准及其实施注意事项。
教程概述
本教程重点介绍如何使用OTel跟踪微服务应用的运行情况。通过教程中的4项挑战,您将学习如何在系统中跟踪请求,并解答有关微服务的问题:
这些挑战说明了首次设置追踪时建议采用的流程。具体步骤如下:
- 了解系统以及要进行插桩的具体操作。
- 确定需要从运行中的系统了解哪些信息。
- 以“朴素”方式对系统进行插桩,即使用默认配置,不尝试剔除不需要的信息,也不收集自定义数据点,然后评估这些插桩是否有助于回答您的问题。
- 调整报告的信息,以便更快地回答这些问题。
注意:本教程旨在说明遥测的一些核心概念,而不是展示在生产环境中部署微服务的正确方式。虽然本教程采用了真实的“微服务”架构,但仍有一些重要注意事项:
- 本教程不使用Kubernetes或Nomad等容器编排框架,以便您专注于学习微服务概念,而不必深究特定框架的具体细节。此处介绍的模式可移植到运行上述任一框架的系统中。
- 这些服务经过优化,侧重于易于理解,而非严格遵循软件工程规范。重点是了解服务在系统中的作用及其通信模式,而不是代码的具体细节。有关更多信息,请参阅各个服务的README文件。
教程架构和遥测目标
架构和用户流程
此图展示了本教程所用微服务及其他元素的整体架构和数据流。

这两个微服务是:
- The messenger服务:一个具备消息存储功能的简单聊天API
- The notifier服务:一种监听器,可根据用户偏好触发事件并向用户发出提醒
三项支持基础设施分别是:
- NGINX Open Source,作为messenger服务及整个系统的入口
- RabbitMQ,一种广受欢迎的开源消息代理,可让服务进行异步通信
- Jaeger,一种开源的端到端分布式追踪系统,用于收集并可视化系统中各组件生成的遥测数据
暂且不考虑OTel,我们可以专注于所追踪的事件序列:用户发送一条新的聊天消息后,接收者收到相关通知的过程。

具体流程如下:
- 用户向messenger服务发送消息。NGINX反向代理会拦截该消息,并将其转发到messenger服务的多个实例之一。
- The messenger服务将新消息写入其数据库。
- The messenger服务在名为chat_queue的RabbitMQ消息队列中生成一个事件,表示消息已发送。该事件为通用事件,不针对任何特定目标。
- 与此同时:
- 4a. messenger服务向发送方返回响应,报告消息已成功发送。
- 4b. notifier服务发现chat_queue中的新事件并将其消费。
- The notifier服务在其数据库中查询新消息接收者的通知偏好设置。
- The notifier服务使用收件人的首选方式发送一条或多条通知(本教程中可选择短信或电子邮件)。
遥测目标
设置遥测检测时,建议先明确一组具体目标,而不是“发送所有数据,然后期待从中获得洞察”。本教程有三个关键遥测目标:
- 了解请求在新消息流中经历的所有步骤
- 确认在正常条件下,该流程可在5秒内完成端到端执行
- 查看notifier服务开始处理由messenger服务分派的事件需要多长时间(延迟过长可能意味着notifier服务无法顺利从事件队列中读取数据,导致事件积压)
请注意,这些目标既关系到系统的技术运行,也关系到用户体验。
教程前提条件和设置
先决条件
要在自己的环境中完成本教程,需要:
- 与Linux/Unix兼容的环境注意:本教程中涉及NGINX跟踪的操作无法在基于ARM的处理器上运行,因为NGINX的OpenTelemetry模块与其不兼容。(这包括Linux aarch64架构以及搭载M1或M2芯片的Apple设备。)涉及messenger和notifier服务的操作可在所有架构上运行。
- 基本熟悉Linux命令行、JavaScript和
bash(但文中提供并解释了所有代码和命令,因此即使相关知识有限,也能顺利完成本教程) - Docker 和 Docker Compose
- Node.js 19.x或更高版本
curl(大多数系统已预装)- 以下是架构和用户流程中列出的技术:messenger和notifier(将在下一节下载),NGINX Open Source、Jaeger和RabbitMQ。
注意:本教程使用JavaScript SDK,因为messenger和notifier服务均使用Node.js编写。您还将设置OTel的自动插桩功能(也称为自动插桩),以便了解可从OTel获取的信息类型。本教程介绍了使用OTel Node.js SDK所需的全部知识。如需了解更多详情,请参阅OTel文档。
设置
- 启动终端会话。
- 在主目录中创建microservices-march目录,并将本教程所需的GitHub存储库克隆到该目录中。(也可以使用其他目录名称,并相应调整说明。)注意:为便于将命令复制并粘贴到终端,本教程省略了Linux命令行提示符。波浪号(
~)表示主目录。
mkdir ~/microservices-marchcd ~/microservices-march git clone https://github.com/microservices-march/messenger --branch mm23-metrics-start git clone https://github.com/microservices-march/notifier --branch mm23-metrics-start git clone https://github.com/microservices-march/platform --branch mm23-metrics-start挑战 1:设置基本的 OTel 插桩
在此挑战中,您将启动messenger服务,并配置OTel自动插桩,将遥测数据发送到控制台。
启动messenger服务
- 切换到platform代码仓库并启动Docker Compose:
cd ~/microservices-march/platformdocker compose up -d --build- 这将启动RabbitMQ和Jaeger,后续挑战中会用到它们。
- The
‑d标志指示Docker Compose在容器启动后与其分离(否则,容器将保持连接到终端)。 - The
--build标志指示Docker Compose在启动时重新构建所有镜像。这样,即使文件可能发生更改,正在运行的镜像也能保持更新。
- The
- 切换到app存储库中的messenger目录并安装Node.js(也可以根据需要改用其他方法):
cd ~/microservices-march/messenger/appasdf install- 安装依赖项:
npm install- 启动messenger服务的PostgreSQL数据库:
docker compose up -d- 创建数据库架构和表,并插入一些种子数据:
npm run refresh-db配置发送到控制台的OTel自动插桩
借助OTel自动插桩,无需修改messenger代码库中的任何内容即可设置跟踪。所有跟踪配置均在一个脚本中定义,而不是直接导入应用程序代码;该脚本会在运行时导入Node.js进程。
在此,为messenger服务配置自动插桩,并将最基本的追踪数据目标位置设为控制台。在挑战 2中,将更改配置,把追踪数据发送到作为外部收集器的Jaeger。
- 继续在app目录下操作,该目录位于messenger代码仓库中。安装核心OTel Node.js软件包:
npm install @opentelemetry/sdk-node@0.36.0 \ @opentelemetry/auto-instrumentations-node@0.36.4- 这些库提供以下功能:注意:OTel有一个特点,其JavaScript SDK被拆分成许多非常小的组件。因此,即使只是完成本教程中的基础示例,也需要额外安装几个软件包。要了解完成本教程未涵盖的插桩任务可能需要哪些软件包,请参阅OTel内容详实的入门指南,并浏览OTel的GitHub存储库。
@opentelemetry/sdk-node:生成和导出OTel数据@opentelemetry/auto-instrumentations-node,使用默认配置自动设置所有最常用的Node.js插桩
- 新建一个名为tracing.mjs的文件,用于存放OTel跟踪的设置和配置代码:
touch tracing.mjs- 在常用的文本编辑器中打开tracing.mjs,并添加以下代码:
//1
import opentelemetry from "@opentelemetry/sdk-node";
import { getNodeAutoInstrumentations } from "@opentelemetry/auto-instrumentations-node";
//2
const sdk = new opentelemetry.NodeSDK({
traceExporter: new opentelemetry.tracing.ConsoleSpanExporter(),
instrumentations: [getNodeAutoInstrumentations()],
});
//3
sdk.start();- 该代码执行以下操作:
- 从OTel SDK导入所需的函数和对象。
- 创建一个新的NodeSDK实例,并将其配置为:
- 将跨度发送到控制台(
ConsoleSpanExporter)。 - 使用自动插桩器作为基础插桩工具集。该工具集会加载所有最常用的自动插桩库。本教程涉及的库包括:
@opentelemetry/instrumentation-pg,用于Postgres数据库库(pg)@opentelemetry/instrumentation-express,用于Node.js Express框架@opentelemetry/instrumentation-amqplib,用于RabbitMQ库(amqplib)
- 将跨度发送到控制台(
- 启动SDK。
- 启动messenger服务,并导入在步骤3中创建的自动插桩脚本。
node --import ./tracing.mjs index.mjs- 片刻后,控制台(终端)中会开始显示大量与跟踪相关的输出:
...
{
traceId: '9c1801593a9d3b773e5cbd314a8ea89c',
parentId: undefined,
traceState: undefined,
name: 'fs statSync',
id: '2ddf082c1d609fbe',
kind: 0,
timestamp: 1676076410782000,
duration: 3,
attributes: {},
status: { code: 0 },
events: [],
links: []
}
...注意:请保持终端会话处于打开状态,以便在挑战 2 中重复使用。
挑战 2:为所有服务设置OTel插桩和跟踪可视化
有许多工具可用于查看和分析跟踪数据,但本教程使用Jaeger。Jaeger是一款简单的开源端到端分布式跟踪框架,内置基于Web的用户界面,可用于查看跨度及其他跟踪数据。platform存储库提供的基础设施中包含Jaeger(已在挑战1的第1步中启动),因此可以专注于分析数据,而无需处理复杂的工具配置。
可在浏览器中通过http://localhost:16686端点访问Jaeger,但如果现在访问该端点,将看不到任何系统信息。这是因为当前收集的跟踪数据正发送到控制台!要在Jaeger中查看跟踪数据,需要使用OpenTelemetry协议(OTLP)格式导出跟踪数据。
在本挑战中,您将通过为以下内容配置检测,对核心用户流程进行检测:
- The messenger服务,将跟踪数据的目标位置从控制台切换到Jaeger
- The notifier服务
- NGINX
配置发送到外部收集器的OTel自动插桩
请注意,使用OTel自动插桩意味着无需修改messenger代码库中的任何内容来设置追踪。所有追踪配置都位于一个脚本中,该脚本会在运行时导入Node.js进程。在此,将messenger服务所生成追踪数据的目标位置从控制台更改为外部收集器(本教程中为Jaeger)。
- 继续使用挑战 1 中的同一终端,并在 app 目录(位于 messenger 代码仓库中)安装OTLP导出器Node.js软件包:
npm install @opentelemetry/exporter-trace-otlp-http@0.36.0- The
@opentelemetry/exporter-trace-otlp-http库通过HTTP以OTLP格式导出跟踪信息。向外部OTel收集器发送遥测数据时会使用该库。 - 打开tracing.mjs(在挑战 1 中创建并编辑的文件),并进行以下更改:注意:为简单起见,本教程假定收集器位于默认地址http://localhost:4318/v1/traces。在实际系统中,建议明确设置其地址。
- 在文件顶部的一组
import语句中添加以下行:
- 在文件顶部的一组
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";- 将提供给OTel SDK的“exporter”从挑战1中使用的控制台exporter更改为可通过HTTP将OTLP数据发送到OTLP兼容收集器的exporter。替换以下内容:
traceExporter:new opentelemetry.tracing.ConsoleSpanExporter(),- 替换为:
traceExporter: new OTLPTraceExporter({ headers: {} }),- 按
Ctrl+c停止messenger服务。该服务是在“配置将 OTel 自动插桩数据发送到控制台”的第 4 步中通过此终端启动的。然后重启该服务,以使用第 2 步中配置的新导出器:
^cnode --import ./tracing.mjs index.mjs- 启动另一个独立的终端会话。(后续说明将其称为“客户端终端”,并将步骤1和步骤3中使用的原始终端称为“消息传递终端”。)等待约10秒,然后向messenger服务发送健康检查请求(如果想查看多条跟踪记录,可以多运行几次):
curl -X GET http://localhost:4000/health- 发送请求前等待10秒,有助于更轻松地找到您的跟踪记录,因为该记录会出现在服务启动时自动插桩生成的大量跟踪记录之后。
- 在浏览器中访问位于http://localhost:16686的Jaeger UI,并验证OTLP导出器是否按预期工作。单击标题栏中的“Search”,然后从“Service”字段的下拉菜单中选择名称以“unknown_service”开头的服务。单击“Find Traces”按钮:

- 单击窗口右侧的某个跟踪,即可显示其中的跨度列表。每个跨度都描述了该跟踪中执行的操作,这些操作有时会涉及多个服务。屏幕截图中的jsonParser跨度显示了
messenger服务的请求处理代码中jsonParser部分的运行时长。
- 如步骤 5 所述,OTel SDK导出的服务名称(unknown_service)没有实际意义。要解决此问题,请在messenger终端中按
Ctrl+c停止messenger服务。然后再安装几个Node.js软件包:
^c npm install @opentelemetry/semantic-conventions@1.10.0 \ @opentelemetry/resources@1.10.0- 这两个库提供以下功能:
@opentelemetry/semantic-conventions:定义OTel规范中规定的跟踪标准属性。@opentelemetry/resources,定义一个表示OTel数据生成源的对象(资源)(在本教程中为messenger服务)。
- 在文本编辑器中打开tracing.mjs,并进行以下更改:
- 将以下代码行添加到文件顶部的
import语句中:
- 将以下代码行添加到文件顶部的
import { Resource } from "@opentelemetry/resources";import { SemanticResourceAttributes } from "@opentelemetry/semantic-conventions";- 在OTel规范的正确键下创建一个名为“
resource”的messenger,方法是在最后一条import语句后添加以下行:
const resource = new Resource({ [SemanticResourceAttributes.SERVICE_NAME]: "messenger",
});- 将
resource对象传递给NodeSDK构造函数,方法是在黑色代码行之间添加橙色突出显示的代码行:
const sdk = new opentelemetry.NodeSDK({ resource,
traceExporter: new OTLPTraceExporter({ headers: {} }),
instrumentations: [getNodeAutoInstrumentations()],
});- 重启messenger服务:
node --import ./tracing.mjs index.mjs- 等待约10秒,然后在客户端终端(即在步骤4中打开的终端)中再次向服务器发送运行状况检查请求(如需查看多条跟踪记录,可多次运行该命令):
curl -X GET http://localhost:4000/health- 注意:请保持客户端终端处于打开状态,以便在下一节中继续使用;同时保持消息传递终端处于打开状态,以便在挑战 3 中继续使用。
- 确认浏览器中的Jaeger UI内出现名为messenger的新服务(这可能需要几秒钟,可能还需要刷新Jaeger UI):

- 从messenger下拉菜单的Service中选择该服务,然后点击Find Traces按钮,查看源自messenger服务的所有近期跟踪记录(截图显示了20条记录中最近的2条):

- 单击某个跟踪,即可显示其中的跨度。每个跨度均已正确标记为来自messenger服务:

配置notifier服务的OTel自动插桩
现在,为notifier服务启动并配置自动插桩,所运行的命令与前两节中针对messenger服务运行的命令基本相同。
- 打开一个新的终端会话(后续步骤中称为“notifier终端”)。切换到notifier存储库中的app目录,然后安装Node.js(也可以根据需要使用其他方法):
cd ~/microservices-march/notifier/appasdf install- 安装依赖项:
npm install- 启动notifier服务的PostgreSQL数据库:
docker compose up -d- 创建数据库架构和表,并插入一些种子数据:
npm run refresh-db- 安装OTel Node.js软件包(有关这些软件包功能的说明,请参阅《配置发送到控制台的OTel自动插桩》中的步骤1和步骤3):
npm install @opentelemetry/auto-instrumentations-node@0.36.4 \ @opentelemetry/exporter-trace-otlp-http@0.36.0 \ @opentelemetry/resources@1.10.0 \ @opentelemetry/sdk-node@0.36.0 \ @opentelemetry/semantic-conventions@1.10.0- 新建一个名为“tracing.mjs”的文件:
touch tracing.mjs- 在常用的文本编辑器中打开tracing.mjs,然后添加以下脚本以启动并运行OTel SDK:
import opentelemetry from "@opentelemetry/sdk-node";
import { getNodeAutoInstrumentations } from "@opentelemetry/auto-instrumentations-node";
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";
import { Resource } from "@opentelemetry/resources";
import { SemanticResourceAttributes } from "@opentelemetry/semantic-conventions";
const resource = new Resource({
[SemanticResourceAttributes.SERVICE_NAME]: "notifier",
});
const sdk = new opentelemetry.NodeSDK({
resource,
traceExporter: new OTLPTraceExporter({ headers: {} }),
instrumentations: [getNodeAutoInstrumentations()],
});
sdk.start();- 注意:此脚本与messenger服务中的脚本完全相同,唯一的区别是
SemanticResourceAttributes.SERVICE_NAME字段的值为notifier。 - 使用OTel自动插桩启动notifier服务:
node --import ./tracing.mjs index.mjs- 等待约10秒,然后在客户端终端中向notifier服务发送健康检查请求。该服务侦听端口5000,以免与侦听端口4000的messenger服务发生冲突:
curl http://localhost:5000/health- 注意:保持客户端和通知程序终端处于打开状态,以便在挑战3中重复使用。
- 确认浏览器中的Jaeger UI内出现了一个名为“notifier”的新服务:

配置NGINX的OTel插桩
对于NGINX,需要手动设置跟踪,而不是使用OTel自动插桩方法。目前,使用OTel对NGINX进行插桩最常见的方式是使用用C语言编写的模块。第三方模块是NGINX生态系统的重要组成部分,但需要完成一些设置工作。本教程将指导您完成设置。如需了解背景信息,请参阅我们博客上的《为NGINX和NGINX Plus编译第三方动态模块》。
- 启动一个新的终端会话(即“NGINX终端”),切换到messenger代码库的根目录,并新建一个名为load-balancer的目录,以及以下文件:Dockerfile、nginx.conf和opentelemetry_module.conf:
cd ~/microservices-march/messenger/mkdir load-balancer cd load-balancer touch Dockerfile touch nginx.conf touch opentelemetry_module.conf- 在常用的文本编辑器中打开Dockerfile,并添加以下内容(注释说明了每行的作用,但即使不完全理解,也可以构建并运行Docker容器):
FROM --platform=amd64 nginx:1.23.1
# Replace the nginx.conf file with our own
COPY nginx.conf /etc/nginx/nginx.conf
# Define the version of the NGINX OTel module
ARG OPENTELEMETRY_CPP_VERSION=1.0.3
# Define the search path for shared libraries used when compiling and running NGINX
ENV LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/opt/opentelemetry-webserver-sdk/sdk_lib/lib
# 1. Download the latest version of Consul template and the OTel C++ web server module, otel-webserver-module
ADD https://github.com/open-telemetry/opentelemetry-cpp-contrib/releases/download/webserver%2Fv${OPENTELEMETRY_CPP_VERSION}/opentelemetry-webserver-sdk-x64-linux.tgz /tmp
RUN apt-get update \
&& apt-get install -y --no-install-recommends dumb-init unzip \
# 2. Extract the module files
&& tar xvfz /tmp/opentelemetry-webserver-sdk-x64-linux.tgz -C /opt \
&& rm -rf /tmp/opentelemetry-webserver-sdk-x64-linux.tgz \
# 3. Install and add the 'load_module' directive at the top of the main NGINX configuration file
&& /opt/opentelemetry-webserver-sdk/install.sh \
&& echo "load_module /opt/opentelemetry-webserver-sdk/WebServerModule/Nginx/1.23.1/ngx_http_opentelemetry_module.so;\n$(cat /etc/nginx/nginx.conf)" > /etc/nginx/nginx.conf
# 4. Copy in the configuration file for the NGINX OTel module
COPY opentelemetry_module.conf /etc/nginx/conf.d/opentelemetry_module.conf
EXPOSE 8085
STOPSIGNAL SIGQUIT- 打开“nginx.conf”,并添加以下内容:
events {}
http {
include /etc/nginx/conf.d/opentelemetry_module.conf;
upstream messenger {
server localhost:4000;
}
server {
listen 8085;
location / {
proxy_pass http://messenger;
}
}
}- 这个非常基础的NGINX配置文件指示NGINX执行以下操作:注意:这与生产环境中将NGINX用作反向代理和负载均衡器时的实际配置非常接近。唯一的主要区别是,
server指令在upstream块中的参数通常是域名或IP地址,而不是localhost。- 设置一个名为messenger的上游组,用于表示messenger服务实例组
- 在端口 8085 上侦听HTTP请求
- 将路径以/开头的所有传入请求(即所有传入请求)转发到messenger上游
- 打开opentelemetry_module.conf并添加以下内容:
NginxModuleEnabled ON;NginxModuleOtelSpanExporter otlp;
NginxModuleOtelExporterEndpoint localhost:4317;
NginxModuleServiceName messenger-lb;
NginxModuleServiceNamespace MicroservicesMarchDemoArchitecture;
NginxModuleServiceInstanceId DemoInstanceId;
NginxModuleResolveBackends ON;
NginxModuleTraceAsError ON;- 构建一个同时包含NGINX和NGINX OTel模块的Docker镜像:
docker build -t messenger-lb .- 启动用于NGINX反向代理和负载均衡器的Docker容器:
docker run --rm --name messenger-lb -p 8085:8085 --network="host" messenger-lb- 在客户端终端中,通过NGINX反向代理和负载均衡器向messenger服务发送健康检查请求(发送此请求前无需等待):
curl http://localhost:8085/health- 注意:请保持NGINX和客户端终端处于打开状态,以便在挑战3中继续使用。
- 在浏览器中,确认新的messenger-lb服务与之前启动的服务一起列在Jaeger UI中。可能需要在浏览器中重新加载Jaeger UI。

挑战3:学习解读OTel追踪数据
在《架构和用户流程》中,我们概述了用户流程的各个阶段。简要回顾如下:
- 用户向另一位用户发送消息,发起对话。
- NGINX反向代理会拦截该消息,并将其转发到messenger服务。
- The messenger服务将消息写入其数据库,然后通过RabbitMQ分发事件。
- The notifier服务使用该事件,查询接收者(第二位用户)的通知偏好设置,并通过其首选方式向接收者发送通知。
实施遥测的目标包括:
- 了解请求为完成新消息流而经历的所有步骤。
- 确认在正常条件下,该流程可在5秒内完成端到端执行。
- 查看notifier服务开始处理由messenger服务分派的事件需要多长时间。
在本挑战中,您将学习如何评估OTel插桩生成的跟踪是否满足上述目标。首先,试用该系统并创建一些跟踪。然后,检查消息流的跟踪,以及其中由NGINX、messenger服务和notifier服务生成的部分。
创建跟踪数据
在客户端终端中创建一个会话,并让两名用户互发几条消息:
curl -X POST \ -H "Content-Type: application/json" \ -d '{"participant_ids": [1, 2]}' \ 'http://localhost:8085/conversations' curl -X POST \ -H "User-Id: 1" \ -H "Content-Type: application/json" \ -d '{"content": "This is the first message"}' \ 'http://localhost:8085/conversations/1/messages' curl -X POST \ -H "User-Id: 2" \ -H "Content-Type: application/json" \ -d '{"content": "This is the second message"}' \ 'http://localhost:8085/conversations/1/messages'以下输出由notifier服务生成,并显示在notifier终端中:
Received new_message: {"type":"new_message","channel_id":1,"user_id":1,"index":1,"participant_ids":[1,2]}Sending notification of new message via sms to 12027621401
Received new_message: {"type":"new_message","channel_id":1,"user_id":2,"index":2,"participant_ids":[1,2]}
Sending notification of new message via email to the_hotstepper@kamo.ze
Sending notification of new message via sms to 19147379938准备读取跟踪信息
在浏览器中打开Jaeger UI,从“messenger-lb”下拉菜单中选择“Service”,然后点击“Find Traces”按钮。此时将显示跟踪列表,其中包含从流程起点开始的跟踪。点击任意跟踪即可查看其详细信息,如以下屏幕截图所示:

点击各处进行一些探索。继续之前,请思考跟踪中的信息如何支持挑战3简介中列出的可观测性插桩目标。相关问题包括:
- 哪些信息有助于实现这些目标?
- 缺少哪些信息?
- 哪些信息不相关?
检查跟踪中的NGINX(messenger-lb)部分
目标1:查看请求在新消息流中经历的所有步骤
先从NGINX跨度入手。该父跨度包含11个子跨度。由于当前的NGINX配置非常简单,这些子跨度没有太多值得关注的信息,只显示NGINX请求处理生命周期中每个步骤的耗时。不过,父跨度(即最前面的跨度)包含一些值得关注的信息:

- 在标签下,可以看到以下属性:综合来看,这三项信息表示:“已向
POST发送/conversations/1/messages请求,响应为201(创建成功)”。这对应于架构和用户流程中的步骤1和4a。http.method字段:POST(在REST术语中,这表示创建)http.status_code字段:201(表示创建成功)http.target字段:conversations/1/messages(消息端点)
- 在“Process”下,
webengine.name字段表明这是请求中由NGINX处理的部分。
此外,由于messenger和notifier的跨度嵌套在messenger-lb的conversations/1跨度中(如准备读取跟踪数据中的屏幕截图所示),因此可以确定,经由NGINX反向代理发送到messenger服务的请求已到达流程中的所有预期组件。
这些信息达到了预期目标,因为可以看出NGINX反向代理参与了该流程。
目标2:验证流程是否在5秒内执行完毕
在标记为messenger-lb的跨度列表中,查看最新的跨度(位于列表底部),了解请求的NGINX部分耗时。在截图中,该跨度从第589微秒(µs)开始,持续24 µs,这意味着整个反向代理操作仅耗时613 µs,约为0.6毫秒(ms)。(实际运行本教程时,具体数值会有所不同。)

在这样的配置中,这些数值大多只有与其他测量结果进行比较时才有意义,而且会因系统而异。不过在本例中,这项操作的耗时显然远未接近5秒。
这些信息达到了预期目的,因为可以看出,NGINX操作的耗时远未达到5秒。如果流程中存在耗时很长的操作,那么它一定发生在后续环节。
目标3:查看notifier服务读取messenger服务分派的事件需要多长时间
NGINX反向代理层不包含与此相关的任何信息,因此可以继续查看messenger跨度。
检查跟踪中的 messenger 部分
目标1:查看请求在新消息流中经历的所有步骤
跟踪记录中的messenger服务部分还包含另外11个跨度。同样,大多数子跨度都与Express框架处理请求时采用的基本步骤有关,并没有太多值得关注的内容。不过,父跨度(即第一个跨度)同样提供了一些值得关注的信息:

在“标签”下,可以看到以下属性:
http.method字段:POST(同样,按照REST术语,这表示创建)http.route字段:/conversations/:conversationId/messages(消息路由)http.target字段:/conversations/1/messages(消息端点)
这些信息符合目标要求,因为它表明messenger服务参与了该流程,并且命中的端点是新消息端点。
目标2:验证流程是否在5秒内执行完毕
如下方截图所示,跟踪中的messenger部分从1.28 ms开始,到36.28 ms结束,总耗时为35 ms。大部分时间用于解析JSON(middleware - jsonParser),而连接数据库所用的时间要多得多(pg-pool.connect和tcp.connect)。
这是合理的,因为在写入消息的过程中还会执行多条SQL查询。因此,可以考虑扩展自动插桩配置,以捕获这些查询的耗时。(本教程未展示这项额外的插桩操作,但在挑战4中,您将手动创建span,随后可用这些span封装数据库查询。)

这些信息达到了目标,因为它表明messenger操作耗时远不到5秒。如果流程中存在耗时很长的操作,那么它一定发生在后续环节。
目标3:查看notifier服务读取messenger服务分派的事件需要多长时间
与NGINX span一样,messenger span也不包含此信息,因此可以转到notifier span。
检查跟踪中的notifier部分
目标1:查看请求在新消息流中经历的所有步骤
跟踪记录的notifier部分仅包含两个跨度:

- The
chat_queueprocessspan,确认notifier服务已处理来自chat_queue消息队列的事件 - The
pg-pool.connectspan 表明,在处理该事件后,notifier服务以某种方式连接到了其数据库
这些跨度提供的信息只能部分满足了解每个步骤的目标。可以看出,notifier服务已执行到从队列中消费事件的阶段,但无法确定:
- 此服务发送的消息通知是否与messenger服务分派的事件相对应
- 相关消息通知已正确发送给消息接收者
这表示,要全面了解notifier服务流程,需要执行以下操作:
- 手动检测显示通知发送过程的span
- 确保由messenger服务分派的事件与由notifier服务使用的事件之间通过跟踪 ID 建立明确关联
目标2:验证流程是否在5秒内执行完毕
查看notifier服务跨度的总体耗时,可以看到请求在流程的notifier部分耗时30.77 ms。但是,由于没有跨度标志着整个流程“结束”(即向接收者发送通知),因此无法确定该流程部分的总耗时,也无法确定该操作的总体完成时间。
目标3:查看notifier服务读取messenger服务分派的事件所需的时间
不过,可以看到,chat_queue的process跨度(属于notifier服务)从6.12 ms开始,比chat_queue的send跨度(属于messenger服务)从4.12 ms开始晚2 ms。

此目标已实现,因为您知道该事件由notifier在messenger服务发出2 ms后消费。与目标2不同,实现此目标无需知道该事件是否已完成处理,也无需知道处理耗时。
结论
根据我们对当前OTel自动插桩所生成跟踪数据的分析,可以明确得出以下结论:
- 其中许多span以当前形式并无实际用途:
- NGINX会生成与授权检查和文件服务等功能相关的跨度,但这些跨度与您关注的反向代理角色无关。不过,目前NGINX的OTel插桩不支持忽略无关跨度,因此暂时无法处理。
- 在Node.js服务(messenger和notifier服务)的跨度中,有些似乎与目标相关,包括JSON解析、
requesthandler以及所有数据库操作的跨度。有些中间件跨度(例如expressInit和corsMiddleware)似乎与目标无关,可以移除。
- 以下项目缺少关键跨度:
- 由notifier服务发送的通知
- 明确映射由messenger服务分发的RabbitMQ事件与由notifier服务处理的事件
这意味着,基础插桩实现了最后一个目标:
- 查看notifier服务开始处理由messenger服务分派的事件需要多长时间
但是,现有信息不足以实现前两个目标:
- 了解请求在新消息流中经历的所有步骤
- 确信在正常情况下,该流程可在5秒内完成端到端执行
挑战4:根据跟踪数据优化检测埋点
在本挑战中,您将根据挑战3中的跟踪分析结果优化OTel插桩,包括移除不必要的跨度、创建新的自定义跨度,以及确认通知服务使用的事件是否由消息服务生成。
移除不必要的跨度
- 在常用的文本编辑器中,打开tracing.mjs文件。该文件位于app目录下的messenger存储库中。然后,在文件顶部的import语句列表末尾添加以下内容:
const IGNORED_EXPRESS_SPANS = new Set([ "middleware - expressInit",
"middleware - corsMiddleware",
]);- 这定义了一组跨度名称,这些名称取自以下Jaeger UI截图中显示的跨度列表。由于它们无法为此流程提供有用信息,因此将从跟踪中省略。您也可以判断截图中列出的其他跨度是否同样不需要,并将其添加到
IGNORED_EXPRESS_SPANS列表中。
- 在自动插桩配置中添加过滤器,以忽略不需要的跨度。请修改橙色突出显示的行:
const sdk = new opentelemetry.NodeSDK({ resource,
traceExporter: new OTLPTraceExporter({ headers: {} }),
instrumentations: [getNodeAutoInstrumentations()],
});- 更改为:
const sdk = new opentelemetry.NodeSDK({ resource,
traceExporter: new OTLPTraceExporter({ headers: {} }),
instrumentations: [ getNodeAutoInstrumentations({ "@opentelemetry/instrumentation-express": { ignoreLayers: [ (name) => { return IGNORED_EXPRESS_SPANS.has(name); }, ], }, }), ],
});- The
getNodeAutoInstrumentations函数引用步骤 1 中定义的 span 集合,将这些 span 从@opentelemetry/instrumentation-express生成的跟踪中滤除。换言之,对于属于return语句的结果为true的 span,IGNORED_EXPRESS_SPANS会将其识别为需要忽略的 span,而ignoreLayers语句会从跟踪中移除该 span。 - 在messenger终端中,按
Ctrl+c停止messenger服务。然后重新启动该服务:
^cnode --import ./tracing.mjs index.mjs- 等待约10秒,然后在客户端终端中发送一条新消息:
curl -X POST \ -H "User-Id: 2" \ -H "Content-Type: application/json" \ -d '{"content": "This is the second message"}' \ 'http://localhost:8085/conversations/1/messages'- 在Jaeger UI中重新检查messenger跨度。两个
middleware跨度expressInit和corsMiddleware已不再显示(可将其与挑战3中“目标2”的截图进行比较,该目标位于“检查跟踪的messenger部分”。
设置自定义Span
在本节中,您将首次接触应用程序代码。自动插桩无需更改应用程序即可生成大量信息,但有些洞察只有通过对特定业务逻辑进行插桩才能获得。
对于正在检测的新消息流,一个示例是跟踪向消息接收者发送通知的过程。
- 打开index.mjs,该文件位于app目录下的notifier存储库中。此文件包含该服务的所有业务逻辑。在文件顶部的
import语句列表末尾添加以下代码行:
import { trace } from "@opentelemetry/api";- 替换以下代码(大约位于文件第 91 行):
for (let pref of preferences) {
console.log(
`Sending notification of new message via ${pref.address_type} to ${pref.address}`
);
}- 替换为:
const tracer = trace.getTracer("notifier"); // 1tracer.startActiveSpan( // 2
"notification.send_all",
{
attributes: {
user_id: msg.user_id,
},
},
(parentSpan) => {
for (let pref of preferences) {
tracer.startActiveSpan( // 3
"notification.send",
{
attributes: { // 4
notification_type: pref.address_type,
user_id: pref.user_id,
},
},
(span) => {
console.log(
`Sending notification of new message via ${pref.address_type} to ${pref.address}`
);
span.end(); // 5
}
);
}
parentSpan.end(); // 6
}
);- 新代码的作用如下:父 span 可确保即使未发现用户的任何通知偏好设置,也会报告每次“发送通知”操作。
- 获取
tracer,这是一个用于与OTel跟踪交互的全局对象。 - 启动一个名为
notification.send_all的新父级跨度,并设置user_id属性以标识消息发送者。 - 进入循环,逐一处理收件人的通知偏好,并在
notification.send下创建一个名为notification.send_all的新子跨度。每条通知都会生成一个新跨度。 - 为子跨度设置更多属性:
notification_type:以下值之一:sms或emailuser_id– 接收通知的用户ID
- 依次结束每个子
notification.send跨度。 - 关闭父级
notification.send_all跨度。
- 获取
- 在notifier终端中,按
Ctrl+c停止notifier服务。然后重新启动该服务:
^cnode --import ./tracing.mjs index.mjs- 等待约10秒,然后在客户端终端中发送一条新消息:
curl -X POST \ -H "User-Id: 2" \ -H "Content-Type: application/json" \ -d '{"content": "This is the second message"}' \ 'http://localhost:8085/conversations/1/messages'- 在Jaeger UI中重新检查notifier跨度。可以看到一个父跨度和两个子跨度,每个跨度都包含“发送通知”操作:

现在可以完全实现第一和第二个目标,因为您可以看到请求在新消息流中经历的所有步骤。每个跨度的计时信息都会显示这些步骤之间的任何延迟。
确认messenger和notifier正在处理同一事件
要全面了解该流程,还需确认一点:notifier服务正在处理的事件,是否确实是由messenger服务分派的?
无需进行任何显式更改即可关联这两条跟踪,但也不能盲目相信自动插桩的神奇效果。
基于这一点,添加一些简单的调试代码,验证从NGINX服务开始的跟踪与notifier服务接收的跟踪确实相同(具有相同的跟踪ID)。
- 打开index.mjs文件,该文件位于app目录下的messenger存储库中,然后进行以下更改:
- 在顶部的import语句列表末尾添加以下行:
import { trace } from "@opentelemetry/api";- 在现有黑色行下方添加以橙色突出显示的行:
async function createMessageInConversation(req, res) {
const tracer = trace.getActiveSpan(); console.log("TRACE_ID: ", tracer.spanContext().traceId);- 新增的代码行会输出
TRACE_ID,该值来自messenger中负责创建新消息的函数。
- 打开index.mjs文件(位于notifier存储库的app目录中),然后在现有的黑色代码行下方添加以橙色突出显示的代码行:
export async function handleMessageConsume(channel, msg, handlers) { console.log("RABBIT_MQ_MESSAGE: ", msg);- 新增的代码行会输出notifier服务收到的AMQP事件的完整内容。
- 停止并重新启动messenger和notifier服务。请在messenger和notifier终端中运行以下命令:
^cnode --import ./tracing.mjs index.mjs- 等待约10秒,然后在客户端终端中再次发送消息:
curl -X POST \ -H "User-Id: 2" \ -H "Content-Type: application/json" \ -d '{"content": "This is the second message"}' \ 'http://localhost:8085/conversations/1/messages'- 查看messenger和notifier服务的日志。messenger服务日志中包含类似下面的一行,用于报告消息的跟踪 ID(运行本教程时,实际 ID 会有所不同):
TRACE_ID: 29377a9b546c50be629c8e64409bbfb5- 同样,notifier服务日志会在如下输出中报告跟踪ID:
_spanContext: {
traceId: '29377a9b546c50be629c8e64409bbfb5',
spanId: 'a94e9462a39e6dbf',
traceFlags: 1,
traceState: undefined
},- 控制台中的跟踪ID相匹配。最后,还可以将其与Jaeger UI中的跟踪ID进行比较。通过相应的跟踪ID端点打开UI(实际端点会有所不同,本例中为http://localhost:16686/trace/29377a9b546c50be629c8e64409bbfb5),即可查看完整跟踪。Jaeger跟踪确认:
- 当事件被分派时,messenger服务中的AMQP自动插桩会将此跟踪ID添加到元数据中。
- The AMQP auto‑instrumentation in the notifier服务中的AMQP自动插桩会读取该元数据,并相应设置跟踪上下文。
注意:在实际生产系统中,确认流程按预期运行后,应删除本节中添加的代码。
资源清理
在本教程中,您已创建了多个容器和镜像。请按照以下说明将其删除。
- 要移除所有正在运行的Docker容器:
docker rm $(docker stop messenger-lb)- 要移除platform服务以及messenger和notifier数据库服务:
cd ~/microservices-march/platform && docker compose down cd ~/microservices-march/notifier && docker compose down cd ~/microservices-march/messenger && docker compose down后续步骤
恭喜,您已完成本教程!
- 您已为一个NGINX反向代理和两个Node.js服务设置OTel插桩。
- 您审慎分析了OTel自动插桩提供的数据,并补充了部分缺失的遥测数据,以实现OTel实验目标:
- 无需直接更改任何应用程序代码,您便能较为清晰地了解特定请求在消息传递系统中的流转过程。
- 您已确认,在正常情况下,该流程的端到端执行时间不到5秒。
不过,这还只是理想追踪配置的初步探索。在生产环境中,您可能还需要为每个数据库查询添加自定义跨度,并为所有跨度添加额外的元数据,用于描述运行时详细信息,例如各项服务的容器ID。还可以实施另外两类OTel数据(指标和日志),以全面了解系统的运行状况。
要继续学习微服务,请参阅Microservices March 2023。在“第4单元:利用可观测性管理微服务的混乱与复杂性”中,您将了解可观测性数据的三大主要类别、基础设施与应用协调一致的重要性,以及如何开始分析深层数据。

相关博客文章
NGINX Plus R26 正式发布
NGINX Plus R26通过缓存JSON Web密钥集加快JWT验证,并强化TLS握手,从而提升安全性。NGINX JavaScript的新功能包括增强对异步函数的支持,以及实现WebCrypto API。
使用NGINX App Protect DoS保护gRPC应用,抵御严重的DoS攻击
gRPC具备开发人员设计和部署现代应用所需的速度与灵活性,但其开放性也使其极易受到第7层DoS攻击。NGINX App Protect DoS可保护gRPC应用免受此类破坏性攻击。
Ingress Controller 选型指南(第 2 部分):风险与面向未来的规划
了解选择错误的Ingress控制器可能带来的风险,以及如何从关键方面着手,让您的选择更好地适应未来需求。
NGINX Plus R19 正式发布
NGINX Plus R19扩展了监控功能,不仅提供更多指标,还新增了指标分析方式;增加试运行模式,用于测试限速效果;增强键值存储,以支持IP地址范围;并使限速更加动态。
NGINX现已正式加入F5
NGINX现已正式成为F5的一部分。Gus Robertson现任F5 NGINX业务部门负责人。他阐述了两家公司在人才、文化和愿景方面融合的价值,以及双方将如何携手增强NGINX的开源产品和商业产品。
