背景
在 2018 年的 2 月,ARMS Java Agent的第一个版本正式发布,为用户提供无侵入的的可观测数据采集服务。6 年后的今天,随着软件技术的迅猛发展、业务场景的逐渐丰富、用户规模的快速增长,我们逐渐发现过去的功能以及架构的设计逐渐难以合理、优雅的满足今天的需求,重构越来越多的被提及,但总是缺少一个合理的契机。
适时,OTel项目异军突起,其社区经过短短四年的发展,活跃度位列CNCF第二,逐渐成为可观测领域的开源标准。OTel(OpenTelemetry)是一个位于云原生计算基金会(CNCF)的开放源代码项目,旨在标准化遥测数据的收集、处理和导出的方式。其贡献者由来自不同公司和组织的成员组成,他们共同协作创建和维护用于分布式追踪、指标和日志的 API、SDK 和工具。他们的目标是使可观测性更加易于访问并整合到云原生软件开发中,从而使用户能够更有效地监控、调试和优化他们的应用程序。
同时,我们调研了国外APM领域的一些头部商业化公司,比如 New Relic、Grafana、Elasticsearch 以及部分云厂商比如 Azure、AWS 都已经开始基于 OTel Java Agent 打造自己的商业化探针。
这些现象也极大的引起了我们的兴趣, 促使我们对OTel Java Agent进行了一次深入的调研。
关于OTel Java Agent
首先我们对比了OTel Java Agent和ARMS 现有探针在Tracing、Metrics、Logs、Profiling以及其他五个方面的功能差异,如下表所示。可以看到,简单的从功能层面来说,OTel Java Agent依托社区广大的贡献者,在插件数量上远远领先。此外,基于一些领先的埋点技术,对于各种异步场景支持较好;ARMS 探针则依托广大的商业化用户场景和多年服务集团内外客户的经验,在采样,多协议支持、指标丰富度以及各种三方功能集成方面比较领先。
| 功能大类 | 功能子类 | ARMS (3.1.4) | OTel Java Agent(1.28.0) |
| Tracing | 插件支持 | ≈60 | ≈128 |
| 采样能力 | 头采样、尾采样 | 头采样 | |
| TraceId 透传协议 | 支持 eagleeye, w3c, zipkin, skywalking,Jaeger,自适应切换,自定义配置 | w3c、zipkin、Jaeger | |
| 异步透传 | 手动配置 | 自动 | |
| 兼容 OTel SDK | 部分支持 | 全支持 | |
| 动态新增 span | 支持 | 不支持 | |
| Metrics | RED 指标 | 支持 | 支持 |
| 线程相关指标 | 支持 | 不支持 | |
| 连接池指标 | 支持 | 支持 | |
| 异常指标 | 支持 | 不支持 | |
| 系统/JVM指标 | 支持 | 支持 | |
| JMX 指标 | 不支持 | 支持 | |
| 动态新增指标 | 不支持 | 不支持 | |
| logs | MDC | 支持 | 支持 |
| 自动采集 | 不支持 | 支持 | |
| profiling | 支持 Profiling 事件类型数量 | 5 类 CPU耗时 内存样本数 内存分配大小 应用级墙钟 代码热点 | 不支持 |
| 关联 traceId | 支持 | 不支持 | |
| event | event 类型数量 | 简单 | 不支持 |
| other | 应用安全 | 支持 | 不支持 |
| Arthas | 支持 | 不支持 | |
| 微服务治理 | 支持 | 不支持 | |
| 内存 dump | 支持 | 不支持 |
除了上面列举的功能对比,在对OTel Java Agent调研中,我们也发现他有很多领先的设计在解决埋点生效判断、异步、类隔离等问题时十分方便。这里简单介绍一下他的几个比较领先的设计。
muzzle-check机制
编译时收集我们埋点代码中访问了被增强类的哪些方法、字段。在运行时,如果待增强类没有相应的方法和字段,则不执行增强动作,避免增强代码报错;在平时,可以对待增强类的所有版本执行静态检测,获得支持版本列表。
VirtualField机制
JVM的字节码增强机制有一些限制,对于已加载类的增强,只能修改方法体,不能给类新增字段。这个限制对于我们影响较大,因为在APM的场景下,往往有较多的场景需要给类增加字段来作一些变量传递。opentelemetry-java-instrumentation提供了VirtualField机制,如下图所示,通过统一的编程接口,可以给类T添加一个类型为F的字段。
- 当类T当前尚未加载时,此时的实现就是给类T增加了类型为F的字段。当访问类T的F字段时和访问普通的类字段一致
- 当类T已经加载时,此时的实现是有一个全局的ConCurrentWeakhashMap,map的key类型为T,value类型为F。当访问类T的F字段时实际在底层为map的get操作
public static <U extends T, V extends F, T, F>
VirtualField<U, V> find(Class<T> type, Class<F>fieldType) {
return RuntimeVirtualFieldSupplier.get().find(type, fieldType);
}
异步上下文透传
除了原生的JDK线程池,对市面上常见的异步框架akka、netty event loop等均做了异步埋点。异步埋点思路整体上包含两个步骤
- 实现了Runnable接口的实现类,利用上述的VirtualField机制,给实现类增加一个记录Trace上下文的字段,同时埋点其run方法,run方法执行时获取增加字段中的trace上下文,并设置到当前线程的ThreadLocal中
- 埋点Exectuor的execute方法,在execute方法执行时从ThreadLocal获取当前Trace上下文,并设置给对应的Runnable实现类
新埋点思路
最大程度利用框架的拓展能力进行埋点,比如利用Dubbo的Filter机制、grpc的Intercepter机制、实现lettuce的tracing接口等等。而不是一味的对框架的方法进行增强
除了上面提到的这些,opentelemetry-java-instrumentation还有很多亮点设计,比如类加载器隔离,opentelemetry-java-sdk兼容,多JDK版本兼容等等,这里不再一一赘述,后续会推出系列文章专门介绍OTel Java Agent的一些亮点设计和技巧。
调研结论
当完成OTel Java Agent各方面的调研之后,我们会发现他的很多设计都是领先于当初选择的Pinpoint探针的,一章节提到的那些代码设计和技巧、埋点方式等帮助我们打开了新的思路,可以解决很多困扰许久的问题。OTel Java Agent 的蓬勃发展成为了一个促使我们进行一次大规模重构最合理的契机,再考虑到拥抱开源、拥抱标准的基本原则,于是我们在2023年的夏天做了一个重大的决定,在 ARMS Java Agent 的下一个大版本 4.x 版本中,基于OTel Java Agent做一次升级重构,将现有ARMS 3.X版本探针的商业化能力迁移过来,并做到能100%兼容3.x探针的功能。
围绕OTel Java Agent做了哪些增强
在接下来将近一年的时间里,围绕openTelemetry-java-instrumentation,首先,我们对其现有的功能进行了升级重构。包括新插件支持,基础的tracing能力增强,指标类型增加、指标维度增加等等;其次,迁移了很多过往几年沉淀的商业化能力。包括Arthas诊断,应用安全、内存Dump,微服务治理(全链路灰度、无损上下线,限流降级等等);最后,围绕探针构建了完善的稳定性保障措施。升级后的探针整体架构图如下图所示

