OTel Profiling 技术发展介绍
OTel Profiling 简介
OTel 社区从 2022 年在 KubeCon + CloudNativeCon Europe 2022 in Valencia 大会开始关注 Profiling 领域的建设,在那次的会议上成立了 Profiling SIG,这个兴趣小组成为了后续推动 OTel 在 Profiling 方面建设发展的主要推动力量。该 SIG 的主要活跃贡献者目前主要来自于 Grafana Labs、Elastic、Red Hat 以及 DataDog 等。
Profiling SIG 资料
OTel Profiling 演进介绍
下图展示了OTel Profiling发展过程中的一些关键节点:
2024年以来,OTel Profiling 发展很快,完成了包括新版 Data model 设计、Data model 合入 OTel社区Spec、基于 Spec 开发对应实现等工作。接下来,结合OTel社区的Spec发起流程,介绍上述相关时间节点细节:
OpenTelemetry 项目对一种新数据类型的支持过程分为以下步骤:
编写并向社区提交 OpenTelemetry Enhancement Proposal(OTEP)。OpenTelemetry OTEP 旨在进行跨领域的变更(即跨语言和实现适用),并且引入新行为、更改所需行为或以其他方式修改需求。基于 OTEP 编写对应的 Specification(Spec)。Spec 规范描述了所有 OpenTelemetry 实现的跨语言要求和期望。基于 OTEP 实现各种语言相关 Profiling 数据采集和传输处理能力。
1. 发起 OTEP 与社区响应
由 Grafana 等公司起草的 Profile 领域数据模型提案经过 3 个多月的讨论和 review,已经在 3 月被社区通过进行了合并 #237、#239。这套标准定义的数据格式跟 pprof 比较接近。详细细节见相关PR。
目前社区对这个领域很看重,2024年3月,在官网通过Blog文章文档形式,介绍和宣布 Profilers 将作为OTel社区继 Traces/Metrics/Logs 后的重要组成部分。
2. 将 OTEP 集成到规范中
在完成 OTEP 的提交和社区合并以后,下一步需要在社区的 Spec 中添加相关 OTEP。目前这块 OTel-Profiles 小组是选择先确定提交 OpenTelemetry Protocol (OTLP) Specification #534,其作为 Spec 中的一部分,旨在描述数据在采集测、中间节点(例如收集器和后端)之间的编码、传输和传递机制。OTel-Profiles 小组希望先确定协议内容后,在 OTel-Collector 中进行基于相关数据传输协议标准的数据收集实现,以跑通整体流程。相关更多细节可以参见 Profiling SIG Meeting Notes。
3. 基于 OTEP 进行数据采集/收集测的实现
目前社区目前社区在相关部分的实现包括:
Splunk 已经针对 .Net 探针捐献了一个版本的实现,详情见#3196,相关代码已合并。
Elastic 向社区捐献一个基于 eBPF 实现的 Profiling 探针实现,详细讨论内容见#1918,该实现opentelemetry-ebpf-profiler已于6月份被社区接受,成为OTel社区第一个相关实现。
OTel Data Model 介绍
OTel Data Model是在Google pprof基础上设计而来,因此,本部分将介绍 pprof 相关基础内容,然后介绍OTel Data Model 相关 pprof 有哪些差异。
pprof简介
pprof 是 Go 语言内置的性能剖析工具,可以帮助分析 Go 程序的 CPU、内存、阻塞、和锁等维度的性能。
接下来,用一个例子来说明 pprof 的使用。比如下面的代码会对10万个数字进行排序,运行时间大概是接近4秒。
如果想对代码进行优化,就可以使用pprof进行CPU热点剖析。程序运行结束后,会生成一个pprof格式的性能剖析文件/tmp/cpu.pprof。
可以用命令行对这个文件进行分析,找到占用CPU的热点方法。从下图可以看到,main.doWork方法占用了2800毫秒,也就是说大多数时间都消耗在这个方法上。
可以进一步分析,找出占用CPU的代码行。从下图可以看到,有1.67秒的时间在执行第42行,有990毫秒在执行第43行。结合代码,很容易看出,问题在于使用了冒泡排序,时间复杂度是N的平方。
只需要更换更高效的排序算法,就应该会大大提升性能,运行时间从4秒缩短为了15毫秒左右。
实际上,pprof 的 CPU 热点启动后,Go 会每隔10毫秒记录一次协程的方法栈,最终写入到 pprof 格式的文件中。下面对 pprof 格式做一些说明:
Profile
代表整个 pprof 文件,由多个 Sample 组成。
Sample
代表的是一次方法栈的快照。所谓方法栈,就是多个存在调用关系的方法,比如a() -> b() -> c()。
Label
在 Tracing 和 Profling 关联的场景,方法栈还有附加信息,比如当前的 TraceId 和 SpanId 等,这些信息则存储在 Label 中,结构有点像一个 map。
Location
pprof 在采样时,会记录c函数中正在执行的指令的地址,也对应的记录 b 和 a 函数的指令的地址。把上述的函数指令地址叫做 Location。
Mapping
实际上,不同的函数可能存在于不同的动态库中,为此也需要将动态库的文件名,动态库在内存中的地址范围等信息记录下来,把这些信息叫做 Mapping。
需要说明的是,由于编译器会对函数进行编译优化,比如内联等,一个 Location 可能表示多个函数及行号,所以 Location 和 Line 是1对多的关系。
也可以将 pprof 文件转换成火焰图,阅读起来会更加直观。
如果想对 pprof 有更多了解,可以阅读文章做更详尽了解。
pprof 和 OTel Format 对比
两种格式的核心功能都是记录 Sample(也就是方法栈),以及和 Sample 相关的附加信息。
在上图中,左边是 pprof,右边是 OTel Format。通过对比可以看到,OTel Format 中保留了几乎所有 pprof的内容,也就是说 OTel Format 可以无损的转换为 pprof,但反过来则不行,因为 OTel Format 中增加了pprof 所没有的一些结构(上图中标记为1的部分)。
除此以为,OTel Format 对 pprof 也有一些优化
Label 的存储更加优化,可以节省更多空间,图中标记为2的部分
Location 的存储更加优化,可以节省更多空间,图中标记为3的部分
直接在每个样本中增加了和 Trace 关联的字段,以及时间字段,虽然 pprof 也能实现,但 OTel 的格式更加简单直接,图中标记为4的部分
还有一些其它的扩展点,比如可以将 JFR 文件的二进制数据直接保存到 OTel 的格式中,等等