ASP.NET Core 性能优化最佳实践
本文提供了 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 应用程序中的常见性能问题通常是由于那些本可以异步调用但却采用阻塞时调用而导致的。 同步阻塞会调用导致 线程池饥饿 和响应时间降级。
不要:
- 通过调用 Task.Wait 或 Task.Result 来阻止异步执行。
- 在公共代码路径中加锁。 ASP.NET Core 应用程序应设计为并行运行代码,如此才能使得性能最佳。
- 调用 Task.Run 并立即 await 。 ASP.NET Core 本身已经是在线程池线程上运行应用程序代码了,因此这样调用 Task.Run 只会导致额外的不必要的线程池调度。 而且即使被调度的代码会阻止线程, Task.Run 也并不能避免这种情况,这样做没有意义。
要:
- 确保 代码热点路径 全部异步化。
- 如在进行调用数据读写、I/O 处理和长时间操作的 API 时,存在可用的异步 API。那么务必选择异步 API 。 但是,不要 使用 Task.Run 来包装同步 API 使其异步化。
- 确保 controller/Razor Page actions 异步化。 整个调用堆栈是异步的,就可以利用 async/await 模式的性能优势。
使用性能分析程序 ( 例如 PerfView) 可用于查找频繁添加到 线程池 的线程。
Microsoft-Windows-DotNETRuntime/ThreadPoolWorkerThread/Start
事件表示新线程被添加到线程池。
使用 IEumerable<T> 或 IAsyncEnumerable<T> 作为返回值
在 Action 中返回IEumerable<T>
将会被序列化器中进行同步迭代 。 结果是可能导致阻塞或者线程池饥饿。 想要要避免同步迭代集合,可以在返回迭代集合之前使用 ToListAsync
使其异步化。
从 ASP.NET Core 3.0 开始, IAsyncEnumerable<T>
可以用作为 IEumerable<T>
的替代方法,以异步方式进行迭代。 有关更多信息,请参阅 Controller Action 的返回值类型。
尽可能少的使用大对象
.NET Core 垃圾收集器 在 ASP.NET Core 应用程序中起到自动管理内存的分配和释放的作用。 自动垃圾回收通常意味着开发者不需要担心如何或何时释放内存。 但是,清除未引用的对象将会占用 CPU 时间,因此开发者应最小化 代码热点路径 中的分配的对象。 垃圾回收在大对象上代价特大 (> 85 K 字节 ) 。 大对象存储在 large object heap 上,需要 full (generation 2) garbage collection 来清理。 与 generation 0 和 generation 1 不同,generation 2 需要临时暂挂应用程序。 故而频繁分配和取消分配大型对象会导致性能耗损。
建议 :
- 要 考虑缓存频繁使用的大对象。 缓存大对象可防止昂贵的分配开销。
- 要使用 ArrayPool<T> 作为池化缓冲区以保存大型数组。
- 不要 在代码热点路径 上分配许多短生命周期的大对象。
可以通过查看 PerfView 中的垃圾回收 (GC) 统计信息来诊断并检查内存问题,其中包括:
- 垃圾回收挂起时间。
- 垃圾回收中耗用的处理器时间百分 比。
- 有多少垃圾回收发生在 generation 0, 1, 和 2.
有关更多信息,请参阅 垃圾回收和性能。
优化数据操作和 I/O
与数据存储器和其他远程服务的交互通常是 ASP.NET Core 应用程序最慢的部分。 高效读取和写入数据对于良好的性能至关重要。
建议 :
- 要 以异步方式调用所有数据访问 API 。
- 不要 读取不需要的数据。 编写查询时,仅返回当前 HTTP 请求所必需的数据。
- 要 考虑缓存从数据库或远程服务检索的频繁访问的数据 ( 如果稍微过时的数据是可接受的话 ) 。 根据具体的场景,可以使用 MemoryCache 或 DistributedCache。 有关更多信息,请参阅 https://docs.microsoft.com/en-us/aspnet/core/performance/caching/response?view=aspnetcore-3.1.
- 要 尽量减少网络往返。 能够单次调用完成就不应该多次调用来读取所需数据。
- 要 在 Entity Framework Core 访问数据以用作只读情况时, 使用 no-tracking方式查询。 EF Core 可以更高效地返回 no-tracking 查询的结果。
- 要 使用过滤器和聚集 LINQ 查询 (例如,
.Where
,.Select
或.Sum
语句) ,以便数据库执行过滤提高性能 。 - 要 考虑 EF Core 可能在客户端解析一些查询运算符,这可能导致查询执行效率低下。 有关更多信息,请参阅 客户端计算相关的性能问题。
- 不要 在集合上使用映射查询,这会导致执行 "N + 1" SQL 查询。 有关更多信息,请参阅 优化子查询。
请参阅 EF 高性能专题 以了解可能提高应用性能的方法:
在代码提交之前,我们建议评估上述高性能方法的影响。 编译查询的额外复杂性可能无法一定确保性能提高。
可以通过使用 Application Insights 或使用分析工具查看访问数据所花费的时间来检测查 询问题。 大多数数据库还提供有关频繁执行的查询的统计信息,这也可以作为重要参考。
通过 HttpClientFactory 建立 HTTP 连接池
虽然 HttpClient 实现了 IDisposable
接口,但它其实被设计为可以重复使用单个实例。 关闭 HttpClient
实例会使套接字在短时间内以 TIME_WAIT
状态打开。 如果经常创建和释放 HttpClient
对象,那么应用程序可能会耗尽可用套接字。 在 ASP.NET Core 2.1 中,引入了HttpClientFactory 作为解决这个问题的办法。 它以池化 HTTP 连接的方式从而优化性能和可靠性。
建议 :
- 不要 直接创建和释放
HttpClient
实例。 - 要 使用 HttpClientFactory 来获取
HttpClient
实例。 有关更多信息,请参阅 使用 HttpClientFactory 以实现弹性 HTTP 请求。
确保公共代码路径快若鹰隼
如果你想要所有的代码都保持高速, 高频调用的代码路径就是优化的最关键路径。 优化措施包括:
- 考虑优化应用程序请求处理管道中的 Middleware ,尤其是在管道中排在更前面运行的 Middleware 。 这些组件对性能有很大影响。
- 考虑优化那些每个请求都要执行或每个请求多次执行的代码。 例如,自定义日志,身份认证与授权或 transient 服务的创建等等。
建议 :
- 不要 使用自定义 middleware 运行长时任务 。
- 要 使用性能分析工具( 如 Visual Studio Diagnostic Tools 或 PerfView) 来定位 代码热点路径。
在 HTTP 请求之外运行长时任务
对 ASP.NET Core 应用程序的大多数请求可以由调用服务的 controller 或页面模型处理,并返回 HTTP 响应。 对于涉及长时间运行的任务的某些请求,最好使整个请求-响应进程异步。
建议 :
- 不要把等待 长时间运行的任务完成,作为普通 HTTP 请求处理的一部分。
- 要 考虑使用 后台服务 或 Azure Function 处理长时间运行的任务。 在应用外执行任务特别有利于 CPU 密集型任务的性能。
- 要 使用实时通信,如 SignalR,以异步方式与客户端通信。
缩小客户端资源
复杂的 ASP.NET Core 应用程序经常包含很有前端文件例如 JavaScript, CSS 或图片文件。 可以通过以下方法优化初始请求的性能:
- 打包,将多个文件合并为一个文件。
- 压缩,通过除去空格和注释来缩小文件大小。
建议 :