引言

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.UserServiceclass-patterngetUserInfomethod-pattern,由于没有添加条件表达式,所以condition-express为空。

(二)关键参数

可以使用stack -h 查看当前版本的arthas中stack命令支持的参数。

1.class-pattern(类名表达式匹配):它支持通配符匹配,例如*Service可以匹配所有以Service结尾的类名。在实际应用中,如果我们不确定具体的类名,但知道类名的部分特征,就可以使用通配符来进行模糊匹配。比如在一个大型的电商项目中,有很多服务类,我们只记得某个涉及订单操作的服务类名中包含 “Order”,就可以使用*Order*Service来尝试匹配相关的类。

2.method-pattern(方法名表达式匹配):同样支持通配符匹配,get*可以匹配所有以get开头的方法名。在一个数据访问层的类中,有多个获取数据的方法,如getUserByIdgetUserByName等,我们可以使用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);
            }
        }
    }
}

文章作者: Z
本文链接:
版权声明: 本站所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 微博客
工具 运维 基础
喜欢就支持一下吧