【问题标题】:CPU Spikes / Wait time for ASP.NET Core applicationASP.NET Core 应用程序的 CPU 峰值/等待时间
【发布时间】:2021-03-25 05:24:55
【问题描述】:

问题是 CPU 经常从 ~10% 飙升到 70% 以上:

不幸的是,这似乎对平均响应时间产生了影响,导致那里也出现了一些峰值。

这是一个令人愉快的场景,平均保持在 1 秒以下,但有时它的表现可能很差。

我曾尝试从 Azure 门户调查此问题,但我注意到一些请求保留在此块中,让我认为这是一个查询问题(从我所见,这并不完全是堆栈跟踪, GetValidFunction() 内可能有多个查询通过此处未显示的另一服务进行)。

如果是这种情况,我在内部重写查询没有问题,因为它们是通过 LINQ 和 EF 完成的,但后来我注意到了一些奇怪的事情。请注意,在此请求中,正在等待 Framework/Library CEEJitInfo::allocMem

对于另一个请求,正在为 REDIS 查询发生等待块。但大多数情况下,呼叫似乎被阻止在GetResults() 内,就像第三张图片一样。所有这些等待时间是否仅与数据库查询有关? (DTU 也有尖峰,但这是我必须解决的另一个问题 - 可能是由于设计不佳,很多表的 GUID 为 PK / FK - 索引重建可能?但这将在下次解决)

为这个应用程序提供一些上下文:

  • 在 .NET 5 上运行的 Web API
  • 允许用户创建自己的剃须刀模板
  • 模板存储在 SQL Server 数据库中
  • 查询模板,然后在运行时编译和渲染

我想到的另一个可能的原因是大量编译的剃须刀模板。这些视图可能有数百个,甚至上千个。我正在考虑框架内部正在执行的视图缓存失效问题,从而强制重新编译视图?

这可能与最初的问题有点偏离主题,但有人知道 razor 运行时编译在 ASP.NET Core 中是如何工作的吗?

具体来说:

  • 这些视图在缓存中保留了多长时间?
  • 是像在 .NET Framework 中那样为每个视图创建一个 DLL,还是只保存在内存中?

我试图寻找这两个问题的答案,但找不到任何答案。

总而言之,如果您对 CPU 峰值/等待时间问题有一些建议,我将不胜感激。您是否知道任何可能导致查询本身等待时间的原因?会不会和视图重编译/垃圾收集器有关?

感谢您的宝贵时间。


后期编辑: 执行的代码类似这样

Controller-> GET ExecuteFunction(functionCode) -> ValidateFunction(functionCode) -> GetValidFunction(functionCode)

ValidateFunction 也在执行其他查询,但在GetValidFunction 之后。

private (string, Functions) GetValidFunction(Guid functionCode)
{
    var cacheKey = CacheKeys.FunctionError(functionCode);
    var cacheTimeSpan = new TimeSpan(0, cacheValidationMinutes, 0);
    var validationErrorMessage = cacheProvider.GetWithSlidingExpiration<string>(cacheKey, cacheTimeSpan);
    var function = functionLogic.GetValidFunctionByCode(functionCode);
    if (function == null)
    {
        cacheProvider.AddToCacheInvariantCase(cacheKey, invalidErrorCode, cacheTimeSpan);
        return (invalidErrorCode, null);
    }
    if (string.isNullOrEmpty(validationErrorMessage)) return (validationErrorMessage, function);
    var functionCodeData = functionCodeLogic.GetFunctionCode(functionCode);
    if (functionCodeData == null)
    {
        cacheProvider.AddToCacheInvariantCase(cacheKey, invalidErrorCode, cacheTimeSpan);
        return (invalidErrorCode, null);
    }
    if (function.StatusId == (int)FunctionStatusName.Active || function.StatusId == (int)FunctionStatusName.Draft)
    {
        cacheProvider.AddToCacheInvariantCase(cacheKey, NoErrorFunction, cacheTimeSpan);
    }

    return (null, function);
}

GetValidFunction 内部的查询会执行这个逻辑

   public T Get(Expression<Func<T, bool>> where)
    {
        return dbset.Where(where).FirstOrDefault();
    }

【问题讨论】:

  • 你看请求数了吗?你已经在使用async await-pattern了吗?
  • 听起来涉及到很多活动部件。您能否简化或排除解决方案中的某些方面,看看是否遇到同样的问题?
  • 在我看来,您正在某个地方进行同步(阻塞)I/O 调用,这会导致线程争用。除非您共享相关的代码,否则有人无法进行故障排除!特别是我有兴趣看到 I/O (DB) 调用以及调用者直到顶部。
  • @Remus - 我认为请求的是 IsValidFunction 中的代码以及如何从控制器调用它。没有代码很难调试问题。
  • 你为什么使用 GetResults() ?,你能一直做异步调用而不是使用 GetResults 吗?

