为什么当我的 Asp.Net Core API 每秒并发请求数增加时,响应时间也会增加

2024-04-24

我正在负载下测试端点。对于每秒 1 个请求,平均响应时间约为 200 毫秒。端点执行一些数据库查找(全部读取),速度非常快,并且自始至终都是异步的。

然而,当每秒执行数百个请求 (req/sec) 时,平均响应时间会增加到一秒多。

我查看了最佳实践指南:

https://learn.microsoft.com/en-us/aspnet/core/performance/performance-best-practices?view=aspnetcore-2.2 https://learn.microsoft.com/en-us/aspnet/core/performance/performance-best-practices?view=aspnetcore-2.2

一些建议,例如“避免阻塞呼叫" and "最小化大对象分配“似乎它们并不适用,因为我已经在整个过程中使用异步,并且单个请求的响应大小小于 50 KB。

有一对夫妇,虽然看起来像他们可能有用, 例如:

https://learn.microsoft.com/en-us/ef/core/what-is-new/ef-core-2.0#high-performance https://learn.microsoft.com/en-us/ef/core/what-is-new/ef-core-2.0#high-performance https://learn.microsoft.com/en-us/aspnet/core/performance/performance-best-practices?view=aspnetcore-2.2#pool-http-connections-with-httpclientfactory https://learn.microsoft.com/en-us/aspnet/core/performance/performance-best-practices?view=aspnetcore-2.2#pool-http-connections-with-httpclientfactory

问题:

  1. 为什么平均响应时间会随着请求/秒的增加而增加?
  2. 上面我标记为“可能有用”的建议可能有帮助吗?我之所以这么问,是因为虽然我想尝试所有的方法,但不幸的是,我的时间有限,所以我想首先尝试最有可能有帮助的选项。
  3. 还有其他值得考虑的选择吗?

我查看了这两个现有线程,但都没有回答我的问题:

每秒请求数和响应时间之间的相关性? https://stackoverflow.com/q/701537

ASP.NET Web API - 每秒处理更多请求 https://stackoverflow.com/q/37022458


如果不访问代码,将很难回答您的具体问题,但要考虑的主要问题是 EF 生成的数据库查询的大小和复杂性。使用 async/await 将提高 Web 服务器启动请求的响应能力,但负载下的请求处理时间将很大程度上取决于数据库成为争用点时正在运行的查询。您需要确保所有查询都尽可能简单。例如,以下3种说法之间存在巨大差异:

var someData = context.SomeTable.Include(x => x.SomeOtherTable)
    .ToList()
    .Where(x => x.SomeCriteriaMethod())
    .ToList();

var someData = context.SomeTable.Include(x => x.SomeOtherTable)
    .Where(x => x.SomeField == someField && x.SomeOtherTable.SomeOtherField == someOtherField)
    .ToList();

var someData = context.SomeTable
    .Where(x => x.SomeField == someField && x.SomeOtherTable.SomeOtherField == someOtherField)
    .Select(x => new SomeViewModel 
    {
       SomeTableId = x.SomeTableId,
       SomeField = x.SomeField,
       SomeOtherField = x.SomeOtherTable.SomeOtherField
    }).ToList();

像上面第一个这样的例子效率极低,因为它们最终会在过滤行之前从数据库中加载相关表中的所有数据。尽管您的 Web 服务器可能只传回几行,但它已经从数据库请求了所有内容。当开发人员想要过滤 EF 无法转换为 SQL 的值(例如函数)时,这些类型的场景就会渗透到应用程序中,因此他们通过放置一个ToList调用,或者它可以作为不良分离的副产品引入,例如返回 IEnumerable 的存储库模式。

第二个示例稍微好一点,他们避免使用读取所有 ToList() 调用,但调用仍然会加载不需要的数据的整行。这会占用数据库和 Web 服务器上的资源。

第三个示例演示了改进查询以仅返回消费者所需的绝对最少数据。这样可以更好地利用数据库服务器上的索引和执行计划。

在负载下您可能面临的其他性能缺陷包括延迟加载等。数据库将执行有限数量的并发请求,因此如果发现某些查询正在启动额外的延迟加载请求,则在没有负载时,这些请求会立即执行。但在负载下,它们与其他查询和延迟加载请求一起排队,这可能会限制数据拉取。

最终,您应该针对数据库运行 SQL 分析器来捕获正在执行的 SQL 查询的类型和数量。在测试环境中执行时,请密切关注读取计数和 CPU 成本,而不是总执行时间。作为一般经验法则,较高的读取和 CPU 成本查询将更容易受到负载下执行时间井喷的影响。它们需要更多的资源来运行,并且“接触”更多的行,这意味着更多的等待行/表锁。

另一件需要注意的事情是非常大的数据系统中的“繁重”查询,这些系统需要接触大量行,例如报告,在某些情况下,还需要高度可定制的搜索查询。如果需要这些,您应该考虑规划您的数据库设计,以包含一个只读副本来运行报告或大型搜索表达式,以避免主数据库中的行锁定情况,从而降低典型读取和写入查询的响应能力。

编辑:识别延迟加载查询。

这些显示在分析器中,您可以在其中查询顶级表,但随后会看到对其后面的相关表的许多其他查询。

例如,假设您有一个名为“订单”的表,其中有一个名为“产品”的相关表、另一个名为“客户”的表以及另一个名为“地址”的送货地址表。要读取某个日期范围内的所有订单,您希望看到如下查询:

SELECT [OrderId], [Quantity], [OrderDate] [ProductId], [CustomerId], [DeliveryAddressId] FROM [dbo].[Orders] WHERE [OrderDate] >= '2019-01-01' AND [OrderDate] < '2020-01-01'

您只想加载订单并返回它们。

当序列化程序遍历字段时,它会找到引用的产品、客户和地址,并且通过尝试读取这些字段,将导致延迟加载:

SELECT [CustomerId], [Name] FROM [dbo].[Customers] WHERE [CustomerId] = 22
SELECT [ProductId], [Name], [Price] FROM [dbo].[Products] WHERE [ProductId] = 1023
SELECT [AddressId], [StreetNumber], [City], [State], [PostCode] FROM [dbo].[Addresses] WHERE [AddressId] = 1211

如果您的原始查询返回 100 个订单,您可能会看到上述一组查询的 100 倍,每个订单一组查询作为 1 个订单行上的延迟加载命中,将尝试按客户 ID 查找相关客户,按产品查找相关产品ID,以及按送货地址 ID 的相关地址。这可以而且确实会变得昂贵。在测试环境中运行时它可能不可见,但这会增加很多潜在的查询。

如果急切加载使用.Include()对于相关实体,EF 将组成JOIN语句一次性获取所有相关行,这比获取每个单独的相关实体要快得多。不过,这可能会导致提取大量不需要的数据。避免这种额外成本的最佳方法是利用预测Select只检索您需要的列。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

为什么当我的 Asp.Net Core API 每秒并发请求数增加时,响应时间也会增加 的相关文章

随机推荐