500万用户如何应对? ASP.NET 身份

How to handle 5 million users? ASP.NET Identity

我是 运行 一个 ASP.NET mvc5 应用程序,目前有 500 万用户。它托管在 Azure 云中。对于身份验证,我使用 EntityFramework 的 Asp.Net 身份。

但是,我的用户越多,注册功能就越慢。我尝试缩放数据库,但结果仍然相同。新用户注册大约需要 6-7 秒。

我也试着搜索如何提高身份系统的性能,但我找不到任何相关的东西。

我真的很想知道是否有人知道如何提高它的性能。

更新:我在搜索的字段上有索引,而且,我在 Azure 中选择的数据库订阅是 P3 SQL 数据库,有 200 个 DTU。

我分析了数据库并发现了一个可疑的 select 查询。 我删除了一些投影并将它们替换为“...”,这样它就不会太长,您可以看到查询的内容。

SELECT
    [UnionAll2].[性别] AS [C1],
    ....
    [UnionAll2].[用户名] AS [C27],
    [UnionAll2].[C1] AS [C28],
    [UnionAll2].[UserId] AS [C29],
    [UnionAll2].[RoleId] AS [C30],
    [UnionAll2].[UserId1] AS [C31],
    [UnionAll2].[C2] AS [C32],
    [UnionAll2].[C3] AS [C33],
    [UnionAll2].[C4] AS [C34],
    [UnionAll2].[C5] AS [C35],
    [UnionAll2].[C6] AS [C36],
    [UnionAll2].[C7] AS [C37],
    [UnionAll2].[C8] AS [C38],
    [UnionAll2].[C9] AS [C39]
    从 (SELECT
        CASE WHEN ([Extent2].[UserId] IS NULL) THEN CAST(NULL AS int) ELSE 1 END AS [C1],
        [限制1].[性别] AS [性别],
        ....
        [Limit1].[用户名] AS [用户名],
        [Extent2].[UserId] AS [UserId],
        [Extent2].[RoleId] AS [RoleId],
        [Extent2].[UserId] AS [UserId1],
        CAST(NULL AS int) AS [C2],
        CAST(NULL AS varchar(1)) AS [C3],
        CAST(NULL AS varchar(1)) AS [C4],
        CAST(NULL AS varchar(1)) AS [C5],
        CAST(NULL AS varchar(1)) AS [C6],
        CAST(NULL AS varchar(1)) AS [C7],
        CAST(NULL AS varchar(1)) AS [C8],
        CAST(NULL AS varchar(1)) AS [C9]
        从 (SELECT 顶部 (1)
            [Extent1].[Id] AS [Id],
            ....
            [Extent1].[用户名] AS [用户名]
            FROM [dbo].[Users] AS [Extent1]
            其中 ((UPPER([Extent1].[UserName])) = (UPPER(@p__linq__0))) 或 ((UPPER([Extent1].[UserName]) 为 NULL) AND (UPPER(@p__linq__0) 是 NULL)) ) 作为 [Limit1]
        LEFT OUTER JOIN [dbo].[UserRoles] AS [Extent2] ON [Limit1].[Id] = [Extent2].[UserId]
    联合所有
        SELECT
        2 作为 [C1],
        [Limit2].[性别] AS [性别],
        ....
        [Limit2].[用户名] AS [用户名],
        CAST(NULL AS varchar(1)) AS [C2],
        CAST(NULL AS varchar(1)) AS [C3],
        CAST(NULL AS varchar(1)) AS [C4],
        [Extent4].[Id] AS [Id1],
        [Extent4].[UserId] AS [UserId],
        [Extent4].[ClaimType] AS [ClaimType],
        [Extent4].[ClaimValue] AS [ClaimValue],
        CAST(NULL AS varchar(1)) AS [C5],
        CAST(NULL AS varchar(1)) AS [C6],
        CAST(NULL AS varchar(1)) AS [C7],
        CAST(NULL AS varchar(1)) AS [C8]
        从 (SELECT 顶部 (1)
            [Extent3].[Id] AS [Id],
            ....
            [Extent3].[用户名] AS [用户名]
            FROM [dbo].[Users] AS [Extent3]
            其中 ((UPPER([Extent3].[UserName])) = (UPPER(@p__linq__0))) 或 ((UPPER([Extent3].[UserName]) 为 NULL) AND (UPPER(@p__linq__0) 是 NULL)) ) 作为 [Limit2]
        INNER JOIN [dbo].[UserClaims] AS [Extent4] ON [Limit2].[Id] = [Extent4].[UserId]
    联合所有
        SELECT
        3 作为 [C1],
        [Limit3].[性别] AS [性别],
        ....
        [Limit3].[用户名] AS [用户名],
        CAST(NULL AS varchar(1)) AS [C2],
        CAST(NULL AS varchar(1)) AS [C3],
        CAST(NULL AS varchar(1)) AS [C4],
        CAST(NULL AS int) AS [C5],
        CAST(NULL AS varchar(1)) AS [C6],
        CAST(NULL AS varchar(1)) AS [C7],
        CAST(NULL AS varchar(1)) AS [C8],
        [Extent6].[LoginProvider] AS [LoginProvider],
        [Extent6].[ProviderKey] AS [ProviderKey],
        [Extent6].[UserId] AS [UserId],
        [Extent6].[UserId] AS [UserId1]
        从 (SELECT 顶部 (1)
            [Extent5].[Id] AS [Id],
            ....
            [Extent5].[用户名] AS [用户名]
            FROM [dbo].[Users] AS [Extent5]
            其中 ((UPPER([Extent5].[UserName])) = (UPPER(@p__linq__0))) 或 ((UPPER([Extent5].[UserName]) 为 NULL) AND (UPPER(@p__linq__0) 是 NULL)) ) 作为 [Limit3]
        INNER JOIN [dbo].[UserLogins] AS [Extent6] ON [Limit3].[Id] = [Extent6].[UserId]) AS [UnionAll2]
    按 [UnionAll2].[Id] ASC, [UnionAll2].[C1] ASC
