
Arthas 的trace命令有多好用用过的人都知道。线上接口慢、第三方 jar 内部逻辑诡异、某个方法调用耗时突然飙高一条trace com.example.OrderService createOrder发出去调用树、每层耗时、异常位置全部打在控制台上整个过程不用改代码、不用重启应用。但痛点也很明显Arthas 的输出是终端的看完就没了团队其他人看不到也没法和 Jaeger、SkyWalking、Grafana 这些可观测性平台打通。另一边OpenTelemetryOTel已经把 trace 的数据模型、采集、导出标准化了生态和工具链都成熟可它想对线上某个具体 Java 方法做动态追踪基本做不到只能靠提前埋点或者挂 agent 做自动化插桩灵活性和 Arthas 差了不止一个量级。我一直在想能不能把这两者揉到一起用 Arthas 负责“动态的、按需的、方法级追踪”用 OpenTelemetry 负责“标准化的、可导出的、可挂接后端平台的 trace 模型”。EasyTelemetry 就是在这个思路下折腾出来的一个桥接方案。它做的事情很直接让 Arthas 的 trace 能力接入 OTel 生态把trace命令的结果转成 OTel Span送进 Collector、Jaeger 或者任意兼容后端。这篇博客我不讲大道理只聊方案怎么设计、怎么落地、踩了哪些坑以及一个能跑通的最小实例。如果你也遇到过“线上问题靠 Arthas 能查但没法沉淀、想接入 OTel 又不想大规模改造”的情况这篇文章应该能给你一个很实在的思路。1. EasyTelemetry 到底想解决什么问题1.1 Arthas trace 的价值和天花板Arthas 的trace命令本质是在运行时对目标类做字节码增强统计方法内部调用路径、每个子调用的耗时、异常信息等。它最爽的地方在于“不需要预先准备”应用跑得好好的发现某个接口卡了连上 Arthas指定类和方法调用树立刻出来。这种能力在故障应急和疑难杂症排查里几乎是不可替代的。但 Arthas 的设计定位是交互式命令行工具这也决定了它有明显的天花板。输出只留在终端里没法沉淀、没法查询、没法跨团队共享。它是“人肉触发”的问题复现时你正好在才能抓到现场不能按条件自动启停。一次 trace 只能覆盖一个类和方法分析链路时如果问题在深层子调用你得一层层往里追。一句话总结Arthas 擅长“把一次调用看穿”但不擅长“把多次调用变成长期可观测的数据资产”。1.2 OpenTelemetry 的标准化优点和动态短板OpenTelemetry 真正厉害的地方是定义了统一的 trace 模型TraceId、SpanId、ParentSpanId、SpanKind、Attributes、Events、Status以及一套和语言无关的导出协议 OTLP。只要能生成符合这个模型的数据就能无缝交给后端平台去展示、检索、关联、告警。OTel 的短板在于获取数据的方式。常规做法有两种一是代码埋点在业务代码里手动创建 Span二是运行 Java agent让 agent 自动拦截常见框架和库的调用。但这两种都解决不了“运行时临时想看某个方法的调用细节”的需求。你可以在代码里写埋点但不可能把每个方法都埋一遍你可以挂 agent但 agent 的插桩规则有限对自定义类、第三方 jar 内部逻辑未必覆盖。如果只想针对某个线上偶发问题动态追踪一个方法OTel 这套体系是有些笨重的。1.3 “按需动态 Trace 标准化导出”才是刚需把两个工具放在一起看需求非常清晰线上能动态追踪数据能标准化导出。比如排查一个订单接口偶发超时的问题你不想改代码、不想重启只想知道某次调用里OrderService.createOrder底下到底哪个环节慢了。你要的是能随时对特定方法开启追踪不用预埋点追踪结果能送到 Jaeger 这类平台按 TraceId 查能看到完整调用链问题复现一次就能抓到数据而不是靠肉眼盯终端。EasyTelemetry 就是干这个的。它把 Arthas 的“动态能力”和 OTel 的“标准模型”对接起来让两条技术路线从“互相看不上”变成“各取所需”。对于 Java 后端开发、SRE、性能排查人员来说这相当于给 OTel 补上了“线上动态方法级追踪”这一环。2. EasyTelemetry 的整体设计和核心思路2.1 架构Arthas 负责定位OTel 负责传输EasyTelemetry 的核心架构并不复杂整体分成四层。第一层是Attach 层。EasyTelemetry 启动后通过 JVM Attach 机制挂到目标 Java 进程上在目标 JVM 里启动 Arthas 的 Core 服务。这一步和手动执行java -jar arthas-boot.jar的逻辑是一样的只是由 EasyTelemetry 自动完成。第二层是命令执行层。EasyTelemetry 不是让你在交互终端里敲 Arthas 命令而是通过 Arthas 的异步命令 API 直接下发trace指令然后持续读取命令输出。这里有一个关键点Arthas 的 trace 命令本身是持续输出的它会一直拦截匹配方法的调用并打印调用树直到你执行stop或者断开连接。这个持续输出正好和可观测性平台的“持续采集”语义天然吻合。第三层是解析映射层。Arthas 的 trace 输出是一段文本比如[arthas-9527] $ trace com.example.OrderService createOrder Press Q or CtrlC to abort. Affect(class-cnt:1 , method-cnt:1) cost in 98 ms. ---ts2025-04-08 14:23:11;thread_namehttp-nio-8080-exec-3;id42;is_daemontrue;priority5; ---[8.1234ms] com.example.OrderService.createOrder() ---[0.0231ms] com.example.PriceCalculator.calculate() | ---[0.0188ms] com.example.PriceCalculator.roundPrice() ---[2.3124ms] com.example.InventoryClient.checkStock() | ---[1.9877ms] com.example.http.HttpUtil.get() ---[5.3301ms] com.example.OrderDao.insert() ---[1.2012ms] com.example.db.ConnectionPool.getConnection()这行文本就是信息源。EasyTelemetry 要做的是解析这段文本把每次方法调用映射成一个 OTel Spancom.example.OrderService.createOrder()是根 SpanPriceCalculator.calculate()、InventoryClient.checkStock()等是它的子 Span。每一层的缩进关系用来确定 ParentSpanId。第四层是导出层。解析出的 Span 通过 OTel SDK 的 SpanProcessor 交给 Exporter用 OTLP 协议发给 OTel Collector再转发到 Jaeger、Tempo、SkyWalking 等任意后端。此时 EasyTelemetry 已经不再是 Arthas 的“壳”而是真正接入了标准可观测性管线。这种“Arthas 做脏活、OTel 做传输”的架构有个很实际的好处EasyTelemetry 本身不需要关心字节码增强细节Arthas 在类加载、增强、兼容性上踩过的坑我们都直接规避了。2.2 为什么不是自研字节码增强也不是魔改 Arthas在设计早期我考虑过两条其他路线。一条是自己做字节码增强在目标方法入口和出口分别插入 Span 逻辑。好处是可控性强可以和 OTel 的 Context 直接打通。但代价是需要处理类加载器隔离、JDK 模块限制、异步线程上下文传播、并发调用栈匹配还要兼容各种类库。如果不是专门做 APM 产品一个月内大概率做不出稳定版本。另一条是魔改 Arthas 源码让它在 trace 时直接生成 OTel Span。这个方案的问题在于 Arthas 是快速迭代的开源项目改源码意味着要长期维护一个私有分支每次跟上官方更新都要做大量合并。而且 Arthas 本身并非为可观测性平台设计直接改它的核心逻辑风险不可控。最终 EasyTelemetry 选择了解析 Arthas 文本输出这条路原因是它最大程度复用了成熟能力又保持了很低的耦合度。Arthas 的 trace 输出虽然是人读的但格式相对规整通过缩进表达调用层级通过---[耗时]表达调用耗时通过ts和thread_name表达线程信息。只要建立好解析规则就能稳定还原出调用树。这个方案还有一个额外好处只要 Arthas 的输出格式不变EasyTelemetry 就能持续工作不用跟着 Arthas 内部实现走。2.3 Trace 模型映射文本调用树如何变成 Span 树把 Arthas 文本转成 OTel Span最核心的问题是“怎么保证父子关系和 TraceId 正确”。OTel 的 Span 模型是这样的一个 Trace 有唯一的 TraceIdTrace 里有多个 Span 通过 ParentSpanId 挂成树状结构。Jaeger 展示的火焰图和调用瀑布图依赖的就是这个树结构。Arthas 的 trace 输出天然就是一棵树但问题是它没有 OTel 的 TraceId 和 SpanId。EasyTelemetry 的做法是在一段时间内把 Arthas 在同一个线程中的连续输出解析为一条 Trace每次顶层方法调用生成一个新的函数级 TraceId方法调用树中的每个节点生成一个 SpanId缩进关系决定父子关系。这里有个必须特别注意的问题Arthas 的 trace 输出是并发环境下混杂在一起的。多个线程同时调用同一个方法时Arthas 会在每个调用树前打印线程信息但输出顺序可能交叉。EasyTelemetry 在解析时必须以thread_name为第一分组维度每个线程单独维护一棵树否则会得到张冠李戴的调用链。实际实现中我用一个ThreadLocal维护“当前解析现场”每次收到一个线程的顶层调用开始行就关闭上一个现场开启新现场收到子调用行就挂到当前栈顶节点下面。Span 的映射规则我定为Span Name使用“类名.方法名”与 Arthas 输出一致Span Kind统一使用INTERNAL因为这是进程内方法调用不是远程调用的 Client/Server 语义Span Status如果 Arthas 输出中包含throw或exception标记则设置为STATUS_ERRORAttributes记录class.name、method.name、thread.name、thread.id、componentarthas-trace。有一点需要说明这里生成的 TraceId 和业务系统里的 OTel TraceId 是独立的。如果业务系统本身已经接了 OTel那这条动态 trace 无法做到和业务 trace 自动串联到同一个 TraceId 下。要实现关联要么让 EasyTelemetry 从当前线程上下文里提取已有 TraceId要么接受它作为独立 Trace 存在。我最初做的是后者因为使用场景大多是临时排查独立 Trace 已经足够。3. 实操过程把 EasyTelemetry 跑起来是个什么体验3.1 环境准备先列出我实际验证过的一套环境JDK8 和 11 都试过17 也可以但需要额外处理模块访问目标应用一个普通的 Spring Boot 服务端口 8080Arthas3.6.7 及以上版本OTel Collector本地 docker 启动端口 4317 接收 OTLPJaeger作为 trace 后端用 all-in-one 镜像最省事EasyTelemetry以可执行 jar 方式启动。准备一个最简单的示例服务核心接口如下RestController public class OrderController { GetMapping(/create) public String create() { double price priceCalculator.calculate(); int stock inventoryClient.checkStock(); orderDao.insert(price, stock); return ok; } }这个接口内部有三次子调用非常适合演示 trace 树的父子关系。3.2 EasyTelemetry 的配置EasyTelemetry 用一个 YAML 文件做配置下面是我常用的最小配置easytelemetry: # attach 目标进程可以是 pid也可以按 Main 类名匹配 target: com.example.OrderApplication trace: # 要追踪的类名模式支持全限定名或通配符 className: com.example.OrderService # 要追踪的方法名 methodName: createOrder # 达到多少 ms 才输出 trace设置 0 表示全部输出 condition: 0 otel: serviceName: order-service-arthas exporter: endpoint: http://127.0.0.1:4317 protocol: grpc sample: # 仅在有调用时采样减少持续增强带来的开销 mode: on-demand注意target既可以用 PID也可以用主类名。实际使用中如果你有两个同名主类还是用 PID 更稳妥。condition对应 Arthas trace 命令的#cost参数意思是“只显示耗时超过该阈值的调用”默认 0 会显示所有。这里我把采样模式配置成on-demand。因为 Arthas 的字节码增强是持续生效的如果不加限制高频方法的增强会对性能有影响。后面我会专门讲这个坑。3.3 启动 EasyTelemetry 并触发 trace启动命令很简单java -jar EasyTelemetry.jar --config /path/to/easytelemetry.yamlEasyTelemetry 启动后会做三件事通过 Attach API 连上目标 JVM在目标 JVM 中启动 Arthas Core下发 trace 命令并保持异步读取。日志会输出[EasyTelemetry] attached to pid 87321 [EasyTelemetry] arthas core started [EasyTelemetry] trace command submitted: trace com.example.OrderService createOrder #cost 0 [EasyTelemetry] listening arthas output...此时访问一次示例接口/createArthas 会拦截到OrderService.createOrder的调用并输出调用树。EasyTelemetry 解析后生成 OTel Span并批量推送到 Collector。控制台层面EasyTelemetry 还会同步打印一行摘要方便你验证解析结果[EasyTelemetry] span committed: trace8f3a1c2e, spanOrderService.createOrder, children3, cost8.1234ms3.4 在 Jaeger 里看到动态 trace打开 Jaeger 的查询页面选择服务order-service-arthas就能看到刚才触发的 Trace。点进去之后span 树长这样OrderService.createOrder 8.12ms ├── PriceCalculator.calculate 0.02ms ├── InventoryClient.checkStock 2.31ms └── OrderDao.insert 5.33ms对比 Arthas 命令行里的输出结构一致只是从终端搬到了标准 trace 后端。你可以按 TraceId 精确检索某一次调用也可以按时间段浏览在这段时间内所有被 trace 的调用。这一步看起来简单但意义不一样Arthas 只能告诉你“这一次调用发生了什么”接入 EasyTelemetry 之后你能看到“这段十分钟内所有被追踪调用的分布”能筛选、能排序、能和其他系统指标对照。所谓可观测性就是数据能留存、能检索、能分析这一点上 EasyTelemetry 把它补齐了。3.5 核心流程的最小实现思路如果你不想直接依赖 EasyTelemetry自己实现这套逻辑也是可以的。核心链路只有四段。第一段Attach 到目标 JVMVirtualMachine vm VirtualMachine.attach(pid); String arthasJarPath /path/to/arthas-core.jar; vm.loadAgent(arthasJarPath, config);第二段执行 Arthas trace 命令并读取输出Arthas Core 支持通过com.alibaba.arthas.core.command.monitor100.TraceCommand执行 trace输出会写入一个可读取的流。实际如果自己接比较容易的方式是走 Arthas 的 Telnet/HTTP 通道发指令、读输出。第三段解析输出为 Span 树维护一个当前解析栈遇到带缩进的调用行就解析出类名、方法名、耗时生成 Span 节点遇到异常标记就设置错误状态。第四段用 OTel SDK 导出Span span tracer.spanBuilder(node.getMethodName()) .setSpanKind(SpanKind.INTERNAL) .setAttribute(class.name, node.getClassName()) .setAttribute(method.name, node.getMethodName()) .startSpan();真正做的时候第二段是最闹心的因为 Arthas 的异步输出格式在不同版本上有细微差别比如thread_id字段在 3.6.x 之后变成了id解析规则必须做兼容。我建议你先手动跑一次trace命令把完整输出截下来再写解析器别凭感觉猜。4. 常见问题与排查技巧实录4.1 trace 命令没有输出最常见的现象是EasyTelemetry 启动成功也提示trace command submitted但访问接口之后 Arthas 没有捕获到任何调用。按优先级排查类名和方法名是否正确。Spring 的 Bean 大多数是 CGLIB 代理类类名会变成com.example.OrderService$$EnhancerBySpringCGLIB$$xxx。如果 Arthas 匹配不到可以用sc -d查看实际类名。对于接口 trace建议用实现类名。方法是否真的被调用。有些逻辑走了缓存、短路、分支判断根本没有进到目标方法。目标类是否已经加载。如果类还没加载Arthas 的 trace 默认是匹配不到的需要开启-D参数或者用trace com.example.OrderService createOrder -n 1配合重试。目标 JVM 是否有运行权限。Arthas attach 在容器环境和受管控环境里经常会失败报Can not attach to current VM或Permission denied。容器里尽量挂载/proc并确保宿主机和容器是同一个 PID namespace。我踩得最深的一个坑是Arthas 成功 attach 了但 trace 命令匹配到的是父类方法而实际调用是子类重写后的方法。这个问题通过打印实际调用类是看不出来的要用stack命令确认调用栈或者直接在目标方法上打一个临时watch。4.2 Jaeger 里 Span 树是乱的或者有多个根如果你看到同一批 Span 在 Jaeger 里被拆成多个 Trace或者父子关系错乱十有八九是并发解析问题。Arthas 在并发调用时输出流是交叉的thread_a: ---[8ms] com.example.OrderService.createOrder() thread_b: ---[5ms] com.example.OrderService.createOrder() thread_a: ---[2ms] com.example.PriceCalculator.calculate() thread_b: ---[1ms] com.example.InventoryClient.checkStock()如果解析器只是简单地把“每次遇到新缩进就挂到当前节点后面”thread_b 的调用很可能挂到 thread_a 的树下面。我的解决办法是严格以线程名为维度缓存解析栈收到顶层调用行时先清理该线程之前的栈再开启新的树。这个细节必须在解析器设计时就要做不然后面查起来非常痛苦。还有个容易忽略的点Arthas 的 trace 命令在输出到顶层方法前会先打印一行Affect(class-cnt:1 , method-cnt:1) cost in 98 ms。这行不是调用树的一部分解析时必须过滤否则它会变成一个莫名其妙的根节点。4.3 性能开销比预期高Arthas 的 trace 本质是增强目标方法每次调用会走增强代码。如果你 trace 的是一个 QPS 很高的方法开销会被放大。我实测过对一个每秒几百次调用的方法开启 traceCPU 额外增加约 5% 到 10%。单次 trace 能接受但持续开着不合适。EasyTelemetry 的解法是trace 命令只在需要时开启用完就停不要真当成全链路追踪长期挂载用condition设置耗时阈值只关注慢调用在高频场景下配合 OTel 的采样策略比如只导出 1% 的 trace。另外Arthas 对已增强类需要做恢复。停止 trace 时Arthas 会移除增强逻辑但偶尔要等几秒才会完全恢复。如果并发又开了另一个 trace可能出现 ClassReTransForm 失败建议两个 trace 之间留至少 1 到 2 秒的间隔。4.4 JDK 版本和 ClassLoader 兼容性JDK 11 以上Arthas attach 有时会报 module 访问错误。操作上我习惯在应用启动脚本里加上--add-opensjava.base/jdk.internal.loaderALL-UNNAMED这类参数能省很多事。ClassLoader 的问题更隐蔽。Arthas 用系统类加载器加载自身但目标类可能是 Web 容器或框架自定义类加载器加载的。当目标类加载器和 Arthas 类加载器不一致时增强会静默失败几乎不报错。排查办法是在 Arthas 里执行classloader命令确认线程上下文类加载器已经切换到 Tomcat 等容器对应的类加载器。EasyTelemetry 在启动后会自动尝试切换到目标类的类加载器下发命令这也算一个大家在自研时必须关注的细节。4.5 常见问题速查表现象可能原因推荐处理attach 失败容器权限、PID namespace 不一致使用 PID 重试挂载 /proc启动参数加 --add-openstrace 无输出类匹配失败用 sc 确认实际类名开启 -D 等待类加载trace 无输出CGLIB 代理覆盖改为 trace 实现类或 trace 接口的代理类名Span 树错乱并发线程输出交叉解析时以线程名分组独立维护解析栈Jaeger 找不到 TraceOTLP exporter 地址错误检查 endpoint 是否可通查看 EasyTelemetry 日志trace 输出中断Arthas 版本输出格式变化升级到最新版调整解析规则兼容 id/thread_id 字段目标 JVM 崩溃/慢高频方法过度增强增加耗时阈值减少 trace 持续时间配合采样Collector 拒绝数据service.name 重复或 metadata 冲突检查资源配置确认 exporter 与 Collector 协议一致5. 一些落地经验和下一步想法EasyTelemetry 这个项目做下来我最深的体会是架构可以很克制需求可以很精确。它没有造轮子没有重新实现字节码增强也没有去定义一套新的 trace 规范只是在两个成熟工具中间补了一层“翻译”。这层翻译让 Arthas 的临时动态能力进入了可观测性平台的标准体系解决了以前那种“查完就丢”的尴尬。实际使用中我认为这个方案最合适的定位是可观测性的“应急补充层”。全链路埋点是日常的常态观测EasyTelemetry 是问题出现时的精确放大镜。它适合在疑难问题排查、性能定位、第三方 jar 黑盒分析时开启但不建议长期挂在高频路径上。这是 Arthas 这类动态增强工具和正式埋点方案的天然边界强行跨界只会带来不必要的性能负担。如果你也想做类似的东西我的建议是先从解析 Arthas 输出开始别急着设计分布式采集和管理端。先用一个单机 demo 跑通“目标方法调用 - 控制台输出 - 解析 - Jaeger 展示”这条链路再逐步考虑多实例、自动启停、对接告警。后续 EasyTelemetry 我想做三件事。一是支持从 OTel 的已有 Span 上下文里继承 TraceId让动态 trace 能挂到业务全链路里。二是做一个规则触发模块比如“请求耗时超过 2 秒时自动对该请求涉及的关键方法开启 trace 并保留现场”。三是把 Arthas 的watch、stack输出的关键字段也转成 Span Event让诊断信息不只是树的形状还有具体参数和调用栈。这条路走下去动态诊断和标准化可观测性之间的边界会越来越模糊对排查问题的人来说体验也会好很多。