ASP.NET Core 应用程序的 CPU 峰值/等待时间

Rem*_*mus 8 .net c# azure azure-web-app-service asp.net-core

问题是 CPU 经常从 ~10% 飙升到超过 70%:

应用 CPU 百分比

不幸的是,这似乎对平均响应时间有影响,也导致了一些峰值。

平均响应时间

这是一个令人愉快的场景,其中平均值保持在 1 秒以下,但有时它的表现会很糟糕。

我试图从 Azure 门户调查这个问题,我注意到一些请求留在这个块中,让我认为这是一个查询问题(从我所看到的,它不完全是一个堆栈跟踪,可能有GetValidFunction()通过此处未显示的另一项服务在内部发生多个查询)。

等待块 1

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

等待块 2

对于另一个请求,Waiting 块发生在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);
}
Run Code Online (Sandbox Code Playgroud)

里面的查询GetValidFunction会执行这个逻辑

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

kri*_*shg 10

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

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

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

下面是这个问题的非常通用的答案,主要来自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);
}
Run Code Online (Sandbox Code Playgroud)

下面是一个等待外部服务响应的示例。该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();
    }
}
Run Code Online (Sandbox Code Playgroud)

您可以在此处找到这两个示例的完整代码。

如何解决问题

用异步操作替换同步 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);
}
Run Code Online (Sandbox Code Playgroud)

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();
    }
}
Run Code Online (Sandbox Code Playgroud)

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

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

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

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

// Invoke the asynchronous wrapper using a task
await LibraryIOOperationAsync();
Run Code Online (Sandbox Code Playgroud)

注意事项

  • 预计生命周期非常短且不太可能引起争用的 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 应用程序。

执行同步 I/O 操作的示例应用程序的性能图表

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

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

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

实施解决方案并验证结果

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

执行异步 I/O 操作的示例应用程序的性能图表

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