您好,登錄后才能下訂單哦!
本篇內容主要講解“ASP.NET Core性能優化的方法是什么”,感興趣的朋友不妨來看看。本文介紹的方法操作簡單快捷,實用性強。下面就讓小編來帶大家學習“ASP.NET Core性能優化的方法是什么”吧!
在本文檔中, 代碼熱點路徑 定義為頻繁調用的代碼路徑以及執行時間的大部分時間。 代碼熱點路徑通常限制應用程序的擴展和性能,并在本文檔的多個部分中進行討論。
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
事件表示新線程被添加到線程池。
在 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.
有關更多信息,請參閱 垃圾回收和性能。
與數據存儲器和其他遠程服務的交互通常是 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 高性能專題 以了解可能提高應用性能的方法:
DbContext 池
顯式編譯的查詢
在代碼提交之前,我們建議評估上述高性能方法的影響。 編譯查詢的額外復雜性可能無法一定確保性能提高。
可以通過使用 Application Insights 或使用分析工具查看訪問數據所花費的時間來檢測查詢問題。 大多數數據庫還提供有關頻繁執行的查詢的統計信息,這也可以作為重要參考。
雖然 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) 來定位 代碼熱點路徑。
對 ASP.NET Core 應用程序的大多數請求可以由調用服務的 controller 或頁面模型處理,并返回 HTTP 響應。 對于涉及長時間運行的任務的某些請求,最好使整個請求 - 響應進程異步。
建議 :
不要把等待長時間運行的任務完成,作為普通 HTTP 請求處理的一部分。
要 考慮使用 后臺服務 或 Azure Function 處理長時間運行的任務。 在應用外執行任務特別有利于 CPU 密集型任務的性能。
要 使用實時通信,如 SignalR,以異步方式與客戶端通信。
復雜的 ASP.NET Core 應用程序經常包含很有前端文件例如 JavaScript, CSS 或圖片文件。 可以通過以下方法優化初始請求的性能:
打包,將多個文件合并為一個文件。
壓縮,通過除去空格和注釋來縮小文件大小。
建議 :
要 使用 ASP.NET Core 的 內置支持 用于打包和壓縮客戶端資源文件的組件。
要 考慮其他第三方工具,如 Webpack,用于復雜客戶資產管理。
減少響應的大小通常會顯著提高應用程序的響應性。 而減小內容大小的一種方法是壓縮應用程序的響應。 有關更多信息,請參閱 響應壓縮。
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) 可以幫助識別應用程序中可能影響性能的常見異常。
下文將提供常見性能提示和已知可靠性問題的解決方案。
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<ContosoData> Get() { var json = new StreamReader(Request.Body).ReadToEnd(); return JsonSerializer.Deserialize<ContosoData>(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<ActionResult<ContosoData>> Get() { var json = await new StreamReader(Request.Body).ReadToEndAsync(); return JsonSerializer.Deserialize<ContosoData>(json); }} |
上述代碼異步將整個 HTTP request body 讀取到內存中。
[!WARNING] 如果請求很大,那么將整個 HTTP request body 讀取到內存中可能會導致內存不足 (OOM) 。 OOM 可導致應用奔潰。 有關更多信息,請參閱 避免將大型請求主體或響應主體讀取到內存中。
應該采用如下操作: 使用不緩沖的方式完成 request body 操作:
public class GoodStreamReaderController : Controller{ [HttpGet("/contoso")] public async Task<ActionResult<ContosoData>> Get() { return await JsonSerializer.DeserializeAsync<ContosoData>(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<IActionResult> Post() { var form = await HttpContext.Request.ReadFormAsync(); Process(form["id"], form["name"]); return Accepted(); } |
在 .NET 中,大于 85 KB 的對象會被分配在大對象堆 (LOH )。 大型對象的開銷較大,包含兩方面:
分配大對象內存時需要對被分配的內存進行清空,這個操作成本較高。 CLR 會保證清空所有新分配的對象的內存。(將內存全部設置為 0)
LOH 只會在內存剩余不足時回收。 LOH 需要在 full garbage collection 或者 Gen2 collection 進行回收。
此 博文 很好描述了該問題:
當分配大對象時,它會被標記為 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
中:
這可能導致 LOH 的剩余空間快速耗盡。
因此產生的 full GC 可能會導致應用程序的性能問題。
例如使用僅支持同步讀取和寫入的序列化器 / 反序列化器時 ( 例如, 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:
異步讀取和寫入 JSON 。
針對 UTF-8 文本進行了優化。
通常比 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
。
應該采用如下操作:
在字段中保存 https://docs.microsoft.com/en-us/aspnet/core/Microsoft.AspNetCore.Http.IHttpContextAccessor?view=aspnetcore-3.1。
在恰當的時機獲取并使用 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<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
只有在 ASP.NET Core 管道處理活躍的 HTTP 請求時才可用。 整個 ASP.NET Core 管道是由異步代理組成的調用鏈,用于處理每個請求。 當 Task
從調用鏈完成并返回時,HttpContext
就會被回收。
不要進行如下操作: 以下示例使用 async void
,這將使得 HTTP 請求在第一個 await
時處理完成,進而就會導致:
在 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"); }} |
不要使用如下操作: 以下示例使用一個閉包從 Controller
屬性讀取 HttpContext
。 這是一種錯誤做法,因為這將導致:
代碼運行在 Http 請求作用域之外。
嘗試讀取錯誤的 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();} |
應該采用如下操作:
在請求處理階段將后臺線程需要的數據全部進行復制。
不要使用 controller 的所有引用
[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();} |
應該采用如下操作:
注入 https://docs.microsoft.com/en-us/aspnet/core/Microsoft.Extensions.DependencyInjection.IServiceScopeFactory?view=aspnetcore-3.1 ,并且在后臺線程中創建新的作用域。 IServiceScopeFactory
是一個單例對象,所以這樣沒有問題。
在后臺線程中創建新作用域注入依賴的服務。
不要引用 controller 的所有內容
不要從請求中讀取 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<ContosoDbContext>(); 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<ContosoDbContext>(); context.Contoso.Add(new Contoso()); await context.SaveChangesAsync(); } }); return Accepted();} |
ASP.NET Core 不會緩沖 HTTP 響應正文。 當正文一旦開始發送:
Header 就會與正文的數據包一起發送到客戶端。
此時就無法修改 header 了。
不要使用如下操作: 以下代碼嘗試在響應啟動后添加響應頭:
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 一次性寫入到客戶端。
通過這種方式,響應頭將在響應開始時調用已注冊的回調進行一次性寫入。 如此這般便可以:
在恰當的時候進行響應頭的修改或者覆蓋。
不需要了解管道中的下一個 middleware 的行為。
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 模式進行托管。
到此,相信大家對“ASP.NET Core性能優化的方法是什么”有了更深的了解,不妨來實際操作一番吧!這里是億速云網站,更多相關內容可以進入相關頻道進行查詢,關注我們,繼續學習!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。