Arthas tt 命令深度指南:用 TimeTunnel 时间隧道回溯、检索与重放方法调用
【免费下载链接】arthasAlibaba Java Diagnostic Tool Arthas/Alibaba Java诊断利器Arthas项目地址: https://gitcode.com/gh_mirrors/ar/arthas
tt(Time Tunnel,时间隧道)是 Arthas 中极具特色的一条命令:它能够将目标方法每次调用的入参、返回值、抛出的异常、调用耗时与调用对象等完整上下文快照成一个个"时间片段"(Time Fragment)缓存下来,随后你可以随时按索引查看某次调用的完整细节、用 OGNL 表达式检索或观察记录、甚至用当时的参数原样重放那次调用。本指南以tt命令为主线,结合 Arthas 源码讲解其实现原理、全部参数与实战用法,读完你即可在线上问题排查中完成"记录调用 → 检索命中 → 查看细节 → 重放复现"的完整闭环。
为什么需要 tt:watch之外的时光机
watch虽然强大,但存在固有的局限:它需要你在方法被调用时守在那里观察输出,且受限于观测时机和表达式的复杂度,面对"偶发异常、难以现场复现"的问题往往力不从心——异常可能在你反应过来之前就已经过去了。
tt正是为此而生。它把每一次方法调用记录下来(默认最多 100 条),相当于给业务方法装了一台"黑匣子"。问题发生后,你不需要等待下一次触发,而是直接回到历史记录中,查看当时发生了什么:参数是什么、返回了什么、抛了什么异常、耗时多久、调用对象是谁。更进一步,你还可以把某次失败的调用"重放"一遍,用当时的入参重新执行该方法,观察问题能否稳定复现。
从源码结构看,tt的实现位于 TimeTunnelCommand.java,类注释即点明了它的本质——"时光隧道命令"。
工作原理与核心数据模型
要熟练使用tt,先理解它的底层模型。
时间片段(TimeFragment)
每次被增强方法的一次调用,Arthas 会封装成一个TimeFragment,其定义在 TimeFragment.java:
class TimeFragment { private final Advice advice; // 调用上下文:入参、返回值、异常、目标对象、类与方法 private final LocalDateTime gmtCreate; // 调用发生时间 private final double cost; // 调用耗时(毫秒) }其中Advice(通知上下文)携带了关键数据,接口定义在 Advice.java,包括:
getTarget():调用该方法的实例对象getParams():方法入参数组getReturnObj():正常返回时的返回值getThrowExp():抛出异常时的异常对象isAfterReturning()/isAfterThrowing():本次调用是正常返回还是异常抛出getLoader():目标类对应的 ClassLoader
时间片段容器
所有TimeFragment存放于一个静态的LinkedHashMap<Integer, TimeFragment>中(timeFragmentMap),键为递增的时间片段索引;索引由一个从1000开始的AtomicInteger序列生成器分配(见TimeTunnelCommand中的sequence字段),这就是为什么你看到的第一条记录 INDEX 总是 1000。按文档说明,该 Map 的默认容量为 100。
记录时机
记录动作由增强后的监听器完成,实现在 TimeTunnelAdviceListener.java:
- 方法执行前(
before):通过pushArgs(args)保存入参时的参数快照,并启动耗时计时; - 方法正常返回(
afterReturning)或抛出异常(afterThrowing):取出入参快照、计算耗时,封装为TimeFragment存入容器,并输出一行表格。
一个值得注意的实现细节:
before阶段保存的入参是原始引用快照,afterReturning/afterThrowing阶段会用popArgs()取回它——源码注释明确写道"取出入参时的 args,因为在函数执行过程中 args 可能被修改"。这正是下文"注意事项"中参数可能被业务代码修改这一限制的对策:tt 记录的是进入方法那一刻的参数。
两个必须牢记的预防措施(Precautions)
- 容量有限:
tt的实现是把函数入参/返回值保存进一个Map<Integer, TimeFragment>,默认大小 100。记录达到上限后,新记录会覆盖旧记录。 - 手动释放内存:使用
tt相关功能后,必须手动释放内存(见下文删除/清空小节),否则长时间运行可能发生 OOM;退出 Arthas 并不会自动清空 tt 的缓存 Map(它挂在静态字段上),这一点务必在生产上注意。
环境准备:启动 math-game 示例
本指南的所有示例围绕仓库内的示例程序math-game展开,其源码在 MathGame.java:程序每秒循环执行run(),内部随机生成一个数并调用primeFactors(int number)做质因数分解;当number < 2时抛IllegalArgumentException("number is: ... , need >= 2")——正是我们观察异常时间片段的理想目标。
启动方式请参考仓库文档 Quick Start,或直接:
# 先启动 math-game(一个持续运行的 Java 进程) java -jar math-game.jar # 再启动 Arthas 并 attach 到该进程 ./as.sh记录方法调用:tt -t
-t(--time-tunnel)是核心的"记录"开关,用于增强目标方法并开始录制:
$ tt -t demo.MathGame primeFactors Press Ctrl+C to abort. Affect(class-cnt:1 , method-cnt:1) cost in 66 ms. INDEX TIMESTAMP COST(ms) IS-RET IS-EXP OBJECT CLASS METHOD ------------------------------------------------------------------------------------------------------------------------------------- 1000 2018-12-04 11:15:38 1.096236 false true 0x4b67cf4d MathGame primeFactors 1001 2018-12-04 11:15:39 0.191848 false true 0x4b67cf4d MathGame primeFactors 1002 2018-12-04 11:15:40 0.069523 false true 0x4b67cf4d MathGame primeFactors 1003 2018-12-04 11:15:41 0.186073 false true 0x4b67cf4d MathGame primeFactors 1004 2018-12-04 11:15:42 17.76437 true false 0x4b67cf4d MathGame primeFactors命令语法为tt -t <class-pattern> <method-pattern> [condition-express]。记录期间可随时按Ctrl+C(或Q)停止增强。
控制记录规模:-n、-m
$ tt -t -m 1 demo.MathGame primeFactors Press Q or Ctrl+C to abort. Affect(class count:1 , method count:1) cost in 130 ms, listenerId: 1. INDEX TIMESTAMP COST(ms) IS-RET IS-EXP OBJECT CLASS METHOD ------------------------------------------------------------------------------------------------------------------------------------- 1000 2022-12-25 19:41:45 2.629929 true false 0x3bf400 MathGame primeFactors 1001 2022-12-25 19:41:55 0.146161 false true 0x3bf400 MathGame primeFactors-t:记录demo.MathGame primeFactors的调用上下文。-n <N>(--limits):限制记录条数,避免记录过多导致溢出。配合-n时,Arthas 会在记录数达到指定上限后自动停止记录(源码中由isLimitExceeded触发abortProcess,见 TimeTunnelAdviceListener.java)。默认值为 100。-m <N>:限制匹配到的 Class 数量,避免匹配到过多类导致 JVM 挂起,默认值 50。-c <classloader hash>(--classloader):当同一个类被多个 ClassLoader 加载时,可用-c只增强指定 ClassLoader 下的类。用sc -d <className>可以查到对应的 classloader hash。-E(--regex):启用正则表达式匹配(默认是通配符匹配)。
记录表格属性说明
| 名称 | 说明 |
|---|---|
| INDEX | 按时间递增的调用索引 |
| TIMESTAMP | 方法调用的时间 |
| COST(ms) | 该方法调用的耗时(毫秒) |
| IS-RET | 方法是否正常返回 |
| IS-EXP | 方法是否抛出异常 |
| OBJECT | 调用该方法对象的hashCode()(十六进制) |
| CLASS | 调用该方法的对象所属类名 |
| METHOD | 被调用的方法名 |
条件表达式:只记录关心的调用
tt -t的第三个位置参数是条件表达式(condition-express),只有表达式求值为true的调用才会被记录。常用技巧(来自官方文档 Tips):
- 按参数个数筛选:
tt -t *Test print params.length==1(匹配只有一个参数的方法调用); - 按参数类型筛选:
tt -t *Test print 'params[1] instanceof Integer'; - 按指定参数值筛选:
tt -t *Test print params[0].mobile=="13989838402"。
表达式中可使用的字段即 fundamental fields in expressions 中定义的核心变量(如params、target、returnObj、throwExp、clazz、method等),并支持 OGNL 的完整语法能力。当表达式异常时,TimeTunnelAdviceListener.afterFinishing会中止记录并输出失败原因。
列出所有记录:tt -l
$ tt -l INDEX TIMESTAMP COST(ms) IS-RET IS-EXP OBJECT CLASS METHOD ------------------------------------------------------------------------------------------------------------------------------------- 1000 2018-12-04 11:15:38 1.096236 false true 0x4b67cf4d MathGame primeFactors 1001 2018-12-04 11:15:39 0.191848 false true 0x4b67cf4d MathGame primeFactors 1002 2018-12-04 11:15:40 0.069523 false true 0x4b67cf4d MathGame primeFactors 1003 2018-12-04 11:15:41 0.186073 false true 0x4b67cf4d MathGame primeFactors 1004 2018-12-04 11:15:42 17.76437 true false 0x4b67cf4d MathGame primeFactors 9 1005 2018-12-04 11:15:43 0.4776 false true 0x4b67cf4d MathGame primeFactors Affect(row-cnt:6) cost in 4 ms.tt -l按时间顺序展示全部时间片段。注意表格中那一行孤立的9:当相邻记录的部分列值相同(这里是 OBJECT 列)时,Arthas 会用"合并占位"的方式简化展示。源码中对应processList分支(见 TimeTunnelCommand.java),它把整个timeFragmentMap渲染为TimeFragmentVO列表输出。
搜索记录:tt -s
记录多了之后,可以用-s(--search-express)按 OGNL 表达式过滤出符合条件的记录。表达式结构与条件表达式一致,同样基于Advice的字段:
$ tt -s 'method.name=="primeFactors"' INDEX TIMESTAMP COST(ms) IS-RET IS-EXP OBJECT CLASS METHOD ------------------------------------------------------------------------------------------------------------------------------------- 1000 2018-12-04 11:15:38 1.096236 false true 0x4b67cf4d MathGame primeFactors 1001 2018-12-04 11:15:39 0.191848 false true 0x4b67cf4d MathGame primeFactors 1002 2018-12-04 11:15:40 0.069523 false true 0x4b67cf4d MathGame primeFactors 1003 2018-12-04 11:15:41 0.186073 false true 0x4b67cf4d MathGame primeFactors 1004 2018-12-04 11:15:42 17.76437 true false 0x4b67cf4d MathGame primeFactors 9 1005 2018-12-04 11:15:43 0.4776 false true 0x4b67cf4d MathGame primeFactors Affect(row-cnt:6) cost in 607 ms.从实现上看,processSearch会遍历timeFragmentMap的每一条记录,用ExpressFactory.threadLocalExpress(advice).is(searchExpress)求值过滤(见 TimeTunnelCommand.java)。更进阶的用法是tt -s '<搜索表达式>' -w '<观察表达式>'组合:先筛出命中的记录,再对每条命中记录求观察表达式并输出结果(对应源码hasWatchExpress()分支)。
表达式支持的核心字段参见 Critical fields in expression。
查看调用上下文:tt -i <index>
tt -i按索引查看某一次调用的完整细节,包括参数对象内容、返回值或异常堆栈:
$ tt -i 1003 INDEX 1003 GMT-CREATE 2018-12-04 11:15:41 COST(ms) 0.186073 OBJECT 0x4b67cf4d CLASS demo.MathGame METHOD primeFactors IS-RETURN false IS-EXCEPTION true PARAMETERS[0] @Integer[-564322413] THROW-EXCEPTION java.lang.IllegalArgumentException: number is: -564322413, need >= 2 at demo.MathGame.primeFactors(MathGame.java:46) at demo.MathGame.run(MathGame.java:24) at demo.MathGame.main(MathGame.java:16) Affect(row-cnt:1) cost in 11 ms.这条输出完美对应了 MathGame.java 中primeFactors对number < 2抛异常的逻辑:PARAMETERS[0]为-564322413,异常信息提示 "number is: -564322413, need >= 2"。通过对比多个tt -i的结果,你就能定位"为什么有的请求失败、有的成功"。
若索引不存在,命令会提示Time fragment[<index>] does not exist.(源码processShow分支)。
重放记录:tt -i <index> -p
这是tt最独特的杀手锏:因为时间片段保存了完整的调用上下文(目标对象 + 入参),Arthas 可以事后用当时的参数再次执行该方法,用于高级问题的复现调试:
$ tt -i 1004 -p RE-INDEX 1004 GMT-REPLAY 2018-12-04 11:26:00 OBJECT 0x4b67cf4d CLASS demo.MathGame METHOD primeFactors PARAMETERS[0] @Integer[946738738] IS-RETURN true IS-EXCEPTION false RETURN-OBJ @ArrayList[ @Integer[2], @Integer[11], @Integer[17], @Integer[2531387], ] Time fragment[1004] successfully replayed. Affect(row-cnt:1) cost in 14 ms.-p(--play):开启重放;--replay-times <N>:重放执行次数(默认 1);--replay-interval <ms>:多次重放之间的间隔,单位毫秒,默认 1000。
从源码看,重放的实现(processPlay,见 TimeTunnelCommand.java)是:取出记录的Advice,通过反射method.invoke(advice.getTarget(), advice.getParams())以记录时的参数重新调用目标方法,并重新统计耗时与返回/异常结果。可见tt重放的是"原方法调用",而不是复现字节码执行,因此重放结果反映的是当前时刻的代码行为。
观察表达式:tt -w与-x
-w(--watch-express)用 OGNL 表达式观察某个时间片段,表达式内可使用 fundamental fields in expressions 中定义的全部变量(params、target、returnObj、throwExp等)。-x(--expand)控制对象的展开层级,默认 1。
先记录并取一个索引,再观察目标对象的字段:
[arthas@10718]$ tt -t demo.MathGame run -n 5 Press Q or Ctrl+C to abort. Affect(class count: 1 , method count: 1) cost in 56 ms, listenerId: 1 INDEX TIMESTAMP COST(ms) IS-RET IS-EXP OBJECT CLASS METHOD ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- 1000 2021-01-08 21:54:17 0.901091 true false 0x7699a589 MathGame run [arthas@10718]$ tt -w 'target.illegalArgumentCount' -x 1 -i 1000 @Integer[60] Affect(row-cnt:1) cost in 7 ms.示例中target.illegalArgumentCount读取了 MathGame.java 中定义的实例字段illegalArgumentCount——通过它你可以观察调用对象的内部状态变化。
获取静态字段、调用静态方法
由于tt -w走 OGNL 表达式,你也可以访问静态成员:
[arthas@10718]$ tt -t demo.MathGame run -n 5 Press Q or Ctrl+C to abort. Affect(class count: 1 , method count: 1) cost in 56 ms, listenerId: 1 INDEX TIMESTAMP COST(ms) IS-RET IS-EXP OBJECT CLASS METHOD ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- 1000 2021-01-08 21:54:17 0.901091 true false 0x7699a589 MathGame run [arthas@10718]$ tt -w '@demo.MathGame@random.nextInt(100)' -x 1 -i 1000 @Integer[46]这里用@demo.MathGame@random.nextInt(100)访问了 MathGame.java 中的静态字段random并调用了它的nextInt(100)。
关于 ClassLoader 的注意点:表达式求值内部使用的是
com.taobao.arthas.core.advisor.Advice#getLoader返回的 ClassLoader(见 Advice.java)。若目标方法在自定义 ClassLoader 下加载,建议配合 ognl 命令 使用精确的 ClassLoader,避免因 ClassLoader 隔离导致表达式访问不到目标类。若需要通过获取 Spring 上下文调用 Bean 方法做更深入的观察,可参考官方 issue #482 中给出的思路。
注意事项(F.Y.I):使用tt前必须了解的限制
ThreadLocal会丢失:Arthas 把入参保存到数组后,重放时会在另一个线程中再次调用该方法,因此方法执行期间依赖的ThreadLocal在重放场景下会丢失,涉及线程上下文(如 traceId、用户态信息)的方法重放结果可能失真。- 参数可能被修改:保存到数组里的是对象引用,而非深拷贝。这些对象可能被其他业务代码修改,导致之后查看/重放时看到的是"变了形"的参数。作为缓解,监听器在
before阶段会保存入参时的快照(见上文源码解析),记录的是方法进入瞬间的参数。 - 内存占用:时间片段持有目标对象与参数对象的强引用,记录长期不清理会积累内存,务必在排查结束后用删除/清空命令释放。
删除与清理:tt -d、tt --delete-all
按索引删除单条记录:
tt -d -i 1001清空全部记录:
tt --delete-all对应源码实现:processDelete从timeFragmentMap中remove(index)并输出 "Time fragment[ ] successfully deleted.";processDeleteAll直接clear()整个 Map 并输出 "Time fragments are cleaned."(见 TimeTunnelCommand.java)。由于该 Map 是静态的、退出 Arthas 也不会自动清理,建议在每次tt排查结束后主动执行tt --delete-all,这是避免长时间运行 OOM 的关键习惯。
参数速查表
| 参数 | 长选项 | 说明 | 默认值 |
|---|---|---|---|
-t | --time-tunnel | 记录方法调用为时间片段 | 关闭 |
-l | --list | 列出全部时间片段 | 关闭 |
-i <index> | --index | 查看/操作指定索引的时间片段 | 无 |
-s <expr> | --search-express | 按 OGNL 表达式搜索记录 | 无 |
-w <expr> | --watch-express | 对指定记录求 OGNL 观察表达式 | 无 |
-p | --play | 重放指定索引的记录 | 关闭 |
--replay-times <N> | - | 重放次数 | 1 |
--replay-interval <ms> | - | 重放间隔(毫秒) | 1000 |
-n <N> | --limits | 记录条数上限,达到后自动停止 | 100 |
-m <N> | - | 匹配类数量上限,避免 JVM 挂起 | 50 |
-c <hash> | --classloader | 只增强指定 ClassLoader 下的类 | 无 |
-E | --regex | 启用正则匹配类/方法名 | 关闭 |
-x <N> | --expand | 结果对象的展开层级 | 1 |
-d | --delete | 删除指定索引的记录(需配合-i) | 关闭 |
--delete-all | - | 清空全部记录 | 关闭 |
-M <bytes> | --sizeLimit | 结果对象大小上限(字节,需大于 0) | 来自 options object-size-limit |
总结:tt 的典型排查流程
tt -t <Class> <method> [-n 100] [condition-express]开启记录,按条件只捕获你关心的调用;- 问题发生后
tt -l概览、tt -s '<expr>'精确筛出问题记录; tt -i <index>查看该次调用的参数、返回值与异常堆栈,结合 MathGame.java 这类业务代码对照分析;- 需要复现时
tt -i <index> -p [--replay-times N] [--replay-interval ms]用历史参数重放; - 用
tt -w '<expr>' -x 1 -i <index>深入观察对象内部状态或调用静态方法; - 排查结束后
tt --delete-all释放内存。
tt与 watch、stack、trace 等命令互为补充:watch/trace适合实时观测,而tt的价值在于"事后取证"与"历史重放",是线上偶发问题定位中不可替代的一环。更多命令级用法可参阅仓库文档 commands 与 arthas3。
【免费下载链接】arthasAlibaba Java Diagnostic Tool Arthas/Alibaba Java诊断利器Arthas项目地址: https://gitcode.com/gh_mirrors/ar/arthas
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考