标签: c# .net azure asp.net-core azure-web-app-service


【解决方案1】:

虽然你没有分享相关的代码,但从描述和症状来看,这似乎是你的代码中某处进行的同步(阻塞)I/O导致线程争用的结果。

更新: 在您的共享代码中,我在 GetValidFunctionGet 方法中看到了同步 I/O 调用。应该如下所示,调用者应该等待。 记住,async all the way

public Task<T> GetAsync(Expression<Func<T, bool>> where)
    {
        return dbset.Where(where).FirstOrDefaultAsync();
    }

下面是这个问题的非常通用的答案,主要来自Synchronous I/O antipattern下面一些旧的 asp.net 应用程序和旧的云服务的参考在今天可能已经过时,但这个概念仍然是相关的

同步 I/O 反模式

在 I/O 完成时阻塞调用线程会降低性能并影响垂直可扩展性。

问题描述

同步 I/O 操作在 I/O 完成时阻塞调用线程。调用线程进入等待状态,在此时间间隔内无法执行有用的工作,浪费处理资源。

I/O 的常见示例包括:

  • 检索数据或将数据持久保存到数据库或任何类型的持久存储中。
  • 向 Web 服务发送请求。
  • 发布消息或从队列中检索消息。
  • 写入或读取本地文件。

这种反模式的出现通常是因为:

  • 这似乎是最直观的操作方式。
  • 应用程序需要来自请求的响应。
  • 应用程序使用的库只为 I/O 提供同步方法。
  • 外部库在内部执行同步 I/O 操作。单个同步 I/O 调用可能会阻塞整个调用链。

以下代码将文件上传到 Azure Blob 存储。等待同步 I/O 的代码块有两个地方,CreateIfNotExists 方法和UploadFromStream 方法。

var blobClient = storageAccount.CreateCloudBlobClient();
var container = blobClient.GetContainerReference("uploadedfiles");

container.CreateIfNotExists();
var blockBlob = container.GetBlockBlobReference("myblob");

// Create or overwrite the "myblob" blob with contents from a local file.
using (var fileStream = File.OpenRead(HostingEnvironment.MapPath("~/FileToUpload.txt")))
{
    blockBlob.UploadFromStream(fileStream);
}

这是一个等待外部服务响应的示例。 GetUserProfile 方法调用返回UserProfile 的远程服务。

public interface IUserProfileService
{
    UserProfile GetUserProfile();
}

public class SyncController : ApiController
{
    private readonly IUserProfileService _userProfileService;

    public SyncController()
    {
        _userProfileService = new FakeUserProfileService();
    }

    // This is a synchronous method that calls the synchronous GetUserProfile method.
    public UserProfile GetUserProfile()
    {
        return _userProfileService.GetUserProfile();
    }
}

您可以找到这两个示例的完整代码here

如何解决问题

将同步 I/O 操作替换为异步操作。这释放了当前线程以继续执行有意义的工作而不是阻塞,并有助于提高计算资源的利用率。异步执行 I/O 对于处理来自客户端应用程序的意外请求激增特别有效。

许多库都提供同步和异步版本的方法。尽可能使用异步版本。这是上一个将文件上传到 Azure Blob 存储的示例的异步版本。

var blobClient = storageAccount.CreateCloudBlobClient();
var container = blobClient.GetContainerReference("uploadedfiles");

await container.CreateIfNotExistsAsync();

var blockBlob = container.GetBlockBlobReference("myblob");

// Create or overwrite the "myblob" blob with contents from a local file.
using (var fileStream = File.OpenRead(HostingEnvironment.MapPath("~/FileToUpload.txt")))
{
    await blockBlob.UploadFromStreamAsync(fileStream);
}

await 运算符在执行异步操作时将控制权返回给调用环境。此语句之后的代码充当异步操作完成时运行的延续。

设计良好的服务还应该提供异步操作。这是返回用户配置文件的 Web 服务的异步版本。 GetUserProfileAsync 方法依赖于用户配置文件服务的异步版本。

public interface IUserProfileService
{
    Task<UserProfile> GetUserProfileAsync();
}

public class AsyncController : ApiController
{
    private readonly IUserProfileService _userProfileService;

