ASP.NET Zero Authenticate 性能问题

前言

​伴随着ASP.NET Zero系统日渐运行,通过/api/TokenAuth/Authenticate获取Token的速度会逐渐变慢,到最后会呈现出一次获取会超过20秒或者导致超时的现象。

先说结论

导致问题产生的代码:

​ TokenAuthController > CreateJwtClaims > AddTokenValidityKeyAsync

await _userManager.AddTokenValidityKeyAsync(
    user,
    tokenValidityKey,
    expirationDate
);

问题

为什么一个获取Token的接口会出现如此性能问题呢?究竟是什么情况下会产生这样的问题?带着这样的疑问,开始了以下的debug之路。

一、调试Authenticate

  1. ​ 通过postman调用/api/TokenAuth/Authenticate

  2. 从调用结果看到,我们获取Token的总耗时是2.20s。

  3. 通过debug接口可以发现接口/api/TokenAuth/Authenticate的主要耗时点在CreateAccessToken,总共花费了2.02s

  4. 这段代码是在做什么呢?通过代码分析,我们可以知道他总共做了2件事情。第一件事情就是CreateJwtClaims,第二件事情就是通过JwtClaims创建JwtSecurityToken

  5. 再次对CreateAccessToken&CreateJwtClaims进行调试,从以下截图我们可以知道。罪魁祸首就是AddTokenValidityKeyAsync,基本的耗时都在这里。

  6. 通过ILSpy工具观看源码

    //AbpUserManager.cs	
    public virtual async Task RemoveTokenValidityKeyAsync(TUser user, string tokenValidityKey, CancellationToken cancellationToken = default(CancellationToken))
    {
        await AbpUserStore.RemoveTokenValidityKeyAsync(user, tokenValidityKey, cancellationToken);
    }
    
    //AbpUserStore.cs
    public virtual async Task AddTokenValidityKeyAsync(TUser user, string tokenValidityKey, DateTime expireDate, CancellationToken cancellationToken = default(CancellationToken))
    {
        cancellationToken.ThrowIfCancellationRequested();
        Check.NotNull<TUser>(user, "user");
        await RepositoryExtensions.EnsureCollectionLoadedAsync<TUser, long, UserToken>(UserRepository, user, (Expression<Func<TUser, IEnumerable<UserToken>>>)((TUser u) => u.Tokens), cancellationToken);
        user.Tokens.Add(new UserToken((AbpUserBase)(object)user, "TokenValidityKeyProvider", tokenValidityKey, null, expireDate));
    }
    
  7. 其中await RepositoryExtensions.EnsureCollectionLoadedAsync会加载所有导航实体,此操作会随着AbpUserTokens表的增大而耗费的时间加长。

二、解决问题

  1. 方法一:通过sql server 直接删除 AbpUserTokens 表数据

    --这区间内的数据是一年后过期的RefreshToken数据,AccessToken的失效日期是一天,基本不需要处理。
    DECLARE @dtnow datetime = getdate()
    DECLARE @BeginTime datetime = DATEADD(day, 2, @dtnow)
    DECLARE @EndTime datetime = DATEADD(year, 1, @dtnow)
    DELETE FROM AbpUserTokens WHERE Id IN (SELECT Id FROM AbpUserTokens WHERE ExpireDate > @BeginTime AND ExpireDate <= @EndTime)
    
  2. 方法二:通过ASP.NET Zero 框架中的 background job定时删除

    public class UserTokenExpirationWorker : PeriodicBackgroundWorkerBase, ISingletonDependency
        {
            private readonly IUnitOfWorkManager _unitOfWorkManager;
            private readonly IRepository<UserToken, long> _userTokenRepository;
            private readonly IRepository<Tenant> _tenantRepository;
            public UserTokenExpirationWorker(
                IUnitOfWorkManager unitOfWorkManager,
                IRepository<UserToken, long> userTokenRepository,
                IRepository<Tenant> tenantRepository,
                AbpTimer timer)
                : base(timer)
            {
                _userTokenRepository = userTokenRepository;
                _unitOfWorkManager = unitOfWorkManager;
                _tenantRepository = tenantRepository;
                Timer.Period = 1 * 1000 * 60; // 执行间隔时间;
            }
    
            [UnitOfWork]
            protected override void DoWork()
            {
                List<int> tenantIds;
                var utcNow = Clock.Now.ToUniversalTime();
                //由于AccessToken有效期是1天有效期,故此处是当前时间+2天。区间和范围都可以根据自己实际情况调整
                var beginTime = utcNow.AddDays(2);
                var endTime = utcNow.AddYears(1);
    
                using (var uow = _unitOfWorkManager.Begin())
                {
                    using (_unitOfWorkManager.Current.SetTenantId(null))
                    {
                        _userTokenRepository.Delete(t => t.ExpireDate > beginTime && t.ExpireDate <= endTime);
                        tenantIds = _tenantRepository.GetAll().Select(t => t.Id).ToList();
                        uow.Complete();
                    }
                }
    
                foreach (var tenantId in tenantIds)
                {
                    using (var uow = _unitOfWorkManager.Begin())
                    {
                        using (_unitOfWorkManager.Current.SetTenantId(tenantId))
                        {
                            _userTokenRepository.Delete(t => t.ExpireDate <= beginTime && t.ExpireDate <= endTime);
                            uow.Complete();
                        }
                    }
                }
            }
        }
    
  3. 执行完以上2种方法后再次调用接口/api/TokenAuth/Authenticate,仅耗时54ms,获取速度回归到正常水平。

三、参考

  1. _userManager.AddTokenValidityKeyAsync performance issue · Issue #5244 · aspnetboilerplate/aspnetboilerplate (github.com)
posted @ 2024-04-02 19:15  Destiny、Yang  阅读(28)  评论(0)    收藏  举报