下面分别展开说明
新插件支持
OTel探针对国内一些被广泛使用的框架、中间件支持较少,比如druid、xxl-job、hsf、influxdb、mybatis、xxlJob、motan、shenyu等,我们此次增加了对这些框架的支持,并且部分已经贡献给开源。
Tracing增强
Tracing能力是APM探针的核心能力,OTel探针原生的Tracing能力在企业内部复杂场景下往往会遇到不少挑战,包括多协议场景下断链、极端场景下span数量爆炸、采样难以命中高价值数据等等。针对这些问题,我们对OTel探针做了以下增强来解决:
多协议支持
原理:默认情况下会自动按照 EagleEye、W3C、Skywalking、Zipkin、Jaeger 、Skywalking的顺序识别并恢复上游透传的 Trace 上下文。同样也支持按照用户需求配置优先或者强制使用某种协议
优点:在客户多语言、内部不同部门使用多套tracing系统、外部流量携带trace上下文、上云迁移等场景下能尽量保证不断链
调用链压缩
原理:ARMS 探针会将一些同一层级的重复span压缩成一个,比如业务代码在一个 for 循环中,调用 数据库 应用 10000 次,那么在调用链中会生成 10000 个 span,而经过调用链压缩后,仅会记录一个span,并在这个span中记录重复span的次数、最大耗时、最小耗时、总耗时等信息,效果如下图所示。

