在线时间:8:00-16:00
迪恩网络APP
随时随地掌握行业动态
扫描二维码
关注迪恩网络微信公众号
本文提供了 ASP.NET Core 的性能最佳实践指南。
积极利用缓存这里有一篇文档在多个部分中讨论了如何积极利用缓存。 有关详细信息,请参阅︰ https://docs.microsoft.com/en-us/aspnet/core/performance/caching/response?view=aspnetcore-3.1. 了解代码中的热点路径在本文档中, 代码热点路径 定义为频繁调用的代码路径以及执行时间的大部分时间。 代码热点路径通常限制应用程序的扩展和性能,并在本文档的多个部分中进行讨论。 避免阻塞式调用ASP.NET Core 应用程序应设计为同时处理许多请求。 异步 API 可以使用一个小池线程通过非阻塞式调用来处理数以千计的并发请求。 线程可以处理另一个请求,而不是等待长时间运行的同步任务完成。 ASP.NET Core 应用程序中的常见性能问题通常是由于那些本可以异步调用但却采用阻塞时调用而导致的。 同步阻塞会调用导致 线程池饥饿 和响应时间降级。 不要:
要:
使用 IEumerable<T> 或 IAsyncEnumerable<T> 作为返回值在 Action 中返回 从 ASP.NET Core 3.0 开始, 尽可能少的使用大对象.NET Core 垃圾收集器 在 ASP.NET Core 应用程序中起到自动管理内存的分配和释放的作用。 自动垃圾回收通常意味着开发者不需要担心如何或何时释放内存。 但是,清除未引用的对象将会占用 CPU 时间,因此开发者应最小化 代码热点路径 中的分配的对象。 垃圾回收在大对象上代价特大 (> 85 K 字节) 。 大对象存储在 large object heap 上,需要 full (generation 2) garbage collection 来清理。 与 generation 0 和 generation 1 不同,generation 2 需要临时暂挂应用程序。 故而频繁分配和取消分配大型对象会导致性能耗损。 建议 :
可以通过查看 PerfView 中的垃圾回收 (GC) 统计信息来诊断并检查内存问题,其中包括:
有关更多信息,请参阅 垃圾回收和性能。 优化数据操作和 I/O与数据存储器和其他远程服务的交互通常是 ASP.NET Core 应用程序最慢的部分。 高效读取和写入数据对于良好的性能至关重要。 建议 :
请参阅 EF 高性能专题 以了解可能提高应用性能的方法: 在代码提交之前,我们建议评估上述高性能方法的影响。 编译查询的额外复杂性可能无法一定确保性能提高。 可以通过使用 Application Insights 或使用分析工具查看访问数据所花费的时间来检测查询问题。 大多数数据库还提供有关频繁执行的查询的统计信息,这也可以作为重要参考。 通过 HttpClientFactory 建立 HTTP 连接池虽然 HttpClient 实现了 建议 :
确保公共代码路径快若鹰隼如果你想要所有的代码都保持高速, 高频调用的代码路径就是优化的最关键路径。 优化措施包括:
建议 :
在 HTTP 请求之外运行长时任务对 ASP.NET Core 应用程序的大多数请求可以由调用服务的 controller 或页面模型处理,并返回 HTTP 响应。 对于涉及长时间运行的任务的某些请求,最好使整个请求 - 响应进程异步。 建议 :
缩小客户端资源复杂的 ASP.NET Core 应用程序经常包含很有前端文件例如 JavaScript, CSS 或图片文件。 可以通过以下方法优化初始请求的性能:
建议 : 压缩 Http 响应减少响应的大小通常会显着提高应用程序的响应性。 而减小内容大小的一种方法是压缩应用程序的响应。 有关更多信息,请参阅 响应压缩。 使用最新的 ASP.NET Core 发行版ASP.NET Core 的每个新发行版都包含性能改进。 .NET Core 和 ASP.NET Core 中的优化意味着较新的版本通常优于较旧版本。 例如, .NET Core 2.1 添加了对预编译的正则表达式的支持,并从使用 Span<T> 改进性能。 ASP.NET Core 2.2 添加了对 HTTP/2 的支持。 ASP.NET Core 3.0 增加了许多改进 ,以减少内存使用量并提高吞吐量。 如果性能是优先考虑的事情,那么请升级到 ASP.NET Core 的当前版本。 最小化异常异常应该竟可能少。 相对于正常代码流程来说,抛出和捕获异常是缓慢的。 因此,不应使用异常来控制正常程序流。 建议 :
应用程序诊断工具 (如 Application Insights) 可以帮助识别应用程序中可能影响性能的常见异常。 性能和可靠性下文将提供常见性能提示和已知可靠性问题的解决方案。 避免在 HttpRequest/HttpResponse body 上同步读取或写入ASP.NET Core 中的所有 I/O 都是异步的。 服务器实现了 不要使用如下操作: https://docs.microsoft.com/en-us/dotnet/api/System.IO.StreamReader.ReadToEnd。 它会阻止当前线程等待结果。 这是 sync over async 的示例。 public class BadStreamReaderController : Controller { [HttpGet("/contoso")] public ActionResult<ContosoData> Get() { var json = new StreamReader(Request.Body).ReadToEnd(); return JsonSerializer.Deserialize<ContosoData>(json); } }
在上述代码中, 应该采用如下操作: https://docs.microsoft.com/en-us/dotnet/api/System.IO.StreamReader.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 request body 读取到内存中。
应该采用如下操作: 使用不缓冲的方式完成 request body 操作: public class GoodStreamReaderController : Controller { [HttpGet("/contoso")] public async Task<ActionResult<ContosoData>> Get() { return await JsonSerializer.DeserializeAsync<ContosoData>(Request.Body); } }
上述代码采用异步方式将 request body 序列化为 C# 对象。 优先选用 Request.Form 的 ReadFormAsync应该使用
不要使用如下操作: 例如以下方式使用 public class BadReadController : Controller { [HttpPost("/form-body")] public IActionResult Post() { var form = HttpContext.Request.Form; Process(form["id"], form["name"]); return Accepted(); }
应该使用如下操作: 使用 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(); }
避免将大型 request body 或 response body 读取到内存中在 .NET 中,大于 85 KB 的对象会被分配在大对象堆 (LOH )。 大型对象的开销较大,包含两方面:
此 博文 很好描述了该问题:
天真地将一个大型 request 或者 response body 存储到单个
使用同步 API 处理数据例如使用仅支持同步读取和写入的序列化器 / 反序列化器时 ( 例如, JSON.NET):
ASP.NET Core 3.0 默认情况下使用 https://docs.microsoft.com/en-us/dotnet/api/system.text.json 进行 JSON 序列化,这将带来如下好处。 https://docs.microsoft.com/en-us/dotnet/api/system.text.json:
不要将 IHttpContextAccessor.HttpContext 存储在字段中IHttpContextAccessor.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 或不正确的 应该采用如下操作:
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
不要使用如下操作: 以下示例将发出三个并行请求,并在 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
不要进行如下操作: 以下示例使用
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"); } }
应该进行如下操作: 以下示例将 public class AsyncGoodTaskController : Controller { [HttpGet("/async")] public async Task Get() { await Task.Delay(1000); await Response.WriteAsync("Hello World"); } }
不要在后台线程中使用 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(); }
后台任务最好采用托管服务进行操作。 有关更多信息,请参阅 采用托管服务运行后台任务 。 不要在后台线程获取注入到 controller 中的服务不要采用如下做法: 以下示例使用闭包从 [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(); }
应该采用如下操作:
[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(); }
以下高亮的的代码说明:
[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(); }
不要在响应正文已经开始发送时尝试修改 status code 或者 headerASP.NET Core 不会缓冲 HTTP 响应正文。 当正文一旦开始发送:
不要使用如下操作: 以下代码尝试在响应启动后添加响应头: app.Use(async (context, next) => { await next(); context.Response.Headers["test"] = "test value"; });
在上述的代码中,如果 应该采用如下操作: 以下示例检查 HTTP 响应在修改 Header 之前是否已启动。 app.Use(async (context, next) => { await next(); if (!context.Response.HasStarted) { context.Response.Headers["test"] = "test value"; } });
应该采用如下操作: 以下示例使用 通过这种方式,响应头将在响应开始时调用已注册的回调进行一次性写入。 如此这般便可以:
app.Use(async (context, next) => { context.Response.OnStarting(() => { context.Response.Headers["someheader"] = "somevalue"; return Task.CompletedTask; }); await next(); });
如果已开始写入响应主体,则请不要调用 next ()仅当后续组件能够处理响应或时才调用它们,因此如果当前已经开始写入响应主体,后续操作就已经不再需要,并有可能引发异常情况。 托管于 IIS 应该使用 In-process 模式使用 in-process 模式托管, ASP.NET Core 应用程序将与 IIS 工作进程在同一进程中运行。 In-process 模式拥有比 out-of-process 更加优秀的性能表现,因为这样不需要将请求通过回环网络适配器进行代理中转。 回环网络适配器是将本机发送的网络流量重新转回本机的的网络适配器。 IIS 进程管理由 Windows Process Activation Service (WAS) 来完成。 在 ASP.NET Core 3.0 和更高版本中的默认将采用 in-process 模式进行托管。 有关更多信息,请参阅 在 Windows 上使用 IIS 托管 ASP.NET Core Newbe.Translations您所阅读的当前文章源自于 Newbe.Translations 项目参与的翻译贡献,您可以通过右侧链接一同参与该项目:https://www.newbe.pro/Newbe.Translations/Newbe.Translations/。 翻译内容具有一定的时效性,不会随着原文内容实时更新,如果内容存在一定过时,您也可以联系我们。
|
请发表评论