    public AsyncController()
    {
        _userProfileService = new FakeUserProfileService();
    }

    // This is an synchronous method that calls the Task based GetUserProfileAsync method.
    public Task<UserProfile> GetUserProfileAsync()
    {
        return _userProfileService.GetUserProfileAsync();
    }
}

对于不提供异步操作版本的库,可以围绕选定的同步方法创建异步包装器。请谨慎遵循此方法。虽然它可能会提高调用异步包装器的线程的响应能力,但它实际上会消耗更多资源。可能会创建一个额外的线程,并且存在与同步该线程完成的工作相关的开销。这篇博文中讨论了一些权衡:Should I expose asynchronous wrappers for synchronous methods?

这是一个围绕同步方法的异步包装器示例。

// Asynchronous wrapper around synchronous library method
private async Task<int> LibraryIOOperationAsync()
{
    return await Task.Run(() => LibraryIOOperation());
}

现在调用代码可以在包装器上等待:

// Invoke the asynchronous wrapper using a task
await LibraryIOOperationAsync();

注意事项

  • 预期寿命很短且不太可能引起争用的 I/O 操作可能比同步操作的性能更高。一个示例可能是读取 SSD 驱动器上的小文件。将任务分派到另一个线程并在任务完成时与该线程同步的开销可能超过异步 I/O 的好处。不过,这些情况比较少见,大部分 I/O 操作都应该异步完成。

  • 提高 I/O 性能可能会导致系统的其他部分成为瓶颈。例如,解除阻塞线程可能会导致对共享资源的并发请求量增加,进而导致资源匮乏或限制。如果这成为问题,您可能需要扩展 Web 服务器或分区数据存储的数量以减少争用。

如何发现问题

对于用户来说,应用程序可能会周期性地无响应。应用程序可能会因超时异常而失败。这些故障还可能返回 HTTP 500(内部服务器)错误。在服务器上,传入的客户端请求可能会被阻塞,直到线程可用,从而导致请求队列长度过长,表现为 HTTP 503(服务不可用)错误。

您可以执行以下步骤来帮助识别问题:

  1. 监控生产系统并确定阻塞的工作线程是否限制了吞吐量。

  2. 如果请求因缺少线程而被阻塞,请查看应用程序以确定哪些操作可能正在同步执行 I/O。

  3. 对执行同步 I/O 的每个操作执行受控负载测试,以确定这些操作是否影响系统性能。

诊断示例

以下部分将这些步骤应用于前面描述的示例应用程序。

监控网络服务器性能

对于 Azure Web 应用程序和 Web 角色,值得监控 IIS Web 服务器的性能。特别要注意请求队列长度,以确定在高活动期间请求是否被阻塞以等待可用线程。你可以通过启用 Azure 诊断来收集此信息。有关详细信息,请参阅:

检测应用程序以查看请求被接受后如何处理。跟踪请求流有助于识别它是否正在执行运行缓慢的调用并阻塞当前线程。线程分析还可以突出显示被阻止的请求。

负载测试应用程序

下图显示了前面所示的同步 GetUserProfile 方法在多达 4000 个并发用户的不同负载下的性能。该应用程序是在 Azure 云服务 Web 角色中运行的 ASP.NET 应用程序。

同步操作被硬编码为休眠 2 秒,以模拟同步 I/O,因此最短响应时间略高于 2 秒。当负载达到大约 2500 个并发用户时,平均响应时间达到一个平台,尽管每秒的请求量继续增加。请注意,这两个度量的比例是对数的。从此时到测试结束,每秒的请求数翻倍。

单独来看,从这个测试中不一定清楚同步 I/O 是否有问题。在较重的负载下,应用程序可能会达到一个临界点,即 Web 服务器无法再及时处理请求,从而导致客户端应用程序接收超时异常。

传入的请求由 IIS Web 服务器排队并交给在 ASP.NET 线程池中运行的线程。因为每个操作都同步执行 I/O,所以线程被阻塞直到操作完成。随着工作负载的增加,最终线程池中的所有 ASP.NET 线程都被分配和阻塞。此时,任何进一步的传入请求都必须在队列中等待可用线程。随着队列长度的增加,请求开始超时。

实施方案并验证结果

下图显示了对代码的异步版本进行负载测试的结果。

吞吐量要高得多。在与之前的测试相同的持续时间内,系统成功地处理了近十倍的吞吐量增长,以每秒请求数来衡量。此外,平均响应时间相对恒定,比之前的测试小约 25 倍。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-01-21
    • 1970-01-01
    • 2014-03-21
    • 2015-09-27
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多