Arthas之Stack命令:如何根据一行日志定位调用链路?
引言
stack 命令的主要职责是输出当前方法被调用的调用路径。这一功能在众多场景中都能发挥出巨大的作用。比如在故障排查时,当应用出现性能问题或崩溃等异常状况,我们可以借助 stack 命令查看线程栈,从而迅速定位问题的源头;在进行死锁分析时,通过 stack 命令监控线程状态,能够及时检查是否存在死锁或线程阻塞的情况;在性能优化阶段,利用 stack 命令分析线程的执行路径,有助于我们识别出可能存在的性能瓶颈。
接下来,我将结合一个实际的线上问题解决示例,为大家详细介绍 stack 命令的使用方法和技巧。
Stack 命令详解
(一)基本语法
stack 命令的基本语法结构为:stack [class-pattern] [method-pattern] [condition-express]
。其中,class-pattern
表示类名表达式匹配,用于指定要追踪的类;method-pattern
表示方法名表达式匹配,用于指定要追踪的方法;condition-express
是条件表达式,使用 OGNL 语法,用于对方法调用进行条件过滤 。
例如,我们想要查看com.example.demo.service.UserService
类中getUserInfo
方法的调用路径,命令可以这样写:stack com.example.demo.service.UserService getUserInfo
。在这个例子中,com.example.demo.service.UserService
是class-pattern
,getUserInfo
是method-pattern
,由于没有添加条件表达式,所以condition-express
为空。
(二)关键参数
可以使用stack -h
查看当前版本的arthas中stack命令支持的参数。
1.class-pattern(类名表达式匹配):它支持通配符匹配,例如*Service
可以匹配所有以Service
结尾的类名。在实际应用中,如果我们不确定具体的类名,但知道类名的部分特征,就可以使用通配符来进行模糊匹配。比如在一个大型的电商项目中,有很多服务类,我们只记得某个涉及订单操作的服务类名中包含 “Order”,就可以使用*Order*Service
来尝试匹配相关的类。
2.method-pattern(方法名表达式匹配):同样支持通配符匹配,get*
可以匹配所有以get
开头的方法名。在一个数据访问层的类中,有多个获取数据的方法,如getUserById
、getUserByName
等,我们可以使用get*
来一次性追踪所有获取用户信息的方法调用路径。
3.condition-express(条件表达式):这是一个非常强大的功能,它基于 OGNL(Object-Graph Navigation Language)语法。通过条件表达式,我们可以根据方法的参数、返回值、执行耗时等条件来过滤方法调用。例如,params[0] > 10
表示当方法的第一个参数大于 10 时才进行追踪;#cost > 100
表示当方法执行耗时超过 100 毫秒时进行追踪 。在一个计算商品价格的方法中,我们可能只关心价格超过 1000 元的计算过程,就可以使用params[0] > 1000
作为条件表达式来过滤方法调用。isThrow==true
,过滤异常的请求。
# 查看CustomerService类的list方法的调用链路
# 系统调用量特别大时-n参数限制捕获的调用次数(达到次数后停止)
# 查找类名时,可以用*进行匹配,防止类匹配过多,可以使用-m参数
stack com.cly.sms.admin.web.service.CustomerService* *list -n 10 -m 3
# 当类中有重载方法时,我们可以根据方法的参数数量、参数类型过滤
# 例如:params.length==1,'params[1] instanceof Integer',params[0].mobile=="13989838402",params[0].contains("xxx")
# 记录参数下标为2的,userName值为33的请求,这样可以更精确的拦截到我们需要的请求记录
stack com.cly.sms.admin.web.service.CustomerService list params[2].userName=="33" -n 10
(三)使用场景
1.故障排查:当应用出现异常行为,如崩溃、报错或者返回错误结果时,stack 命令可以帮助我们快速定位问题的根源。通过查看方法的调用路径,我们可以了解到在问题发生时,程序的执行流程是怎样的,从而找出可能出现问题的代码位置。比如在一个文件上传功能中,突然出现上传失败并报错的情况,我们可以使用 stack 命令追踪文件上传方法的调用路径,查看是否是因为文件路径错误、权限不足等原因导致的问题。
2.性能优化:在性能优化过程中,我们需要找出应用中的性能瓶颈。stack 命令可以帮助我们分析方法的调用路径,找出那些执行耗时较长或者被频繁调用的方法。通过优化这些方法,我们可以提高应用的整体性能。在一个高并发的电商系统中,订单处理的速度直接影响用户体验,我们可以使用 stack 命令追踪订单处理方法的调用路径,分析哪些环节耗时较长,进而进行针对性的优化,如优化数据库查询语句、减少不必要的计算等。
3.死锁分析:死锁是多线程编程中常见的问题,它会导致应用无法正常运行。stack 命令可以监控线程状态,通过查看线程的调用栈信息,我们可以检查是否存在死锁或线程阻塞的情况。一旦发现死锁,我们可以根据调用栈信息找出死锁发生的位置和原因,从而采取相应的措施来解决问题,如调整线程的加锁顺序、增加超时机制等。
线上问题实战:定位 Redis 缓存日志源头
(一)问题浮现
在我们的线上系统中,近期突然出现了频繁打印拉取 Redis 缓存的日志信息,导致系统无法正常提供服务 。然而,通过传统的日志分析方法,我们无法确定到底是哪个方法在调用拉取 Redis 缓存的操作。
(二)排查之旅
面对这一棘手的问题,我们决定使用 Arthas 的 stack 命令来寻找突破口。首先,我们需要确定与 Redis 缓存操作相关的类和方法。我们直接到代码中搜索日志中相关的类和方法:com.github.jesse.l2cache.cache.RedissonRBucketCache#batchGet
接下来,我们在 Arthas 的命令行中输入:stack com.github.jesse.l2cache.cache.RedissonRBucketCache batchGet -n 1
。执行命令后,Arthas 开始输出batchGet
方法的调用路径信息如下:
从调用链路中我们很清晰的可以看到频繁打印的redis日志是由该方法导致的。我们可以针对性的去检查该代码。最终定位到了其中一个双层嵌套循环导致的bug。然后就可以针对该问题去解决。
public void refreshAuthoritiesByRoleId(Integer roleId) {
Role role = getRole(roleId);
Collection<Client> clientList = getClientList(role.getAppId());
for (Client client : clientList) {
// tokenList数量太大,导致重复执行restoreAccessToken。需要排查tokenList数量太大的原因。
Collection<OAuth2AccessToken> tokenList = tokenStore.findTokensByClientId(client.getClientKey());
for (OAuth2AccessToken token : tokenList) {
OAuth2Authentication authentication = tokenStore.readAuthentication(token);
if (authentication != null) {
restoreAccessToken(token, authentication);
}
}
}
}