龙空技术网

ASP.NET Core性能最佳实践

老衲法号不行 476

前言:

如今看官们对“aspnet调用组件的好处”大概比较关注,同学们都需要分析一些“aspnet调用组件的好处”的相关文章。那么小编也在网络上收集了一些有关“aspnet调用组件的好处””的相关知识,希望我们能喜欢,大家快快来了解一下吧!

主动缓存

本文档的几个部分将讨论 Caching。 有关详细信息,请参阅 ASP.NET Core 中的响应缓存。

了解热代码路径

在本文档中,将 热代码路径 定义为经常调用的代码路径和执行时间量。 热代码路径通常限制应用向外缩放和性能,并将在本文档的几个部分中进行讨论。

避免阻塞调用

应将 ASP.NET Core 应用程序设计为同时处理许多请求。 异步 API 允许较小线程池处理数千个并发请求,无需等待阻塞调用。 线程可以处理另一个请求,而不是等待长时间运行的同步任务完成。

ASP.NET Core 应用中的常见性能问题是阻止可能是异步的调用。 许多同步阻塞调用都会导致线程池饥饿和响应时间降低。

禁止行为:

通过调用 Task.Wait 或 Task.Result 阻止异步执行。获取常见代码路径中的锁。 当构建为并行运行代码时,ASP.NET Core 应用程序的性能最高。调用 Task.Run 并立即等待它完成。 ASP.NET Core 已经在普通线程池线程上运行应用代码,因此调用 Task.Run 只会导致不必要的额外线程池计划。 即使计划的代码会阻止某个线程,Task.Run 也不会阻止该线程。

建议做法

使热代码路径 处于异步状态。如果异步 API 可用,则异步调用数据访问、i/o 和长时间运行的操作 Api。 不要 使用任务。运行以使同步 API 成为异步同步。使控制器/ Razor 页面操作异步。 为了获益于 async/await 模式,整个调用堆栈都是异步的。

探查器(例如 PerfView)可用于查找频繁添加到线程池中的线程。 Microsoft-Windows-DotNETRuntime/ThreadPoolWorkerThread/Start 事件指示添加到线程池的线程。

跨多个小型页面返回大型集合

网页不应同时加载大量数据。 返回对象集合时,请考虑它是否会导致性能问题。 确定设计是否会产生以下不良结果:

OutOfMemoryException 或高内存消耗线程池不足 (参阅) 上的以下备注 IAsyncEnumerable<T>响应时间缓慢频繁的垃圾回收

添加分页 以减轻上述方案。 使用页大小和页索引参数,开发人员应优先于返回部分结果。 当需要详尽的结果时,应使用分页来异步填充结果批,以避免锁定服务器资源。

有关分页和限制返回的记录数的详细信息,请参阅:

性能注意事项将分页添加到 ASP.NET Core 应用返回IEnumerable<T>或IAsyncEnumerable<T>

IEnumerable<T>从操作返回会导致序列化程序同步集合迭代。 因此会阻止调用,并且可能会导致线程池资源不足。 若要避免同步枚举,请 ToListAsync 在返回可枚举的前使用。

从 ASP.NET Core 3.0 开始, IAsyncEnumerable<T> 可将其用作 IEnumerable<T> 异步枚举的替代方法。 有关详细信息,请参阅 控制器操作返回类型。

最小化大型对象分配

.net Core 垃圾回收器在 ASP.NET Core 应用中自动管理内存的分配和释放。 自动垃圾回收通常意味着开发人员无需担心如何或何时释放内存。 但是,清理未引用的对象会占用 CPU 时间,因此开发人员应最大限度地减少 热代码路径中的对象分配。 垃圾回收在大型对象上特别昂贵 (> 85 K 字节) 。 大型对象存储在 大型对象堆 上,需要完整的 (第2代) 垃圾回收。 与第0代和第1代回收不同,第2代回收需要临时暂停应用执行。 频繁分配和取消分配大型对象会导致性能不一致。

建议:

请考虑缓存 经常使用的大型对象。 Caching 大型对象会阻止开销较高的分配。使用 ArrayPool <T>存储大型数组 来池缓冲区不要 在 热代码路径上分配很多生存期较短的大型对象。