同时,为了避免尽可能保留重要信息。会将耗时top3和最开始报错的三个span转换为spanEvent保留在压缩后span的spanEvent中。
优点:一方面可以避免极端场景下产生大量数据,客户overhead过高;另一方面避免span过多场景下,后端查询缓慢、前端渲染卡顿、展示臃肿、客户排查问题难以抓住重点等问题
缺点:因为仅保留了部分样本,无法看到全部的信息,可能导致丢失用户真正关注的数据
采样
相比于其他产品单一的采样策略,ARMS 探针提供相对较为丰富的采样策略,且大多不需要用户进行复杂配置,每个采样策略保证特定场景下高价值Tracing数据被采样,低价值Tracing数据少采样,分别如下所示:
固定比例采样
即现有的默认采样,按照百分比采样链路
自适应采样
自适应采样会按照 LFU 的策略选取当前调用量 top-1000 的接口,每个接口的采样彼此隔离,可设置两种采样策略,两种策略两种采样分别如下所示。
- 每秒固定条数(默认):一秒采样10条
- 自适应比例:默认10%,会根据该接口上一分钟请求量动态调整,避免大流量接口采样太多无效数据
另外对于调用量 top-1000 以外的接口,可以认为是一个 other 接口。处理逻辑和前面介绍的 top-1000 中任1接口一致
小流量采样
无需用户配置,自动保证每一个接口每一分钟至少有一个span。原理是用一个布隆过滤器存储一分钟内已经被采样过的接口。并每一分钟定期重置该布隆过滤器。这样可以保证无论用户接口有多发散,内存开销都是确定的。
错慢异常采样
无需用户配置,当一次调用满足下面三个条件时,则上报该次调用相关span。
接口报错:http 类接口响应码非 2xx、3xx或者本次调用的 localRootSpan 埋点方法处抛出异常
接口内部有异常:一次调用的非 LocalRootSpan 的 span 记录到异常信息
接口调用耗时长的定义:接口耗时大于过去一段时间该接口的 p99耗时
该采样对于问题排查十分重要,但是因为时机问题,无法保证链路完整。比如接口A调用接口B,A命中错慢采样,并不能保证B接口的Span一定上报
自定义采样
即用户自己配置 100% 采样接口、接口前缀、接口后缀等等。满足用户配置要求的调用会一定采样
总结
上述各个采样策略会在一次调用中都生效,一个Span是否上报由上述采样策略综合决定,详细流程如下图所示

其中不同颜色的采样策略区别在于
紫色:标准的头采样,只会在链路的RootSpan处触发,采样后可以保证后续链路完整
蓝色:只要当前的采样结果是不采样,可以在链路的任何一个LocalRootSpan处触发,采样后可以保证后续链路完整
绿色:只要当前的采样结果不采样,可以在链路的任何节点触发,采样后无法保证后续链路完整
以一个常见的链路A->B->C为例说明,在不同节点命中不同采样规则时,对应会链路哪些span会上报,哪些span不上报

Metrics增强
更丰富的指标
- 线程池监控指标:针对常见JDK线程池,Jetty、Undertow线程池监控,支持核心线程、最大线程、活跃线程、当前线程、历史最大线程、调度任务、完成任务、拒绝任务以及队列大小9类指标。便于排查线程池打满类问题

- 线程监控指标:将当前JVM种所有线程归类后,统计不同类别线程的耗时以及处于不同状态线程的数量,并定时抓取线程栈,便于排查线程阻塞、线程耗时高等问题

- MQ消费延迟指标:针对MQ类组件,出了常见的RED指标,增加消费延迟指标,便于排查消费延迟类问题

- 数据库响应大小:针对DB类操作,增加请求、响应大小指标,便于排查大查询类问题

