大家好!我是 MY.GAMES 的 Java 开发人员 Dmitriy Apanasevich,正在开发游戏 Rush Royale,我想分享我们将 OpenTelemetry 框架集成到 Java 后端的经验。这里有很多内容需要介绍:我们将介绍实现它所需的必要代码更改,以及我们需要安装和配置的新组件 - 当然,我们还会分享一些结果。
让我们为我们的案例提供更多背景信息。作为开发人员,我们希望创建易于监控、评估和理解的软件(这正是实施 OpenTelemetry 的目的——最大限度地提高系统
收集应用程序性能见解的传统方法通常涉及手动记录事件、指标和错误:
当然,有很多框架允许我们使用日志,我相信阅读本文的每个人都有一个配置好的用于收集、存储和分析日志的系统。
日志记录也为我们完全配置,因此我们没有使用 OpenTelemetry 提供的处理日志的功能。
监控系统的另一种常见方式是利用指标:
我们还有一个用于收集和可视化指标的完整配置系统,因此在这里我们也忽略了 OpenTelemetry 在处理指标方面的功能。
但获取和分析此类系统数据的一个不太常见的工具是
跟踪表示请求在其生命周期内通过系统的路径,通常从系统收到请求开始,到响应结束。跟踪由多个
在本次讨论中,我们将集中讨论 OpenTelemetry 的跟踪方面。
让我们也来了解一下 OpenTelemetry 项目,该项目是由
OpenTelemetry 现在基于标准提供了全面的组件,该标准为各种编程语言定义了一组 API、SDK 和工具,该项目的主要目标是生成、收集、管理和导出数据。
也就是说,OpenTelemetry 不提供数据存储或可视化工具的后端。
由于我们只对跟踪感兴趣,因此我们探索了用于存储和可视化跟踪的最流行的开源解决方案:
最终,我们选择了 Grafana Tempo,因为它具有出色的可视化功能、快速的开发速度以及与我们现有的 Grafana 设置集成以实现指标可视化。拥有一个统一的工具也是一个显著的优势。
让我们稍微分析一下 OpenTelemetry 的组件。
规格:
API — 数据类型、操作、枚举
SDK — 规范实现,不同编程语言的 API。不同的语言意味着不同的 SDK 状态,从 alpha 到稳定版。
数据协议 (OTLP) 和
Java API SDK:
OpenTelemetry Collector是一个重要组件,它是一个接收数据、处理数据并传递数据的代理——让我们仔细看看。
对于每秒处理数千个请求的高负载系统,管理数据量至关重要。跟踪数据的数量通常超过业务数据,因此确定要收集和存储哪些数据的优先级至关重要。这就是我们的数据处理和过滤工具发挥作用的地方,它使您能够确定哪些数据值得存储。通常,团队希望存储符合特定条件的跟踪,例如:
以下是用来确定要保存哪些跟踪以及要丢弃哪些跟踪的两种主要采样方法:
OpenTelemetry Collector 可帮助配置数据收集系统,以便它仅保存必要的数据。我们稍后会讨论其配置,但现在,让我们先讨论一下需要在代码中进行哪些更改才能开始生成跟踪的问题。
获取跟踪生成实际上只需要很少的编码——只需要使用 java-agent 启动我们的应用程序,指定
-javaagent:/opentelemetry-javaagent-1.29.0.jar
-Dotel.javaagent.configuration-file=/otel-config.properties
OpenTelemetry 支持大量
在我们的代理配置中,我们禁用了我们不想在跟踪中看到其跨度的库,为了获取有关我们的代码如何工作的数据,我们将其标记为
@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]
收集器-处理器的配置更加复杂,我们来看一下:
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部分演示了一个
Latency500-policy :此规则选择延迟超过 500 毫秒的跟踪。
error-policy :此规则选择在处理过程中遇到错误的跟踪。它在跟踪范围中搜索名为“error”且值为“true”或“True”的字符串属性。
probabilistic10-policy :此规则随机选择所有跟踪的 10%,以提供对正常应用程序操作、错误和长请求处理的洞察。
除了 tail_sampling 之外,此示例还显示了resource/delete部分,用于删除数据分析和存储不需要的不必要属性。
生成的 Grafana 跟踪搜索窗口可让您按各种条件过滤数据。在此示例中,我们仅显示从大厅服务收到的跟踪列表,该服务处理游戏元数据。该配置允许将来按延迟、错误和随机采样等属性进行过滤。
跟踪视图窗口显示大厅服务的执行时间线,包括构成请求的各个跨度。
从图中可以看出,事件的顺序如下 - 获取锁,然后从缓存中检索对象,接着执行处理请求的事务,之后将对象再次存储在缓存中并释放锁。
其中数据库请求相关的span是通过标准库的instrumentation自动生成的,而锁管理、缓存操作、事务发起相关的span,则是需要通过前面提到的注解,手动添加到业务代码中。
查看跨度时,您可以看到一些属性,这些属性可以让您更好地了解处理过程中发生的情况,例如,查看数据库中的查询。
Grafana Tempo 的一个有趣功能是
正如我们所见,使用 OpenTelemetry 跟踪极大地增强了我们的观察能力。通过最少的代码更改和结构良好的收集器设置,我们获得了深刻的见解 - 此外,我们还看到了 Grafana Tempo 的可视化功能如何进一步补充我们的设置。谢谢阅读!