排序

我的 EntityFramework 用户 POCO class

    public class User : IdentityUser
    {
        [Index]
        public DateTime Created { get; set; }
        [Index(IsUnique = true), MaxLength(255)]
        public override string Email { get; set; }
        public string Firstname { get; set; }
        public string Lastname { get; set; }
        [Index]
        public GenderType Gender { get; set; }
        [Index]
        public DateTime? Birthdate { get; set; }
        [Index, MaxLength(2)]
        public string Country { get; set; }
        [MaxLength(2)]
        public string Language { get; set; }
        [Index, MaxLength(256)]
        public string Referral { get; set; }
        public string ImageUrl { get; set; }
        [Index]
        public UserIdentityStatus IdentityConfirmed { get; set; }
        [Index]
        public DateTime? Deleted { get; set; }
        public ICollection<Reward> Ads { get; set; }
        public ICollection<Thought> Thoughts { get; set; }
        public ICollection<Achievement> Achievements { get; set; }
        public ICollection<Subscription> Subscriptions { get; set; }
        public DateTime? TutorialShown { get; set; }
        [Index]
        public DateTime? LastActivity { get; set; }
        [Index]
        public DateTime? LastBulkEmail { get; set; }
    }

尝试从某些 Azure 审计工具中获取准确的 SQL 命令,并将其写到此处的评论中。 7 秒看起来像是索引中的问题(对于索引蚂蚁的访问来说太长了,对于将大量数据传输到客户端来说太短了)。

INSERT 命令本身总是很快(如果没有被触发器减慢)并且不会随着行数的增加而显着变慢。身份框架可能是 运行 一些 SELECT(例如重新阅读新行...?)。在所有受影响的列上都有索引是不够的。 Select 命令在每个 table 上只能使用一个索引,所以你只需要找到一个索引,但正确的索引 - 正确的列组合,索引中的列顺序也很重要。而这一切都取决于 SQL 命令,即试图执行的身份引擎。试着找到它,然后我可以帮助你。

是的,这个查询可能是问题所在。这就是我不喜欢 SQL 框架的原因。

问题出在这里:

WHERE 
((UPPER([Extent3].[UserName])) = (UPPER(@p__linq__0))) 

右侧是不变的,左侧 "upper(username)" 是 SQL 试图找到某处的引擎。简单 "username" 上的索引是无用的,因为它包含简单 "username" 而不是 upper(username) 值。 SQL 每次添加新用户时,引擎都必须为所有 500 万行计算 upper(username),然后找到相应的值。 7 秒...

您需要创建这样的索引:

create index ix_name on Users(upper(UserName))

这样的索引将被sql 引擎自动使用。但恐怕 MS SQL 将不允许您在不添加计算列的情况下执行此操作(这对您的目的没有用,因为您不能强制 EF 使用另一列作为用户名值)。

另一个选项是强制 Identity framework 不使用 UPPER 函数。但是我不知道怎么办。

顺便说一句,我真的很惊讶在如此重要的包中找到这种类型的代码 - entity framework。 "searching functional value in index" 是编写高效 sql 代码时的基本错误之一。

还有一条评论:这一切都应该由框架完成,它负责正确处理 "username" 列。这是一个错误,此建议只是解决方法。

我想我有适合您的解决方案。我做了另一个测试,第二个选项 - 计算列上的索引有效。以下是 sql 代码中的步骤,您可能可以使用 EF 注释执行相同的操作。

  1. 在 table 用户上创建计算列作为 upper(username):

    改变 table 用户添加 upper_username 作为上层(用户名)

  2. 在该列上创建索引:

    在 a_upload(upper_username)

  3. 上创建索引 ix2

就是这样。 EF select 仍将使用 UPPER,但 MS SQL 优化器应该能够使用此索引,因为它与 where 子句中的函数具有相同的定义。

以下是我电脑上的测试结果:

test sql: select field001 from a_upload where upper(field001)='10'

BEFORE(SCAN表示引擎要一条一条读取所有记录)

在功能列上创建索引后(SEEK=引擎将利用索引)

即使在 BEFORE 场景中,也不要感到困惑,sql 引擎正在使用索引 (ix1)。这只是因为我 select 只 "field001" 并且优化器知道,它不仅包含在 table 中,而且也包含在索引中。并且索引的字节数少于整个 table。但这并不意味着系统使用索引,它必须为每个 select 上的每一行计算 upper() 无论如何。

虽然我不得不用 int 替换 userId guid,但我创建了一个 CustomUserStore。在此 UserStore 中,您可以简单地覆盖 FindByNameAsync:

public class CustomUserStore<TUser> : UserStore<TUser, CustomRole, int, CustomUserLogin, CustomUserRole, CustomUserClaim> where TUser : MyUser
{
    public CustomUserStore(MyDbContext context) : base(context) { }

    public override Task<TUser> FindByNameAsync(string userName)
    {
        return this.GetUserAggregateAsync(u => u.UserName.Equals(userName, StringComparison.InvariantCultureIgnoreCase));
    }
}

这导致查询没有 UPPER()。