- 新增异常类指标:指标主要为异常次数,维度记录了当前接口,便于排错异常类问题

更多样的维度
- 接口类RED指标记录上游接口:方便查看接口到接口的调用拓扑、调用关系,方便接口异常时快速定位上下游

- 数据库类调用指标。额外记录当前接口、数据库语句维度,方便接口出问题时,快速定位是否db的问题。db出问题时,快速查询影响的接口,具体出问题的sql语句

Profiling能力支持
和阿里云 Dragonwell 团队合作,底层基于async-profiler,提供 CP(Continuous Profiling)的能力。阿里云 Java Agent 提供的 CP 支持多种剖析类型,比如 CPU 热点剖析、堆内存热点剖析,墙钟热点剖析等。
特色能力介绍
除了常见的 CPU 热点剖析、内存热点剖析,ARMS 还针对慢调用链诊断场景,提供了代码热点产品能力,其是在开源 Async Profiler 墙钟能力的基础上,通过关联调用链中的 TraceId & SpanId 信息提供了调用链级别的 On & Off-CPU 火焰图,可有效对 Tracing 的监控盲区细节进行还原,帮助用户诊断各类常见的慢调用链问题,详情可参见文档

那么,ARMS持续剖析和客户自己用开源的 Async Profiler 生成火焰图相比有什么优势呢?
首先,支持常态化开启,开源的 Async Profiler 未提供支持常态化开启的数据存储与处理能力,难以在生产环境常态化开启,对于一些线上偶现的问题,难以使用其进行问题排查。
其次,运行环境覆盖面更广,开源的 Async Profiler 一些剖析类型对应用运行环境有一定要求,比如 Alpine Linux 基础镜像为了控制体积而去除了 JDK 调试符号(debug symbols)导致无法使用内存热点剖析功能,但是 ARMS 在其基础上通过针对特定版本的 Alpine Linux 基础镜像对应的 JDK 调试符内容做了预适配,对相关类型的环境,在不安装调试符的情况下,也可以使用内存热点。
最后,更好的稳定性。开源的 Async Profiler 常态化开启过程中可能会容易出现 Crash 问题比如#694或者多个剖析引擎(CPU热点、内存热点等)同时启动,一个外部条件不满足引发的单引擎失败会导致整体失败,ARMS在开源 Async Profiler 基础上做了一些 bugfix 和剖析引擎隔离优化,稳定性更好。
性能优化
在分析OTel Java Agent的过程中,我们发现它在创建span、记录指标等地方,对于Attributes有大量的重复copy以及排序操作,这些部分是占用整个探针CPU开销的大头,我们对这些操作进行了大量的优化,结果表明在TPS4000流量的测试场景下,aliyun-java-agent探针相较开源版本OTel Java探针CPU性能表现更好,整体容器CPU开销水位大约降低2%;内存性能表现上,在进行2h压测后容器申请的RSS内存,aliyun-java-agent探针相较开源版本OTel探针的内存占用降低约10MB
问题诊断场景的增强
- 集成代码级问题诊断利器Arthas。无需依赖JDK,一键开启、关闭。常见命令白屏化操作。且支持企业级鉴权、审计能力。避免任意用户随意执行Arthas命令。详情见 链接
- 内存 Dump。一键对指定机器执行内存dump,并配套白屏化分析能力。
云产品集成
- 微服务治理。在同一个 Java Agent 中集成了阿里云 MSE 微服务治理能力,包括全链路灰度、限流降级、无损上下线,系统防护、消息灰度等;
- 集成云安全中心应用安全 RASP。一键开启后拥有危险组件检测、25+种攻击行为的监控,阻断的能力
探针稳定性建设
由于Java探针和用户代码运行在一个进程中,且会对用户代码进行增强修改,Java探针的稳定性建设尤为重要,多年的公有云用户服务经验告诉我们,对于一款可观测产品而言,我们的底线是不能影响业务行为,比如导致用户进程启动失败,用户进程crash,用户接口报错,占用大量用户机器资源等等。为了最大程度的避免这类问题,并在出现这类问题时能够及时止血, 我们在OT的基础上增加了下述能力
- 探针CPU/内存占用上限控制能力:在探针CPU开销,内存占用超过指定阈值时,自动降级探针的Tracing或者Metrics数据采集能力
- 探针启动预检能力;因为探针本身有运行的环境要求,为了避免在非预期环境中出现异常行为,探针启动有若干检测项,比如JVM类型、JVM版本、最大堆内存等等,最大程度的避免影响用户业务
- 探针功能可动态插拔能力:大部分探针功能,特别是可能影响用户业务的能力,都具备动态控制开关,可以在出现问题时快速关闭
阿里云 Java Aent 4.0 探针带来了哪些好处
从功能层面上来说,这次升级,完全吸纳了OTel的优秀设计,对我们现有的很多功能做了升级或者增强
- 遵循JDBC规范的数据库埋点从JDBC接口层面埋点,理论上支持所有遵循JDBC规范的数据库埋点(3.x探针仅支持固定的9种)
- 异步埋点无需用户配置,不会断链
- vertx、webflux、lettuce、Rabbitmq、kafka、RocketMq、ONS等插件相比老版本,因为埋点的位置优化,指标统计更准确,支持版本范围更广
- 支持容器场景的系统指标采集
- 线程池监控支持用户自定义线程池的监控
从工程质量上来看来说,这次升级重构是对3.x代码的一次取其精华、去其糟粕的过程、是重新树立更合理科学的开发规范的过程,通过这次升级重构
- 4.0 探针的内存占用下降了20%、线程数降低了60%,探针包大小降低了30%。
最后,从长远发展来看,我们制定了每三个月合并一次开源最新稳定代码的计划,可以快速的享受到社区快速迭代的红利。
我们为社区做了什么
在基于OTel Java Agent二次开发的过程中,我们也积极的反哺开源,在过去6个月中,我们累计向社区贡献并合并各类PR 40+,其中包含新增在国内广泛使用的XXL-JOB、InfluxDB、MyBatis等插件,参与社区日常PR Review 100+,steverao和123liuziming两位同事成为社区member,其中steverao受邀作为该项目的Triager并且负责该社区的日常维护、代码CR,且个人贡献长期位列社区前四名,贡献度积分位列社区Top 20,亚太地区第一。

