大家好!我是 MY.GAMES 的 Java 开发人员 Dmitriy Apanasevich,正在开发游戏 Rush Royale,我想分享我们将 OpenTelemetry 框架集成到 Java 后端的经验。这里有很多内容需要介绍:我们将介绍实现它所需的必要代码更改,以及我们需要安装和配置的新组件 - 当然,我们还会分享一些结果。 我们的目标:实现系统可观测性 让我们为我们的案例提供更多背景信息。作为开发人员,我们希望创建易于监控、评估和理解的软件(这正是实施 OpenTelemetry 的目的——最大限度地提高系统 )。 可观察性 收集应用程序性能见解的传统方法通常涉及手动记录事件、指标和错误: 当然,有很多框架允许我们使用日志,我相信阅读本文的每个人都有一个配置好的用于收集、存储和分析日志的系统。 日志记录也为我们完全配置,因此我们没有使用 OpenTelemetry 提供的处理日志的功能。 监控系统的另一种常见方式是利用指标: 我们还有一个用于收集和可视化指标的完整配置系统,因此在这里我们也忽略了 OpenTelemetry 在处理指标方面的功能。 但获取和分析此类系统数据的一个不太常见的工具是 。 踪迹 跟踪表示请求在其生命周期内通过系统的路径,通常从系统收到请求开始,到响应结束。跟踪由多个 ,每个跨度代表由开发人员或其选择的库确定的特定工作单元。这些跨度形成一个层次结构,有助于直观地了解系统如何处理请求。 跨度 在本次讨论中,我们将集中讨论 OpenTelemetry 的跟踪方面。 关于 OpenTelemetry 的一些背景信息 让我们也来了解一下 OpenTelemetry 项目,该项目是由 和 项目。 开放追踪 开放人口普查 OpenTelemetry 现在基于标准提供了全面的组件,该标准为各种编程语言定义了一组 API、SDK 和工具,该项目的主要目标是生成、收集、管理和导出数据。 也就是说,OpenTelemetry 不提供数据存储或可视化工具的后端。 由于我们只对跟踪感兴趣,因此我们探索了用于存储和可视化跟踪的最流行的开源解决方案: 耶格尔 齐普金 Grafana Tempo 最终,我们选择了 Grafana Tempo,因为它具有出色的可视化功能、快速的开发速度以及与我们现有的 Grafana 设置集成以实现指标可视化。拥有一个统一的工具也是一个显著的优势。 OpenTelemetry 组件 让我们稍微分析一下 OpenTelemetry 的组件。 规格: API — 数据类型、操作、枚举 SDK — 规范实现,不同编程语言的 API。不同的语言意味着不同的 SDK 状态,从 alpha 到稳定版。 数据协议 (OTLP) 和 语义惯例 Java API SDK: 代码检测库 导出器——将生成的跟踪导出到后端的工具 跨服务传播器——一种在进程(JVM)外传输执行上下文的工具 是一个重要组件,它是一个接收数据、处理数据并传递数据的代理——让我们仔细看看。 OpenTelemetry Collector OpenTelemetry 收集器 对于每秒处理数千个请求的高负载系统,管理数据量至关重要。跟踪数据的数量通常超过业务数据,因此确定要收集和存储哪些数据的优先级至关重要。这就是我们的数据处理和过滤工具发挥作用的地方,它使您能够确定哪些数据值得存储。通常,团队希望存储符合特定条件的跟踪,例如: 响应时间超过特定阈值的跟踪。 处理过程中遇到错误的跟踪。 包含特定属性的跟踪,例如通过某个微服务或在代码中被标记为可疑的跟踪。 随机选择的常规跟踪可提供系统正常运行的统计快照,帮助您了解典型行为并识别趋势。 以下是用来确定要保存哪些跟踪以及要丢弃哪些跟踪的两种主要采样方法: ——在跟踪开始时决定是否保留它 头部采样 ——只有在获得完整跟踪后才做出决定。当决策取决于跟踪中稍后出现的数据时,尾部采样是必要的。例如,包括错误跨度的数据。这些情况无法通过头部采样来处理,因为它们需要先分析整个跟踪 尾部采样 OpenTelemetry Collector 可帮助配置数据收集系统,以便它仅保存必要的数据。我们稍后会讨论其配置,但现在,让我们先讨论一下需要在代码中进行哪些更改才能开始生成跟踪的问题。 零代码检测 获取跟踪生成实际上只需要很少的编码——只需要使用 java-agent 启动我们的应用程序,指定 : 配置 -javaagent:/opentelemetry-javaagent-1.29.0.jar -Dotel.javaagent.configuration-file=/otel-config.properties OpenTelemetry 支持大量 因此,在使用代理启动应用程序后,我们立即收到了有关服务之间、DBMS 中处理请求阶段的数据跟踪。 库和框架 在我们的代理配置中,我们禁用了我们不想在跟踪中看到其跨度的库,为了获取有关我们的代码如何工作的数据,我们将其标记为 : 注释 @WithSpan("acquire locks") public CompletableFuture<Lock> acquire(SortedSet<Object> source) { var traceLocks = source.stream().map(Object::toString).collect(joining(", ")); Span.current().setAttribute("locks", traceLocks); return CompletableFuture.supplyAsync(() -> /* async job */); } 在这个例子中,方法使用了 注释,表示需要创建一个名为“ ”的新跨度,并在方法主体中为创建的跨度添加“ ”属性。 @WithSpan acquire locks locks 当方法完成工作时,span 就会关闭,对于异步代码来说,注意这个细节很重要。如果您需要在从带注释的方法调用的 lambda 函数中获取与异步代码工作相关的数据,则需要将这些 lambda 分离到单独的方法中,并使用附加注释对其进行标记。 我们的跟踪收集设置 现在,我们来谈谈如何配置整个跟踪收集系统。我们所有的 JVM 应用程序都是通过 Java 代理启动的,该代理会将数据发送到 OpenTelemetry 收集器。 但是,单个收集器无法处理大量数据流,因此系统的这一部分必须扩展。如果为每个 JVM 应用程序启动单独的收集器,尾部采样将会中断,因为跟踪分析必须在一个收集器上进行,如果请求经过多个 JVM,则一条跟踪的跨度将最终出现在不同的收集器上,并且无法对其进行分析。 这里, 平衡器来救援了。 收集器已配置 结果,我们得到了以下系统:每个 JVM 应用程序将数据发送到同一个平衡器收集器,该收集器的唯一任务是将从不同应用程序接收但与给定跟踪相关的数据分发到同一个收集器处理器。然后,收集器处理器将数据发送到 Grafana Tempo。 让我们仔细看看这个系统中组件的配置。 负载均衡收集器 在收集器-平衡器配置中,我们配置了以下主要部分: receivers: otlp: protocols: grpc: exporters: loadbalancing: protocol: otlp: tls: insecure: true resolver: static: hostnames: - collector-1.example.com:4317 - collector-2.example.com:4317 - collector-3.example.com:4317 service: pipelines: traces: receivers: [otlp] exporters: [loadbalancing] — 配置方法(收集器可通过该方法接收数据)。我们仅以 OTLP 格式配置了数据接收。(可以通过以下方式配置数据接收 ,例如 Zipkin、Jaeger。) 接收器 许多其他协议 — 配置中配置数据平衡的部分。在本节指定的收集器-处理器中,数据根据从跟踪标识符计算出的哈希值进行分配。 导出器 指定了服务如何工作的配置:仅使用跟踪,使用在顶部配置的 OTLP 接收器并将数据作为平衡器传输,即不进行处理。 服务部分 具有数据处理功能的采集器 收集器-处理器的配置更加复杂,我们来看一下: receivers: otlp: protocols: grpc: endpoint: 0.0.0.0:14317 processors: tail_sampling: decision_wait: 10s num_traces: 100 expected_new_traces_per_sec: 10 policies: [ { name: latency500-policy, type: latency, latency: {threshold_ms: 500} }, { name: error-policy, type: string_attribute, string_attribute: {key: error, values: [true, True]} }, { name: probabilistic10-policy, type: probabilistic, probabilistic: {sampling_percentage: 10} } ] resource/delete: attributes: - key: process.command_line action: delete - key: process.executable.path action: delete - key: process.pid action: delete - key: process.runtime.description action: delete - key: process.runtime.name action: delete - key: process.runtime.version action: delete exporters: otlp: endpoint: tempo:4317 tls: insecure: true service: pipelines: traces: receivers: [otlp] exporters: [otlp] 与收集器-平衡器配置类似,处理配置由接收器、导出器和服务部分组成。不过,我们将重点介绍处理器部分,该部分解释了如何处理数据。 首先, 部分演示了一个 允许过滤存储和分析所需的数据: tail_sampling 配置 :此规则选择延迟超过 500 毫秒的跟踪。 Latency500-policy :此规则选择在处理过程中遇到错误的跟踪。它在跟踪范围中搜索名为“error”且值为“true”或“True”的字符串属性。 error-policy :此规则随机选择所有跟踪的 10%,以提供对正常应用程序操作、错误和长请求处理的洞察。 probabilistic10-policy 除了 tail_sampling 之外,此示例还显示了 部分,用于删除数据分析和存储不需要的不必要属性。 resource/delete 结果 生成的 Grafana 跟踪搜索窗口可让您按各种条件过滤数据。在此示例中,我们仅显示从大厅服务收到的跟踪列表,该服务处理游戏元数据。该配置允许将来按延迟、错误和随机采样等属性进行过滤。 跟踪视图窗口显示大厅服务的执行时间线,包括构成请求的各个跨度。 从图中可以看出,事件的顺序如下 - 获取锁,然后从缓存中检索对象,接着执行处理请求的事务,之后将对象再次存储在缓存中并释放锁。 其中数据库请求相关的span是通过标准库的instrumentation自动生成的,而锁管理、缓存操作、事务发起相关的span,则是需要通过前面提到的注解,手动添加到业务代码中。 查看跨度时,您可以看到一些属性,这些属性可以让您更好地了解处理过程中发生的情况,例如,查看数据库中的查询。 Grafana Tempo 的一个有趣功能是 ,以图形方式显示所有导出跟踪的服务、它们之间的连接以及请求的速率和延迟: 服务图 总结 正如我们所见,使用 OpenTelemetry 跟踪极大地增强了我们的观察能力。通过最少的代码更改和结构良好的收集器设置,我们获得了深刻的见解 - 此外,我们还看到了 Grafana Tempo 的可视化功能如何进一步补充我们的设置。谢谢阅读!