记一次生产问题的排查思路

文章描述了一个系统中用户登录变慢的问题,通过排查发现与用户认证环节的数据库查询无关,怀疑是Shiro缓存过多过期会话导致。借助开源工具Arthas追踪方法执行,定位到SessionManager的定时清理逻辑存在问题,修复后解决了登录问题。
摘要由CSDN通过智能技术生成

最近系统出现一个很奇怪的问题,用户登录变得十分缓慢,平均登录时间在10-20s不等,并且伴随出现了堆内存告警,重启服务后问题依然存在,此问题严重影响了系统正常使用。

为了避免影响用户体验,我们首先采取的办法是对版本进行回退,将代码回退到上一发布版本,但回退后发现问题依然存在。

尝试在测试环境复现此场景,但相同的代码在测试环境却无法复现登录缓慢的情况。

通过排查日志登录接口,显示主要耗时在用户认证环节,因为涉及到对用户信息的查询,首先怀疑可能是数据库问题,通过日志分析相关sql执行时间都不超过50ms,同时其他业务功能是正常的,例如业务数据查询速度很快,因此可以排除数据库问题。

由于生产环境日志级别为error,我们只能知道主要耗时在用户认证环节,但是过程中涉及很多的方法调用,无法具体定位是哪一块代码的执行时间很长。为了解决这一问题,可以尝试在相关执行方法上都增加耗时统计的逻辑,然后调低日志级别,最后通过日志来定位具体问题,但是这样做需要增加很多无业务意义的代码,并且在抓取到日志后又得再回退版本,对于生产环境上线流程来说十分繁琐。

问题在测试环境无法复现,并且生产环境能够排查到的信息十分有限,那有没有什么办法可以不修改线上代码,就可以定位代码执行情况呢?有的,我们使用了Alibaba 开源的 Java 诊断工具Arthas,以下是官方对这个工具的介绍:Arthas 是一款线上监控诊断产品,通过全局视角实时查看应用 load、内存、gc、线程的状态信息,并能在不修改应用代码的情况下,对业务问题进行诊断,包括查看方法调用的出入参、异常,监测方法执行耗时,类加载信息等,大大提升线上问题排查效率。
官方首页 https://arthas.aliyun.com/

通过部署Arthas,使用trace打印方法内部调用路径,并输出方法路径上的每个节点上耗时,在定位到方法后,使用watch命令观测函数的出入参数,最终发现是在认证过程中筛选当前用户所属会话的操作,大概的处理逻辑是从缓存中拿到所有session并进行循环处理,这里发现循环的session集合竟然有十多万条,意味着每次用户登陆都需要进行大量的循环处理。好了,问题就出在这里。

为什么会堆积这么多的会话呢?系统的认证框架是shiro,用户登录后会话会放入shiro缓存,通过分析发现其实缓存中大部分会话已经过期,但是系统却没有销毁过期的session,于是检查SessionManager的配置,发现是有定时清理失效会话的处理sessionValidationSchedulerEnabled=true,默认使用ExecutorServiceSessionValidationScheduler调度器来进行定时清理,下面是简化后的源码。

public class ExecutorServiceSessionValidationScheduler implements SessionValidationScheduler, Runnable {

	......
    
    public void enableSessionValidation() {
        if (this.interval > 0L) {
            //创建一个定时器,定时验证所有session
            this.service = Executors.newSingleThreadScheduledExecutor(new ThreadFactory() {
                public Thread newThread(Runnable r) {
                    Thread thread = new Thread(r);
                    thread.setDaemon(true);
                    return thread;
                }
            });
            this.service.scheduleAtFixedRate(this, this.interval, this.interval, TimeUnit.MILLISECONDS);
            this.enabled = true;
        }
    }

    public void run() {
        ......
        long startTime = System.currentTimeMillis();
        //下面这个方法会定时调用
        this.sessionManager.validateSessions();
        long stopTime = System.currentTimeMillis();
        if (log.isDebugEnabled()) {
            log.debug("Session validation completed successfully in " + (stopTime - startTime) + " milliseconds.");
        }

    }
}

分析上面代码说明SessionManager的validateSessions方法会定时得到执行,而我们业务代码中对这个方法进行了复写,复写后的代码如下:

public class OnlineWebSessionManager extends DefaultWebSessionManager{
    ......
    
    /**
     * 验证session是否有效 用于删除过期session
     */
    @Override
    public void validateSessions()
    {
        //在这里出现了异常
        Principal p = UserUtils.getPrincipal();
        
        String loginName = p.getLoginName;
        ......
    	super.validateSessions();
        ......
    }
}

通过断点调试发现11行代码抛出了异常,所以后面的校验根本没有执行,难受的是控制台输出日志中没有任何异常信息,并且定时任务中断,不会再次执行,也就是说,之前的session都没有销毁成功,同时日志没有任何提示,直到这次上线才暴露出这个问题。

修改此处代码后session校验可以正常执行,大量过期session得到回收,登录业务恢复正常。虽然问题得到解决,但是很奇怪为什么定时任务在遇到异常后会中断并不会继续执行,推测官方是认为重复执行一个失败的任务没有意义,所以遇到异常后直接就失效了。百度后发现很多人也遇到同样的问题,解决方案是进行try_catch处理,将异常信息暴露出来,这样虽然任务会执行失败,但是定时任务不会失效,并且会将异常信息暴露出来。

Java中的定时任务-ScheduledExecutorService的坑-CSDN博客

为什么系统之前没有出现过这个问题呢?主要原因是最近上线了系统健康探测的组件,每搁十秒会进行一次远程调用,这就导致了大量的孤立会话,加上清理任务失效,以至于出现了大量会话堆积。

补充:查看最新版本shiro的源码发现官方已注意到这个问题了,原先的版本是1.2.3,查看最新版本源码,在ExecutorServiceSessionValidationScheduler中增加了try_catch处理,以下是1.10.1的源码。

public class ExecutorServiceSessionValidationScheduler implements SessionValidationScheduler, Runnable {
	 ......
	 public void run() {
	        ......
	        Thread.currentThread().setUncaughtExceptionHandler((t, e) -> {
	            log.error("Error while validating the session, the thread will be stopped and session validation disabled", e);
	            this.disableSessionValidation();
	        });
	        long startTime = System.currentTimeMillis();
	
	        try {
	            this.sessionManager.validateSessions();
	        } catch (RuntimeException var5) {
	            log.error("Error while validating the session", var5);
	        }
	
	        long stopTime = System.currentTimeMillis();
	        if (log.isDebugEnabled()) {
	            log.debug("Session validation completed successfully in " + (stopTime - startTime) + " milliseconds.");
	        }

    }

参考:https://github.com/apache/shiro/commit/04866620386dd20ca292328218bc852f17fa4c18

  • 16
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值