此外,我们也积极参与社区相关各类会议活动,今年6月,受社区邀请,在北美举行的2024 OpenTelemetry Community Day活动中,我们同事望陶和铖朴,为社区带来了《GraalVM 静态编译下 OTel Java Agent 的自动增强方案与实现》主题分享,对相关问题的原创性解决方案得到了社区开发者的广泛关注。今年8月,在中国香港举行的KubeCon China 2024大会上,望陶和铖朴与社区其他开发者一起,在社区Governance Committee团队的支持下,代表社区在大会上做了《社区最新进展以及阿里云拥抱OTel社区实践》相关分享。此外,为了促进亚太地区与社区的交流,在团队相关同学向社区提议,在与社区Governance Committee Member成员沟通后,OTel社区也在多个领域,设立了亚太地区友好的周会交流时间。其中包含Java: SDK + Instrumentation、Semantic Conventions: LLM、Contributor Experience和Developer Experience,相关时间可以参考社区周会安排,相关会议中有社区最资深的开发者一起参与,欢迎有兴趣的朋友加入。
目前,我们也正在将由阿里云开源的 Go Instrumentation 贡献到OTel社区,相关内容正在与社区相关Governance Committee和Technical Committee团队讨论中#1961。
写在最后
我们用了接近一年的时间完成了基于 OTel Java Agent 的升级重构,并于今年5月份发布了4.x探针的第一个版本4.1.0,经过接近半年时间的验证、回归、优化,目前最新的稳定版本4.1.12已经正式发布,欢迎大家了解使用。
接下来的时间,一方面我们将持续的 Follow OTel Java Agent 的发版节奏,定期合并开源稳定代码,保障用户可以持续的享受社区最新的feature;另一方面,我们也将重点打造阿里云 Java Agent相比OTel Java Agent的差异化能力,补齐其不足与短板,帮助用户获得更全面、更透彻的应用可观测体验。