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 注释执行相同的操作。
在 table 用户上创建计算列作为 upper(username):
改变 table 用户添加 upper_username 作为上层(用户名)
在该列上创建索引:
在 a_upload(upper_username)
上创建索引 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()。
我是 运行 一个 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 注释执行相同的操作。
在 table 用户上创建计算列作为 upper(username):
改变 table 用户添加 upper_username 作为上层(用户名)
在该列上创建索引:
在 a_upload(upper_username)
上创建索引 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()。