可以通过查看 PerfView 中的垃圾回收 (GC) 统计信息并进行检查来诊断内存问题,如前面的问题:

垃圾回收暂停时间。垃圾回收所用的处理器时间百分比。第0代、第1代和第2代垃圾回收量。

有关详细信息,请参阅 垃圾回收和性能。

优化数据访问和 i/o

与数据存储和其他远程服务的交互通常是 ASP.NET Core 应用程序的最慢部分。 有效读取和写入数据对于良好的性能至关重要。

建议:

以异步方式调用所有数据访问 api。检索的数据 是必需的。 编写查询以仅返回当前 HTTP 请求所必需的数据。如果数据可以接受,请考虑缓存 经常访问的从数据库或远程服务检索的数据。 使用 MemoryCache 或 microsoft.web.distributedcache,具体取决于方案。 有关详细信息,请参阅 ASP.NET Core 中的响应缓存。尽量减少 网络往返次数。 目标是使用单个调用而不是多个调用来检索所需数据。当出于只读目的访问数据时, 在Entity Framework Core中使用 无跟踪查询。 EF Core可以更有效地返回无跟踪查询的结果。使用、或语句 (筛选和 聚合 LINQ 查询 .Where .Select .Sum ,例如) ,以便数据库执行筛选。请考虑 EF Core 在客户端上解析一些查询运算符,这可能导致查询执行效率低下。 有关详细信息,请参阅 客户端评估性能问题。不要 对集合使用投影查询,这可能会导致 SQL 查询执行 "N + 1"。 有关详细信息,请参阅 相关子查询的优化。

请参阅 EF 高性能 ,了解可提高大规模应用程序性能的方法:

DbContext 池显式编译的查询

建议在提交基本代码之前测量前面的高性能方法的影响。 已编译查询的额外复杂性可能不会提高性能。

通过查看Application Insights或分析工具访问数据所用的时间,可以检测到查询问题。 大多数数据库还提供有关频繁执行的查询的统计信息。

与 HttpClientFactory 建立池 HTTP 连接

尽管 HttpClient 实现了 IDisposable 接口,但它是为重复使用而设计的。 关闭 HttpClient 的实例使套接字在 TIME_WAIT 一小段时间内处于打开状态。 如果经常使用创建和处置对象的代码路径 HttpClient ,应用可能会耗尽可用的套接字。 HttpClientFactory是在 ASP.NET Core 2.1 中引入的,作为此问题的解决方案。 它处理池 HTTP 连接以优化性能和可靠性。

建议:

不要 直接创建 HttpClient 和释放实例。使用 HttpClientFactory 检索 HttpClient 实例。 有关详细信息,请参阅使用 HttpClientFactory 实现可复原的 HTTP 请求。快速保留常见代码路径

希望所有代码都快。 经常调用的代码路径是优化的最关键。 其中包括:

应用请求处理管道中的中间件组件,尤其是中间件在管道的早期运行。 这些组件对性能具有很大影响。针对每个请求执行的代码,或针对每个请求执行多次的代码。 例如,自定义日志记录、授权处理程序或暂时性服务的初始化。

建议:

不要将 自定义中间件组件用于长时间运行的任务。使用 性能分析工具(例如 Visual Studio 诊断工具 或 PerfView) )来识别 热代码路径。在 HTTP 请求之外完成长时间运行的任务

对应用 ASP.NET Core大多数请求都可以由控制器或页面模型处理,这些控制器或页面模型调用必要的服务并返回 HTTP 响应。 对于涉及长时间运行的任务的一些请求,最好使整个请求-响应过程异步。

建议:

普通 HTTP 请求处理过程中,不要等待长时间运行的任务完成。请考虑 使用后台服务处理长时间运行 的请求 ,或者使用 Azure 函数 处理进程 外的请求。 完成进程外的工作对于 CPU 密集型任务尤其有利。使用 实时通信选项(如 SignalR )以异步方式与客户端通信。缩小客户端资产

ASP.NET Core前端的应用经常提供许多 JavaScript、CSS 或图像文件。 初始加载请求的性能可以通过:

捆绑,它将多个文件合并为一个文件。缩小,通过删除空格和注释来减小文件的大小。

建议:

请使用捆绑和缩小准则 ,其中提及兼容的工具,并演示如何使用 ASP.NET Core 标记 environment 来处理 和 Development Production 环境。请考虑 使用其他第三方工具(如 Webpack)进行复杂的客户端资产管理。压缩响应

减小响应大小通常会显著提高应用的响应能力,通常可以显著提高响应能力。 减小有效负载大小的一种方式是压缩应用的响应。 有关详细信息,请参阅 响应压缩。

使用最新的 ASP.NET Core 版本

每个新版本的 ASP.NET Core包括性能改进。 .NET Core 和 ASP.NET Core意味着较新版本通常优于旧版本。 例如,.NET Core 2.1 添加了对已编译正则表达式的支持,并受益于 <T> Span。 ASP.NET Core 2.2 添加了对 HTTP/2 的支持。 ASP.NET Core 3.0 添加了许多改进,以减少内存使用量并提高吞吐量。 如果性能是优先级,请考虑升级到当前版本的 ASP.NET Core。

最小化异常

异常应很少出现。 相对于其他代码流模式,引发和捕获异常的速度较慢。 因此,不应使用异常来控制正常的程序流。

建议:

请勿 将引发或捕获异常用作正常程序流的一种方法,尤其是在热代码路径 中。 应用中包括逻辑,以检测和处理导致异常的条件。对于 异常或意外情况,请勿引发或捕获异常。

应用程序诊断工具(Insights)可帮助识别应用中可能影响性能的常见异常。

性能和可靠性

以下部分提供性能提示和已知可靠性问题和解决方案。

避免在 HttpRequest/HttpResponse 正文上同步读取或写入

所有 I/O ASP.NET Core异步。 服务器实现 Stream 接口,该接口具有同步重载和异步重载。 应首选异步线程以避免阻塞线程池线程。 阻塞线程可能会导致线程池资源不足。

请勿这样做: 下面的示例使用 ReadToEnd 。 它会阻止当前线程等待结果。 这是通过异步 同步的示例。

C#复制

public class BadStreamReaderController : Controller{    [HttpGet("/contoso")]    public ActionResult<ContosoData> Get()    {        var json = new StreamReader(Request.Body).ReadToEnd();        return JsonSerializer.Deserialize<ContosoData>(json);    }}

在以上代码中, Get 将整个 HTTP 请求正文同步读入内存。 如果客户端上传缓慢,则应用通过异步执行同步。 应用通过异步进行同步,因为 Kestrel 不支持 同步读取。

执行此操作: 下面的示例在 ReadToEndAsync 读取时使用 和 不阻止线程。

public class GoodStreamReaderController : Controller{    [HttpGet("/contoso")]    public async Task<ActionResult<ContosoData>> Get()    {        var json = await new StreamReader(Request.Body).ReadToEndAsync();        return JsonSerializer.Deserialize<ContosoData>(json);    }}

前面的代码以异步方式将整个 HTTP 请求正文读入内存。

警告

如果请求很大,将整个 HTTP 请求正文读入内存可能会导致 OOM (内存) 不足。 OOM 可能会导致拒绝服务。 有关详细信息,请参阅本文档 中的避免将大型请求主体或响应主体 读入内存。

执行此操作: 以下示例使用非缓冲请求正文完全异步:

public class GoodStreamReaderController : Controller{    [HttpGet("/contoso")]    public async Task<ActionResult<ContosoData>> Get()    {        return await JsonSerializer.DeserializeAsync<ContosoData>(Request.Body);    }}

前面的代码以异步方式将请求正文反序列化为 C# 对象。

首选 ReadFormAsync,而首选 Request.Form

请使用 HttpContext.Request.ReadFormAsync,而不是 HttpContext.Request.Form。 HttpContext.Request.Form 可以安全只读,但需要满足以下条件:

通过调用 和 读取了 ReadFormAsync 窗体正在使用 读取缓存的窗体值 HttpContext.Request.Form

