目录
积极利用缓存
了解代码中的热点路径
避免阻塞式调用
使用 IEumerable 或 IAsyncEnumerable 作为返回值
尽可能少的使用大对象
优化数据操作和 I/O
通过 HttpClientFactory 建立 HTTP 连接池
确保公共代码路径快若鹰隼
在 HTTP 请求之外运行长时任务
缩小客户端资源
压缩 Http 响应
使用最新的 ASP.NET Core 发行版
最小化异常
性能和可靠性
避免在 HttpRequest/HttpResponse body 上同步读取或写入
优先选用 Request.Form 的 ReadFormAsync
避免将大型 request body 或 response body 读取到内存中
使用同步 API 处理数据
不要将 IHttpContextAccessor.HttpContext 存储在字段中
不要尝试在多线程下使用 HttpContext
请求处理完成后不要使用 HttpContext
不要在后台线程中使用 HttpContext
不要在后台线程获取注入到 controller 中的服务
不要在响应正文已经开始发送时尝试修改 status code 或者 header
如果已开始写入响应主体,则请不要调用 next ()
托管于 IIS 应该使用 In-process 模式
Newbe.Translations
译文原文地址:https://docs.microsoft.com/en-us/aspnet/core/performance/performance-best-practices?view=aspnetcore-3.1
这里有一篇文档在多个部分中讨论了如何积极利用缓存。 有关详细信息,请参阅︰ https://docs.microsoft.com/en-us/aspnet/core/performance/caching/response?view=aspnetcore-3.1.
在本文档中, 代码热点路径 定义为频繁调用的代码路径以及执行时间的大部分时间。 代码热点路径通常限制应用程序的扩展和性能,并在本文档的多个部分中进行讨论。
ASP.NET Core 应用程序应设计为同时处理许多请求。 异步 API 可以使用一个小池线程通过非阻塞式调用来处理数以千计的并发请求。 线程可以处理另一个请求,而不是等待长时间运行的同步任务完成。
ASP.NET Core 应用程序中的常见性能问题通常是由于那些本可以异步调用但却采用阻塞时调用而导致的。 同步阻塞会调用导致 线程池饥饿 和响应时间降级。
不要:
要:
Microsoft-Windows-DotNETRuntime/ThreadPoolWorkerThread/Start
事件表示新线程被添加到线程池。在 Action 中返回 IEumerable
将会被序列化器中进行同步迭代 。 结果是可能导致阻塞或者线程池饥饿。 想要要避免同步迭代集合,可以在返回迭代集合之前使用 ToListAsync
使其异步化。
从 ASP.NET Core 3.0 开始, IAsyncEnumerable
可以用作为 IEumerable
的替代方法,以异步方式进行迭代。 有关更多信息,请参阅 Controller Action 的返回值类型。
.NET Core 垃圾收集器 在 ASP.NET Core 应用程序中起到自动管理内存的分配和释放的作用。 自动垃圾回收通常意味着开发者不需要担心如何或何时释放内存。 但是,清除未引用的对象将会占用 CPU 时间,因此开发者应最小化 代码热点路径 中的分配的对象。 垃圾回收在大对象上代价特大 (> 85 K 字节) 。 大对象存储在 large object heap 上,需要 full (generation 2) garbage collection 来清理。 与 generation 0 和 generation 1 不同,generation 2 需要临时暂挂应用程序。 故而频繁分配和取消分配大型对象会导致性能耗损。
建议 :
可以通过查看 PerfView 中的垃圾回收 (GC) 统计信息来诊断并检查内存问题,其中包括:
有关更多信息,请参阅 垃圾回收和性能。
与数据存储器和其他远程服务的交互通常是 ASP.NET Core 应用程序最慢的部分。 高效读取和写入数据对于良好的性能至关重要。
建议 :
.Where
, .Select
或 .Sum
语句) ,以便数据库执行过滤提高性能 。请参阅 EF 高性能专题 以了解可能提高应用性能的方法:
在代码提交之前,我们建议评估上述高性能方法的影响。 编译查询的额外复杂性可能无法一定确保性能提高。
可以通过使用 Application Insights 或使用分析工具查看访问数据所花费的时间来检测查询问题。 大多数数据库还提供有关频繁执行的查询的统计信息,这也可以作为重要参考。
虽然 HttpClient 实现了 IDisposable
接口,但它其实被设计为可以重复使用单个实例。 关闭 HttpClient
实例会使套接字在短时间内以 TIME_WAIT
状态打开。 如果经常创建和释放 HttpClient
对象,那么应用程序可能会耗尽可用套接字。 在 ASP.NET Core 2.1 中,引入了 HttpClientFactory 作为解决这个问题的办法。 它以池化 HTTP 连接的方式从而优化性能和可靠性。
建议 :
HttpClient
实例。HttpClient
实例。 有关更多信息,请参阅 使用 HttpClientFactory 以实现弹性 HTTP 请求。如果你想要所有的代码都保持高速, 高频调用的代码路径就是优化的最关键路径。 优化措施包括:
建议 :
对 ASP.NET Core 应用程序的大多数请求可以由调用服务的 controller 或页面模型处理,并返回 HTTP 响应。 对于涉及长时间运行的任务的某些请求,最好使整个请求 - 响应进程异步。
建议 :
复杂的 ASP.NET Core 应用程序经常包含很有前端文件例如 JavaScript, CSS 或图片文件。 可以通过以下方法优化初始请求的性能:
建议 :
减少响应的大小通常会显着提高应用程序的响应性。 而减小内容大小的一种方法是压缩应用程序的响应。 有关更多信息,请参阅 响应压缩。
ASP.NET Core 的每个新发行版都包含性能改进。 .NET Core 和 ASP.NET Core 中的优化意味着较新的版本通常优于较旧版本。 例如, .NET Core 2.1 添加了对预编译的正则表达式的支持,并从使用 Span
异常应该竟可能少。 相对于正常代码流程来说,抛出和捕获异常是缓慢的。 因此,不应使用异常来控制正常程序流。
建议 :
应用程序诊断工具 (如 Application Insights) 可以帮助识别应用程序中可能影响性能的常见异常。
下文将提供常见性能提示和已知可靠性问题的解决方案。
ASP.NET Core 中的所有 I/O 都是异步的。 服务器实现了 Stream
接口,它同时具有同步和异步的方法重载。 应该首选异步方式以避免阻塞线程池线程。 阻塞线程会导致线程池饥饿。
不要使用如下操作: https://docs.microsoft.com/en-us/dotnet/api/System.IO.StreamReader.ReadToEnd。 它会阻止当前线程等待结果。 这是 sync over async 的示例。
public class BadStreamReaderController : Controller
{
[HttpGet("/contoso")]
public ActionResult Get()
{
var json = new StreamReader(Request.Body).ReadToEnd();
return JsonSerializer.Deserialize(json);
}
}
在上述代码中, Get
采用同步的方式将整个 HTTP 请求主体读取到内存中。 如果客户端上载数据很慢,那么应用程序就会出现看似异步实际同步的操作。 应用程序看似异步实际同步,因为 Kestrel 不 支持同步读取。
应该采用如下操作: https://docs.microsoft.com/en-us/dotnet/api/System.IO.StreamReader.ReadToEndAsync ,在读取时不阻塞线程。
public class GoodStreamReaderController : Controller
{
[HttpGet("/contoso")]
public async Task> Get()
{
var json = await new StreamReader(Request.Body).ReadToEndAsync();
return JsonSerializer.Deserialize(json);
}
}
上述代码异步将整个 HTTP request body 读取到内存中。
[!WARNING] 如果请求很大,那么将整个 HTTP request body 读取到内存中可能会导致内存不足 (OOM) 。 OOM 可导致应用奔溃。 有关更多信息,请参阅 避免将大型请求主体或响应主体读取到内存中。
应该采用如下操作: 使用不缓冲的方式完成 request body 操作:
public class GoodStreamReaderController : Controller
{
[HttpGet("/contoso")]
public async Task> Get()
{
return await JsonSerializer.DeserializeAsync(Request.Body);
}
}
上述代码采用异步方式将 request body 序列化为 C# 对象。
应该使用 HttpContext.Request.ReadFormAsync
而不是 HttpContext.Request.Form
。 HttpContext.Request.Form
只能在以下场景用安全使用。
ReadFormAsync
调用,并且HttpContext.Request.Form
读取并缓存不要使用如下操作: 例如以下方式使用 HttpContext.Request.Form
。 HttpContext.Request.Form
使用了 sync over async ,这将导致线程饥饿.
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 Post()
{
var form = await HttpContext.Request.ReadFormAsync();
Process(form["id"], form["name"]);
return Accepted();
}
}
在 .NET 中,大于 85 KB 的对象会被分配在大对象堆 (LOH )。 大型对象的开销较大,包含两方面:
此 博文 很好描述了该问题:
当分配大对象时,它会被标记为 Gen 2 对象。 而不像是 Gen 0 那样的小对象。 这样的后果是,如果你在使用 LOH 时耗尽内存, GC 会清除整个托管堆,而不仅仅是 LOH 部分。 因此,它将清理 Gen 0, Gen 1 and Gen 2 (包括 LOH) 。 这称为 full garbage collection,是最耗时的垃圾回收。 对于很多应用,这是可以接受的。 但绝对不适用于高性能 Web 服务器,因为高性能 Web 服务器需要更多的内存用于处理常规 Web 请求 ( 从套接字读取,解压缩,解码 JSON 等等 )。
天真地将一个大型 request 或者 response body 存储到单个 byte[]
或 string
中:
例如使用仅支持同步读取和写入的序列化器 / 反序列化器时 ( 例如, JSON.NET):
[!WARNING] 如果请求较大,那么可能导致内存不足 (OOM) 。 OOM 可导致应用奔溃。 有关更多信息,请参阅 避免将大型请求主体或响应主体读取到内存。
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:
Newtonsoft.Json
更高的性能。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
。
应该采用如下操作:
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
可能会导致不符预期的行为,例如线程挂起,崩溃和数据损坏。
不要使用如下操作: 以下示例将发出三个并行请求,并在 HTTP 请求之前和之后记录传入的请求路径。 请求路径将被多个线程 (可能并行) 访问。
public class AsyncBadSearchController : Controller
{
[HttpGet("/search")]
public async Task 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 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 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 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
只有在 ASP.NET Core 管道处理活跃的 HTTP 请求时才可用。 整个 ASP.NET Core 管道是由异步代理组成的调用链,用于处理每个请求。 当 Task
从调用链完成并返回时,HttpContext
就会被回收。
不要进行如下操作: 以下示例使用 async void
,这将使得 HTTP 请求在第一个 await
时处理完成,进而就会导致:
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");
}
}
不要使用如下操作: 以下示例使用一个闭包从 Controller
属性读取 HttpContext
。 这是一种错误做法,因为这将导致:
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
获取 DbContext
进行操作。 这是一个错误的做法。 这将导致代码云在请求的作用域之外。 而 ContocoDbContext
是基于请求作用域的,因此这样将引发 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
是一个单例对象,所以这样没有问题。ContocoDbContext
。[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();
context.Contoso.Add(new Contoso());
await context.SaveChangesAsync();
}
});
return Accepted();
}
以下高亮的的代码说明:
ContocoDbContext
,即只能在请求作用域中使用该对象。[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();
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";
});
在上述的代码中,如果 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";
}
});
应该采用如下操作: 以下示例使用 HttpResponse.OnStarting
来设置 Header,这样便可以在响应启动时将 Header 一次性写入到客户端。
通过这种方式,响应头将在响应开始时调用已注册的回调进行一次性写入。 如此这般便可以:
app.Use(async (context, next) =>
{
context.Response.OnStarting(() =>
{
context.Response.Headers["someheader"] = "somevalue";
return Task.CompletedTask;
});
await next();
});
仅当后续组件能够处理响应或时才调用它们,因此如果当前已经开始写入响应主体,后续操作就已经不再需要,并有可能引发异常情况。
使用 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 项目参与的翻译贡献,您可以通过右侧链接一同参与该项目:https://www.newbe.pro/Newbe.Translations/Newbe.Translations/。
翻译内容具有一定的时效性,不会随着原文内容实时更新,如果内容存在一定过时,您也可以联系我们。