请勿这样做: 下面的示例使用 HttpContext.Request.Form 。 HttpContext.Request.Form 通过 异步使用同步 ,可能会导致线程池资源不足。

public class BadReadController : Controller{    [HttpPost("/form-body")]    public IActionResult Post()    {        var form =  HttpContext.Request.Form;        Process(form["id"], form["name"]);        return Accepted();    }

执行此操作: 下面的示例使用 HttpContext.Request.ReadFormAsync 异步读取窗体正文。

public class GoodReadController : Controller{    [HttpPost("/form-body")]    public async Task<IActionResult> Post()    {       var form = await HttpContext.Request.ReadFormAsync();        Process(form["id"], form["name"]);        return Accepted();    }
避免将大型请求主体或响应主体读入内存

在 .NET 中,每个大于 85 KB 的对象分配最终都会在 LOH (大型) 。 大型对象以两种方式昂贵:

分配成本很高,因为必须清除新分配的大型对象的内存。 CLR 保证清除所有新分配对象的内存。LOH 与堆的其余部分一起收集。 LOH 需要完全 垃圾回收或 Gen2 收集。

此 博客文章 简洁地描述了问题:

分配大型对象时,它标记为第 2 代对象。 对于小型对象,不是第 0 代。 后果是,如果在 LOH 中内存不足,GC 将清理整个托管堆,而不仅是 LOH。 因此,它会清理第 0 代、第 1 代和第 2 代,包括 LOH。 这称为完全垃圾回收,是最耗时的垃圾回收。 对于许多应用程序,这可以接受。 但肯定不用于高性能 Web 服务器,因为处理从套接字读取、解压缩和解码 JSON (平均 Web 请求时,需要很少的大内存缓冲区,& JSON) 。

将大型请求或响应正文单独存储到单个 或 byte[] 中 string :

可能导致 LOH 中的空间快速不足。由于运行完整的 GCS,可能会导致应用出现性能问题。使用同步数据处理 API

使用仅支持同步读取和写入的序列化程序/反序列化程序 ( 例如 ,JSON.NET) :

将数据异步缓冲到内存中,然后再将数据传递到序列化程序/反序列化程序。

警告

如果请求很大,则可能会导致 OOM (内存) 不足。 OOM 可能会导致拒绝服务。 有关详细信息,请参阅本文档 中的避免将大型请求主体或响应主体 读入内存。

ASP.NET Core 3.0 System.Text.Json 默认使用 进行 JSON 序列化。 System.Text.Json:

以异步方式读取和写入 JSON。针对 UTF-8 文本进行了优化。通常比 Newtonsoft.Json 性能更高。请勿将 IHttpContextAccessor.HttpContext 存储在字段中

从 请求线程访问时,IHttpContextAccessor.HttpContext 返回 HttpContext 活动请求的 。 IHttpContextAccessor.HttpContext不应 存储在 字段或变量中。

请勿这样做: 以下示例将 存储在 HttpContext 字段中,然后尝试稍后使用它。

public class MyBadType{    private readonly HttpContext _context;    public MyBadType(IHttpContextAccessor accessor)    {        _context = accessor.HttpContext;    }    public void CheckAdmin()    {        if (!_context.User.IsInRole("admin"))        {            throw new UnauthorizedAccessException("The current user isn't an admin");        }    }}

前面的代码经常捕获构造函数中的 null HttpContext 或不正确的 。

执行此操作: 以下示例:

将 存储在 IHttpContextAccessor 字段中。在 HttpContext 正确的时间使用 字段并检查 null 。

public class MyGoodType{    private readonly IHttpContextAccessor _accessor;    public MyGoodType(IHttpContextAccessor accessor)    {        _accessor = accessor;    }    public void CheckAdmin()    {        var context = _accessor.HttpContext;        if (context != null && !context.User.IsInRole("admin"))        {            throw new UnauthorizedAccessException("The current user isn't an admin");        }    }}
不要从多个线程访问 HttpContext

HttpContext 不是 线程 安全的。 并行从多个线程访问可能会导致未定义的行为,例如挂起、崩溃 HttpContext 和数据损坏。

请勿这样做: 以下示例执行三个并行请求,并记录传出 HTTP 请求之前和之后传入的请求路径。 请求路径从多个线程访问,可能并行访问。

public class AsyncBadSearchController : Controller{           [HttpGet("/search")]    public async Task<SearchResults> Get(string query)    {        var query1 = SearchAsync(SearchEngine.Google, query);        var query2 = SearchAsync(SearchEngine.Bing, query);        var query3 = SearchAsync(SearchEngine.DuckDuckGo, query);        await Task.WhenAll(query1, query2, query3);        var results1 = await query1;        var results2 = await query2;        var results3 = await query3;        return SearchResults.Combine(results1, results2, results3);    }           private async Task<SearchResults> SearchAsync(SearchEngine engine, string query)    {        var searchResults = _searchService.Empty();        try        {            _logger.LogInformation("Starting search query from {path}.",                                     HttpContext.Request.Path);            searchResults = _searchService.Search(engine, query);            _logger.LogInformation("Finishing search query from {path}.",                                     HttpContext.Request.Path);        }        catch (Exception ex)        {            _logger.LogError(ex, "Failed query from {path}",                              HttpContext.Request.Path);        }        return await searchResults;    }

执行此操作: 以下示例在提出三个并行请求之前,从传入请求复制所有数据。

public class AsyncGoodSearchController : Controller{           [HttpGet("/search")]    public async Task<SearchResults> Get(string query)    {        string path = HttpContext.Request.Path;        var query1 = SearchAsync(SearchEngine.Google, query,                                 path);        var query2 = SearchAsync(SearchEngine.Bing, query, path);        var query3 = SearchAsync(SearchEngine.DuckDuckGo, query, path);        await Task.WhenAll(query1, query2, query3);        var results1 = await query1;        var results2 = await query2;        var results3 = await query3;        return SearchResults.Combine(results1, results2, results3);    }    private async Task<SearchResults> SearchAsync(SearchEngine engine, string query,                                                  string path)    {        var searchResults = _searchService.Empty();        try        {            _logger.LogInformation("Starting search query from {path}.",                                   path);            searchResults = await _searchService.SearchAsync(engine, query);            _logger.LogInformation("Finishing search query from {path}.", path);        }        catch (Exception ex)        {            _logger.LogError(ex, "Failed query from {path}", path);        }        return await searchResults;    }
请求完成后,请勿使用 HttpContext

HttpContext仅在管道中存在活动的 HTTP 请求时 ASP.NET Core有效。 整个 ASP.NET Core管道是执行每个请求的异步委托链。 从此 Task 链返回的 完成后,将回收 HttpContext 。

请勿这样做: 以下示例使用 async void ,在到达第一个 HTTP 请求时 await ,它使 HTTP 请求完成:

对于 应用, 这始终是 ASP.NET Core做法。在 HTTP HttpResponse 请求完成后访问 。使进程崩溃。

public class AsyncBadVoidController : Controller{    [HttpGet("/async")]    public async void Get()    {        await Task.Delay(1000);        // The following line will crash the process because of writing after the         // response has completed on a background thread. Notice async void Get()        await Response.WriteAsync("Hello World");    }}

执行此操作: 以下示例将 返回到 Task 框架,因此在操作完成之前,HTTP 请求不会完成。

public class AsyncGoodTaskController : Controller{    [HttpGet("/async")]    public async Task Get()    {        await Task.Delay(1000);        await Response.WriteAsync("Hello World");    }}
请勿在后台线程中捕获 HttpContext

请勿这样做: 下面的示例演示了从 属性捕获 HttpContext 的闭 Controller 包。 这是一种错误的做法,因为工作项可以:

在请求范围之外运行 。尝试读取错误的 HttpContext 。

[HttpGet("/fire-and-forget-1")]public IActionResult BadFireAndForget(){    _ = Task.Run(async () =>    {        await Task.Delay(1000);        var path = HttpContext.Request.Path;        Log(path);    });    return Accepted();}

执行此操作: 以下示例:

复制请求期间后台任务中所需的数据。不从控制器引用任何内容。

[HttpGet("/fire-and-forget-3")]public IActionResult GoodFireAndForget(){    string path = HttpContext.Request.Path;    _ = Task.Run(async () =>    {        await Task.Delay(1000);        Log(path);    });    return Accepted();}

后台任务应作为托管服务实现。 有关详细信息,请参阅使用托管服务的后台任务。

不要捕获注入到后台线程上的控制器中的服务

请勿这样做: 下面的示例演示一个闭包正在从 DbContext action 参数 Controller 捕获 。 这是一种错误的做法。 工作项可以在请求范围之外运行。 ContosoDbContext的范围为请求,导致 ObjectDisposedException 。

[HttpGet("/fire-and-forget-1")]public IActionResult FireAndForget1([FromServices]ContosoDbContext context){    _ = Task.Run(async () =>    {        await Task.Delay(1000);        context.Contoso.Add(new Contoso());        await context.SaveChangesAsync();    });    return Accepted();}

执行此操作: 以下示例:

注入 以 IServiceScopeFactory 在后台工作项中创建作用域。 IServiceScopeFactory 是单一的。在后台线程中创建新的依赖关系注入范围。不从控制器引用任何内容。不会从传入 ContosoDbContext 请求捕获 。

[HttpGet("/fire-and-forget-3")]public IActionResult FireAndForget3([FromServices]IServiceScopeFactory                                     serviceScopeFactory){    _ = Task.Run(async () =>    {        await Task.Delay(1000);        using (var scope = serviceScopeFactory.CreateScope())        {            var context = scope.ServiceProvider.GetRequiredService<ContosoDbContext>();            context.Contoso.Add(new Contoso());            await context.SaveChangesAsync();                                                }    });    return Accepted();}

以下突出显示的代码:

创建后台操作生存期的范围,并解析其中的服务。从 ContosoDbContext 正确的范围使用 。

[HttpGet("/fire-and-forget-3")]public IActionResult FireAndForget3([FromServices]IServiceScopeFactory                                     serviceScopeFactory){    _ = Task.Run(async () =>    {        await Task.Delay(1000);        using (var scope = serviceScopeFactory.CreateScope())        {            var context = scope.ServiceProvider.GetRequiredService<ContosoDbContext>();            context.Contoso.Add(new Contoso());            await context.SaveChangesAsync();                                                }    });    return Accepted();}
在响应正文启动后,请勿修改状态代码或标头

ASP.NET Core不会缓冲 HTTP 响应正文。 首次写入响应时:

标头与正文的区块一起发送到客户端。无法再更改响应标头。

请勿这样做: 以下代码尝试在响应启动后添加响应标头:

app.Use(async (context, next) =>{    await next();    context.Response.Headers["test"] = "test value";});

在以上代码中,如果 已写入 context.Response.Headers["test"] = "test value"; 响应, next() 则 将引发异常。

执行此操作: 以下示例在修改标头之前检查 HTTP 响应是否已启动。

app.Use(async (context, next) =>{    await next();    if (!context.Response.HasStarted)    {        context.Response.Headers["test"] = "test value";    }});

执行此操作: 以下示例使用 HttpResponse.OnStarting 在将响应标头刷新到客户端之前设置标头。

如果检查响应是否尚未启动,则允许在写入响应标头之前注册将调用的回调。 检查响应是否尚未启动:

提供实时追加或重写标头的能力。不需要了解管道中的下一个中间件。

app.Use(async (context, next) =>{    context.Response.OnStarting(() =>    {        context.Response.Headers["someheader"] = "somevalue";        return Task.CompletedTask;    });    await next();});
如果已开始写入 () ,请不要调用下一个

只有在组件可以处理和操作响应时,才需要调用组件。

将进程内托管与 IIS 一同使用

使用进程内托管,ASP.NET Core 在与其 IIS 工作进程相同的进程中运行。 进程内托管比进程外托管提供改进的性能,因为请求不会通过环回适配器进行代理。 环回适配器是一个网络接口,用于将传出网络流量返回给同一台计算机。 IIS 使用 Windows 进程激活服务 (WAS) 处理进程管理。

项目默认为 ASP.NET Core 3.0 及更高版本中的进程内托管模型。

标签: #aspnet调用组件的好处