ASP.NET Core 性能优化最佳实践
本文提供了 ASP.NET Core 的性能最佳實(shí)踐指南。
譯文原文地址:https://docs.microsoft.com/en-us/aspnet/core/performance/performance-best-practices?view=aspnetcore-3.1
積極利用緩存
這里有一篇文檔在多個(gè)部分中討論了如何積極利用緩存。有關(guān)詳細(xì)信息,請(qǐng)參閱︰?https://docs.microsoft.com/en-us/aspnet/core/performance/caching/response?view=aspnetcore-3.1.
了解代碼中的熱點(diǎn)路徑
在本文檔中,?代碼熱點(diǎn)路徑?定義為頻繁調(diào)用的代碼路徑以及執(zhí)行時(shí)間的大部分時(shí)間。代碼熱點(diǎn)路徑通常限制應(yīng)用程序的擴(kuò)展和性能,并在本文檔的多個(gè)部分中進(jìn)行討論。
避免阻塞式調(diào)用
ASP.NET Core 應(yīng)用程序應(yīng)設(shè)計(jì)為同時(shí)處理許多請(qǐng)求。異步 API 可以使用一個(gè)小池線程通過非阻塞式調(diào)用來處理數(shù)以千計(jì)的并發(fā)請(qǐng)求。線程可以處理另一個(gè)請(qǐng)求,而不是等待長(zhǎng)時(shí)間運(yùn)行的同步任務(wù)完成。
ASP.NET Core 應(yīng)用程序中的常見性能問題通常是由于那些本可以異步調(diào)用但卻采用阻塞時(shí)調(diào)用而導(dǎo)致的。同步阻塞會(huì)調(diào)用導(dǎo)致?線程池饑餓?和響應(yīng)時(shí)間降級(jí)。
不要:
通過調(diào)用?Task.Wait?或?Task.Result?來阻止異步執(zhí)行。
在公共代碼路徑中加鎖。ASP.NET Core 應(yīng)用程序應(yīng)設(shè)計(jì)為并行運(yùn)行代碼,如此才能使得性能最佳。
調(diào)用?Task.Run?并立即 await 。ASP.NET Core 本身已經(jīng)是在線程池線程上運(yùn)行應(yīng)用程序代碼了,因此這樣調(diào)用 Task.Run 只會(huì)導(dǎo)致額外的不必要的線程池調(diào)度。而且即使被調(diào)度的代碼會(huì)阻止線程, Task.Run 也并不能避免這種情況,這樣做沒有意義。
要:
確保?代碼熱點(diǎn)路徑?全部異步化。
如在進(jìn)行調(diào)用數(shù)據(jù)讀寫、I/O 處理和長(zhǎng)時(shí)間操作的 API 時(shí),存在可用的異步 API。那么務(wù)必選擇異步 API 。但是,不要?使用?Task.Run?來包裝同步 API 使其異步化。
確保 controller/Razor Page actions 異步化。整個(gè)調(diào)用堆棧是異步的,就可以利用?async/await?模式的性能優(yōu)勢(shì)。
使用性能分析程序 ( 例如?PerfView) 可用于查找頻繁添加到?線程池?的線程。?Microsoft-Windows-DotNETRuntime/ThreadPoolWorkerThread/Start?事件表示新線程被添加到線程池。
使用 IEumerable<T>?或 IAsyncEnumerable<T> 作為返回值
在 Action 中返回?IEumerable<T>?將會(huì)被序列化器中進(jìn)行同步迭代 。結(jié)果是可能導(dǎo)致阻塞或者線程池饑餓。想要要避免同步迭代集合,可以在返回迭代集合之前使用?ToListAsync?使其異步化。
從 ASP.NET Core 3.0 開始,?IAsyncEnumerable<T>?可以用作為?IEumerable<T>?的替代方法,以異步方式進(jìn)行迭代。有關(guān)更多信息,請(qǐng)參閱?Controller Action 的返回值類型。
盡可能少的使用大對(duì)象
.NET Core 垃圾收集器?在 ASP.NET Core 應(yīng)用程序中起到自動(dòng)管理內(nèi)存的分配和釋放的作用。自動(dòng)垃圾回收通常意味著開發(fā)者不需要擔(dān)心如何或何時(shí)釋放內(nèi)存。但是,清除未引用的對(duì)象將會(huì)占用 CPU 時(shí)間,因此開發(fā)者應(yīng)最小化?代碼熱點(diǎn)路徑?中的分配的對(duì)象。垃圾回收在大對(duì)象上代價(jià)特大 (> 85 K 字節(jié)) 。大對(duì)象存儲(chǔ)在?large object heap?上,需要 full (generation 2) garbage collection 來清理。與 generation 0 和 generation 1 不同,generation 2 需要臨時(shí)暫掛應(yīng)用程序。故而頻繁分配和取消分配大型對(duì)象會(huì)導(dǎo)致性能耗損。
建議 :
要?考慮緩存頻繁使用的大對(duì)象。緩存大對(duì)象可防止昂貴的分配開銷。
要使用?ArrayPool<T>?作為池化緩沖區(qū)以保存大型數(shù)組。
不要?在代碼熱點(diǎn)路徑?上分配許多短生命周期的大對(duì)象。
可以通過查看?PerfView?中的垃圾回收 (GC) 統(tǒng)計(jì)信息來診斷并檢查內(nèi)存問題,其中包括:
垃圾回收掛起時(shí)間。
垃圾回收中耗用的處理器時(shí)間百分比。
有多少垃圾回收發(fā)生在 generation 0, 1, 和 2.
有關(guān)更多信息,請(qǐng)參閱?垃圾回收和性能。
優(yōu)化數(shù)據(jù)操作和 I/O
與數(shù)據(jù)存儲(chǔ)器和其他遠(yuǎn)程服務(wù)的交互通常是 ASP.NET Core 應(yīng)用程序最慢的部分。高效讀取和寫入數(shù)據(jù)對(duì)于良好的性能至關(guān)重要。
建議 :
要?以異步方式調(diào)用所有數(shù)據(jù)訪問 API 。
不要?讀取不需要的數(shù)據(jù)。編寫查詢時(shí),僅返回當(dāng)前 HTTP 請(qǐng)求所必需的數(shù)據(jù)。
要?考慮緩存從數(shù)據(jù)庫或遠(yuǎn)程服務(wù)檢索的頻繁訪問的數(shù)據(jù) (如果稍微過時(shí)的數(shù)據(jù)是可接受的話) 。根據(jù)具體的場(chǎng)景,可以使用?MemoryCache?或?DistributedCache。有關(guān)更多信息,請(qǐng)參閱?https://docs.microsoft.com/en-us/aspnet/core/performance/caching/response?view=aspnetcore-3.1.
要?盡量減少網(wǎng)絡(luò)往返。能夠單次調(diào)用完成就不應(yīng)該多次調(diào)用來讀取所需數(shù)據(jù)。
要?在 Entity Framework Core 訪問數(shù)據(jù)以用作只讀情況時(shí), 使用?no-tracking?方式查詢。EF Core 可以更高效地返回 no-tracking 查詢的結(jié)果。
要?使用過濾器和聚集 LINQ 查詢 (例如,?.Where,?.Select?或?.Sum?語句) ,以便數(shù)據(jù)庫執(zhí)行過濾提高性能 。
要?考慮 EF Core 可能在客戶端解析一些查詢運(yùn)算符,這可能導(dǎo)致查詢執(zhí)行效率低下。有關(guān)更多信息,請(qǐng)參閱?客戶端計(jì)算相關(guān)的性能問題。
不要?在集合上使用映射查詢,這會(huì)導(dǎo)致執(zhí)行 “N + 1” SQL 查詢。有關(guān)更多信息,請(qǐng)參閱?優(yōu)化子查詢。
請(qǐng)參閱?EF 高性能專題?以了解可能提高應(yīng)用性能的方法:
DbContext 池
顯式編譯的查詢
在代碼提交之前,我們建議評(píng)估上述高性能方法的影響。編譯查詢的額外復(fù)雜性可能無法一定確保性能提高。
可以通過使用?Application Insights?或使用分析工具查看訪問數(shù)據(jù)所花費(fèi)的時(shí)間來檢測(cè)查詢問題。大多數(shù)數(shù)據(jù)庫還提供有關(guān)頻繁執(zhí)行的查詢的統(tǒng)計(jì)信息,這也可以作為重要參考。
通過 HttpClientFactory 建立 HTTP 連接池
雖然?HttpClient?實(shí)現(xiàn)了?IDisposable?接口,但它其實(shí)被設(shè)計(jì)為可以重復(fù)使用單個(gè)實(shí)例。關(guān)閉?HttpClient?實(shí)例會(huì)使套接字在短時(shí)間內(nèi)以?TIME_WAIT?狀態(tài)打開。如果經(jīng)常創(chuàng)建和釋放?HttpClient?對(duì)象,那么應(yīng)用程序可能會(huì)耗盡可用套接字。在 ASP.NET Core 2.1 中,引入了?HttpClientFactory?作為解決這個(gè)問題的辦法。它以池化 HTTP 連接的方式從而優(yōu)化性能和可靠性。
建議 :
不要?直接創(chuàng)建和釋放?HttpClient?實(shí)例。
要?使用?HttpClientFactory?來獲取?HttpClient?實(shí)例。有關(guān)更多信息,請(qǐng)參閱?使用 HttpClientFactory 以實(shí)現(xiàn)彈性 HTTP 請(qǐng)求。
確保公共代碼路徑快若鷹隼
如果你想要所有的代碼都保持高速, 高頻調(diào)用的代碼路徑就是優(yōu)化的最關(guān)鍵路徑。優(yōu)化措施包括:
考慮優(yōu)化應(yīng)用程序請(qǐng)求處理管道中的 Middleware ,尤其是在管道中排在更前面運(yùn)行的 Middleware 。這些組件對(duì)性能有很大影響。
考慮優(yōu)化那些每個(gè)請(qǐng)求都要執(zhí)行或每個(gè)請(qǐng)求多次執(zhí)行的代碼。例如,自定義日志,身份認(rèn)證與授權(quán)或 transient 服務(wù)的創(chuàng)建等等。
建議 :
不要?使用自定義 middleware 運(yùn)行長(zhǎng)時(shí)任務(wù) 。
要?使用性能分析工具 ( 如?Visual Studio Diagnostic Tools?或?PerfView) 來定位?代碼熱點(diǎn)路徑。
在 HTTP 請(qǐng)求之外運(yùn)行長(zhǎng)時(shí)任務(wù)
對(duì) ASP.NET Core 應(yīng)用程序的大多數(shù)請(qǐng)求可以由調(diào)用服務(wù)的 controller 或頁面模型處理,并返回 HTTP 響應(yīng)。對(duì)于涉及長(zhǎng)時(shí)間運(yùn)行的任務(wù)的某些請(qǐng)求,最好使整個(gè)請(qǐng)求 - 響應(yīng)進(jìn)程異步。
建議 :
不要把等待長(zhǎng)時(shí)間運(yùn)行的任務(wù)完成,作為普通 HTTP 請(qǐng)求處理的一部分。
要?考慮使用?后臺(tái)服務(wù)?或?Azure Function?處理長(zhǎng)時(shí)間運(yùn)行的任務(wù)。在應(yīng)用外執(zhí)行任務(wù)特別有利于 CPU 密集型任務(wù)的性能。
要?使用實(shí)時(shí)通信,如?SignalR,以異步方式與客戶端通信。
縮小客戶端資源
復(fù)雜的 ASP.NET Core 應(yīng)用程序經(jīng)常包含很有前端文件例如 JavaScript, CSS 或圖片文件??梢酝ㄟ^以下方法優(yōu)化初始請(qǐng)求的性能:
打包,將多個(gè)文件合并為一個(gè)文件。
壓縮,通過除去空格和注釋來縮小文件大小。
建議 :
要?使用 ASP.NET Core 的?內(nèi)置支持?用于打包和壓縮客戶端資源文件的組件。
要?考慮其他第三方工具,如?Webpack,用于復(fù)雜客戶資產(chǎn)管理。
壓縮 Http 響應(yīng)
減少響應(yīng)的大小通常會(huì)顯著提高應(yīng)用程序的響應(yīng)性。而減小內(nèi)容大小的一種方法是壓縮應(yīng)用程序的響應(yīng)。有關(guān)更多信息,請(qǐng)參閱?響應(yīng)壓縮。
使用最新的 ASP.NET Core 發(fā)行版
ASP.NET Core 的每個(gè)新發(fā)行版都包含性能改進(jìn)。.NET Core 和 ASP.NET Core 中的優(yōu)化意味著較新的版本通常優(yōu)于較舊版本。例如, .NET Core 2.1 添加了對(duì)預(yù)編譯的正則表達(dá)式的支持,并從使用?Span<T>?改進(jìn)性能。ASP.NET Core 2.2 添加了對(duì) HTTP/2 的支持。?ASP.NET Core 3.0 增加了許多改進(jìn)?,以減少內(nèi)存使用量并提高吞吐量。如果性能是優(yōu)先考慮的事情,那么請(qǐng)升級(jí)到 ASP.NET Core 的當(dāng)前版本。
最小化異常
異常應(yīng)該盡可能少。 相對(duì)于正常代碼流程來說,拋出和捕獲異常是緩慢的。 因此,不應(yīng)使用異常來控制正常程序流。
建議 :
不要?使用拋出或捕獲異常作為正常程序流的手段,特別是在?代碼熱點(diǎn)路徑?中。
要?在應(yīng)用程序中包含用于檢測(cè)和處理導(dǎo)致異常的邏輯。
要?對(duì)意外的執(zhí)行情況拋出或捕獲異常。
應(yīng)用程序診斷工具 (如 Application Insights) 可以幫助識(shí)別應(yīng)用程序中可能影響性能的常見異常。
性能和可靠性
下文將提供常見性能提示和已知可靠性問題的解決方案。
避免在 HttpRequest/HttpResponse body 上同步讀取或?qū)懭?/h2>
ASP.NET Core 中的所有 I/O 都是異步的。服務(wù)器實(shí)現(xiàn)了?Stream?接口,它同時(shí)具有同步和異步的方法重載。應(yīng)該首選異步方式以避免阻塞線程池線程。阻塞線程會(huì)導(dǎo)致線程池饑餓。
不要使用如下操作:?https://docs.microsoft.com/en-us/dotnet/api/System.IO.StreamReader.ReadToEnd。它會(huì)阻止當(dāng)前線程等待結(jié)果。這是?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?采用同步的方式將整個(gè) HTTP 請(qǐng)求主體讀取到內(nèi)存中。如果客戶端上載數(shù)據(jù)很慢,那么應(yīng)用程序就會(huì)出現(xiàn)看似異步實(shí)際同步的操作。應(yīng)用程序看似異步實(shí)際同步,因?yàn)?Kestrel 不?支持同步讀取。
應(yīng)該采用如下操作:?https://docs.microsoft.com/en-us/dotnet/api/System.IO.StreamReader.ReadToEndAsync?,在讀取時(shí)不阻塞線程。
| 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);}} |
上述代碼異步將整個(gè) HTTP request body 讀取到內(nèi)存中。
[!WARNING] 如果請(qǐng)求很大,那么將整個(gè) HTTP request body 讀取到內(nèi)存中可能會(huì)導(dǎo)致內(nèi)存不足 (OOM) 。OOM 可導(dǎo)致應(yīng)用奔潰。有關(guān)更多信息,請(qǐng)參閱?避免將大型請(qǐng)求主體或響應(yīng)主體讀取到內(nèi)存中。
應(yīng)該采用如下操作:?使用不緩沖的方式完成 request body 操作:
| public class GoodStreamReaderController : Controller {[HttpGet("/contoso")]public async Task<ActionResult<ContosoData>> Get(){return await JsonSerializer.DeserializeAsync<ContosoData>(Request.Body);} } |
上述代碼采用異步方式將 request body 序列化為 C# 對(duì)象。
優(yōu)先選用 Request.Form 的 ReadFormAsync
應(yīng)該使用?HttpContext.Request.ReadFormAsync?而不是?HttpContext.Request.Form。?HttpContext.Request.Form?只能在以下場(chǎng)景用安全使用。
該表單已被?ReadFormAsync?調(diào)用,并且
數(shù)據(jù)已經(jīng)被從?HttpContext.Request.Form?讀取并緩存
不要使用如下操作:?例如以下方式使用?HttpContext.Request.Form。?HttpContext.Request.Form?使用了?sync over async?,這將導(dǎo)致線程饑餓.
| public class BadReadController : Controller {[HttpPost("/form-body")]public IActionResult Post(){var form = HttpContext.Request.Form;Process(form["id"], form["name"]);return Accepted();} |
應(yīng)該使用如下操作:?使用?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();} |
避免將大型 request body 或 response body 讀取到內(nèi)存中
在 .NET 中,大于 85 KB 的對(duì)象會(huì)被分配在大對(duì)象堆 (LOH?)。大型對(duì)象的開銷較大,包含兩方面:
分配大對(duì)象內(nèi)存時(shí)需要對(duì)被分配的內(nèi)存進(jìn)行清空,這個(gè)操作成本較高。CLR 會(huì)保證清空所有新分配的對(duì)象的內(nèi)存。(將內(nèi)存全部設(shè)置為 0)
LOH 只會(huì)在內(nèi)存剩余不足時(shí)回收。LOH 需要在?full garbage collection?或者?Gen2 collection?進(jìn)行回收。
此?博文?很好描述了該問題:
當(dāng)分配大對(duì)象時(shí),它會(huì)被標(biāo)記為 Gen 2 對(duì)象。而不像是 Gen 0 那樣的小對(duì)象。這樣的后果是,如果你在使用 LOH 時(shí)耗盡內(nèi)存, GC 會(huì)清除整個(gè)托管堆,而不僅僅是 LOH 部分。因此,它將清理 Gen 0, Gen 1 and Gen 2 (包括 LOH) 。這稱為 full garbage collection,是最耗時(shí)的垃圾回收。對(duì)于很多應(yīng)用,這是可以接受的。但絕對(duì)不適用于高性能 Web 服務(wù)器,因?yàn)楦咝阅?Web 服務(wù)器需要更多的內(nèi)存用于處理常規(guī) Web 請(qǐng)求 ( 從套接字讀取,解壓縮,解碼 JSON 等等 )。
天真地將一個(gè)大型 request 或者 response body 存儲(chǔ)到單個(gè)?byte[]?或?string?中:
這可能導(dǎo)致 LOH 的剩余空間快速耗盡。
因此產(chǎn)生的 full GC 可能會(huì)導(dǎo)致應(yīng)用程序的性能問題。
使用同步 API 處理數(shù)據(jù)
例如使用僅支持同步讀取和寫入的序列化器 / 反序列化器時(shí) ( 例如,?JSON.NET):
將數(shù)據(jù)異步緩沖到內(nèi)存中,然后將其傳遞到序列化器 / 反序列化器。
[!WARNING] 如果請(qǐng)求較大,那么可能導(dǎo)致內(nèi)存不足 (OOM) 。OOM 可導(dǎo)致應(yīng)用奔潰。有關(guān)更多信息,請(qǐng)參閱?避免將大型請(qǐng)求主體或響應(yīng)主體讀取到內(nèi)存。
ASP.NET Core 3.0 默認(rèn)情況下使用?https://docs.microsoft.com/en-us/dotnet/api/system.text.json?進(jìn)行 JSON 序列化,這將帶來如下好處。?https://docs.microsoft.com/en-us/dotnet/api/system.text.json:
異步讀取和寫入 JSON 。
針對(duì) UTF-8 文本進(jìn)行了優(yōu)化。
通常比?Newtonsoft.Json?更高的性能。
不要將 IHttpContextAccessor.HttpContext 存儲(chǔ)在字段中
IHttpContextAccessor.HttpContext?返回當(dāng)前請(qǐng)求線程中的?HttpContext.?IHttpContextAccessor.HttpContext** 不應(yīng)該 ** 被存儲(chǔ)在一個(gè)字段或變量中。
不要使用如下操作:?例如將?HttpContext?存儲(chǔ)在字段中,然后在后續(xù)使用該字段。
| 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");}} } |
以上代碼在構(gòu)造函數(shù)中經(jīng)常得到 Null 或不正確的?HttpContext。
應(yīng)該采用如下操作:
在字段中保存?https://docs.microsoft.com/en-us/aspnet/core/Microsoft.AspNetCore.Http.IHttpContextAccessor?view=aspnetcore-3.1。
在恰當(dāng)?shù)臅r(shí)機(jī)獲取并使用?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?不是?線程安全的。從多個(gè)線程并行訪問?HttpContext?可能會(huì)導(dǎo)致不符預(yù)期的行為,例如線程掛起,崩潰和數(shù)據(jù)損壞。
不要使用如下操作:?以下示例將發(fā)出三個(gè)并行請(qǐng)求,并在 HTTP 請(qǐng)求之前和之后記錄傳入的請(qǐng)求路徑。請(qǐng)求路徑將被多個(gè)線程 (可能并行) 訪問。
| 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;} |
應(yīng)該這樣操作:?以下示例在發(fā)出三個(gè)并行請(qǐng)求之前,從傳入請(qǐng)求復(fù)制下文需要使用的數(shù)據(jù)。
| 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;} |
請(qǐng)求處理完成后不要使用 HttpContext
HttpContext?只有在 ASP.NET Core 管道處理活躍的 HTTP 請(qǐng)求時(shí)才可用。整個(gè) ASP.NET Core 管道是由異步代理組成的調(diào)用鏈,用于處理每個(gè)請(qǐng)求。當(dāng)?Task?從調(diào)用鏈完成并返回時(shí),HttpContext?就會(huì)被回收。
不要進(jìn)行如下操作:?以下示例使用?async void?,這將使得 HTTP 請(qǐng)求在第一個(gè)?await?時(shí)處理完成,進(jìn)而就會(huì)導(dǎo)致:
在 ASP.NET Core 應(yīng)用程序中, 這是一個(gè)完全錯(cuò)誤?的做法
在 HTTP 請(qǐng)求完成后訪問?HttpResponse。
進(jìn)程崩潰。
| 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");} } |
應(yīng)該進(jìn)行如下操作:?以下示例將?Task?返回給框架,因此,在操作完成之前, HTTP 請(qǐng)求不會(huì)完成。
| public class AsyncGoodTaskController : Controller {[HttpGet("/async")]public async Task Get(){await Task.Delay(1000);await Response.WriteAsync("Hello World");} } |
不要在后臺(tái)線程中使用 HttpContext
不要使用如下操作:?以下示例使用一個(gè)閉包從?Controller?屬性讀取?HttpContext。這是一種錯(cuò)誤做法,因?yàn)檫@將導(dǎo)致:
代碼運(yùn)行在 Http 請(qǐng)求作用域之外。
嘗試讀取錯(cuò)誤的?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(); } |
應(yīng)該采用如下操作:
在請(qǐng)求處理階段將后臺(tái)線程需要的數(shù)據(jù)全部進(jìn)行復(fù)制。
不要使用 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(); } |
后臺(tái)任務(wù)最好采用托管服務(wù)進(jìn)行操作。有關(guān)更多信息,請(qǐng)參閱?采用托管服務(wù)運(yùn)行后臺(tái)任務(wù)?。
不要在后臺(tái)線程獲取注入到 controller 中的服務(wù)
不要采用如下做法:?以下示例使用閉包從?controller?獲取?DbContext?進(jìn)行操作。這是一個(gè)錯(cuò)誤的做法。這將導(dǎo)致代碼云在請(qǐng)求的作用域之外。而?ContocoDbContext?是基于請(qǐng)求作用域的,因此這樣將引發(fā)?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(); } |
應(yīng)該采用如下操作:
注入?https://docs.microsoft.com/en-us/aspnet/core/Microsoft.Extensions.DependencyInjection.IServiceScopeFactory?view=aspnetcore-3.1?,并且在后臺(tái)線程中創(chuàng)建新的作用域。?IServiceScopeFactory?是一個(gè)單例對(duì)象,所以這樣沒有問題。
在后臺(tái)線程中創(chuàng)建新作用域注入依賴的服務(wù)。
不要引用 controller 的所有內(nèi)容
不要從請(qǐng)求中讀取?ContocoDbContext。
| [HttpGet("/fire-and-forget-3")] public IActionResult FireAndForget3([FromServices]IServiceScopeFactoryserviceScopeFactory) {_ = 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(); } |
以下高亮的的代碼說明:
為后臺(tái)操作創(chuàng)建新的作用域,并且從中獲取需要的服務(wù)。
在正確的作用域中使用?ContocoDbContext,即只能在請(qǐng)求作用域中使用該對(duì)象。
| [HttpGet("/fire-and-forget-3")] public IActionResult FireAndForget3([FromServices]IServiceScopeFactoryserviceScopeFactory) {_ = 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(); } |
不要在響應(yīng)正文已經(jīng)開始發(fā)送時(shí)嘗試修改 status code 或者 header
ASP.NET Core 不會(huì)緩沖 HTTP 響應(yīng)正文。當(dāng)正文一旦開始發(fā)送:
Header 就會(huì)與正文的數(shù)據(jù)包一起發(fā)送到客戶端。
此時(shí)就無法修改 header 了。
不要使用如下操作:?以下代碼嘗試在響應(yīng)啟動(dòng)后添加響應(yīng)頭:
| app.Use(async (context, next) => {await next();context.Response.Headers["test"] = "test value"; }); |
在上述的代碼中,如果?next()?已經(jīng)開始寫入響應(yīng),則?context.Response.Headers["test"] = "test value";?將會(huì)拋出異常。
應(yīng)該采用如下操作:?以下示例檢查 HTTP 響應(yīng)在修改 Header 之前是否已啟動(dòng)。
| app.Use(async (context, next) => {await next();if (!context.Response.HasStarted){context.Response.Headers["test"] = "test value";} }); |
應(yīng)該采用如下操作:?以下示例使用?HttpResponse.OnStarting?來設(shè)置 Header,這樣便可以在響應(yīng)啟動(dòng)時(shí)將 Header 一次性寫入到客戶端。
通過這種方式,響應(yīng)頭將在響應(yīng)開始時(shí)調(diào)用已注冊(cè)的回調(diào)進(jìn)行一次性寫入。如此這般便可以:
在恰當(dāng)?shù)臅r(shí)候進(jìn)行響應(yīng)頭的修改或者覆蓋。
不需要了解管道中的下一個(gè) middleware 的行為。
| app.Use(async (context, next) => {context.Response.OnStarting(() =>{context.Response.Headers["someheader"] = "somevalue";return Task.CompletedTask;});await next(); }); |
如果已開始寫入響應(yīng)主體,則請(qǐng)不要調(diào)用 next ()
僅當(dāng)后續(xù)組件能夠處理響應(yīng)或時(shí)才調(diào)用它們,因此如果當(dāng)前已經(jīng)開始寫入響應(yīng)主體,后續(xù)操作就已經(jīng)不再需要,并有可能引發(fā)異常情況。
托管于 IIS 應(yīng)該使用 In-process 模式
使用 in-process 模式托管, ASP.NET Core 應(yīng)用程序?qū)⑴c IIS 工作進(jìn)程在同一進(jìn)程中運(yùn)行。In-process 模式擁有比 out-of-process 更加優(yōu)秀的性能表現(xiàn),因?yàn)檫@樣不需要將請(qǐng)求通過回環(huán)網(wǎng)絡(luò)適配器進(jìn)行代理中轉(zhuǎn)?;丨h(huán)網(wǎng)絡(luò)適配器是將本機(jī)發(fā)送的網(wǎng)絡(luò)流量重新轉(zhuǎn)回本機(jī)的的網(wǎng)絡(luò)適配器。IIS 進(jìn)程管理由?Windows Process Activation Service (WAS)?來完成。
在 ASP.NET Core 3.0 和更高版本中的默認(rèn)將采用 in-process 模式進(jìn)行托管。
有關(guān)更多信息,請(qǐng)參閱?在 Windows 上使用 IIS 托管 ASP.NET Core
Newbe.Translations
您所閱讀的當(dāng)前文章源自于 Newbe.Translations 項(xiàng)目參與的翻譯貢獻(xiàn),您可以通過右側(cè)鏈接一同參與該項(xiàng)目:https://www.newbe.pro/Newbe.Translations/Newbe.Translations/。
翻譯內(nèi)容具有一定的時(shí)效性,不會(huì)隨著原文內(nèi)容實(shí)時(shí)更新,如果內(nèi)容存在一定過時(shí),您也可以聯(lián)系我們。
本文作者:?newbe36524
本文鏈接:?https://www.newbe.pro/Newbe.Translations/001-ASP.NET-Core-Performance-Best-Practices/
版權(quán)聲明:?本博客所有文章除特別聲明外,均采用?BY-NC-SA?許可協(xié)議。轉(zhuǎn)載請(qǐng)注明出處!
總結(jié)
以上是生活随笔為你收集整理的ASP.NET Core 性能优化最佳实践的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: C# 中 Struct 和 Class
- 下一篇: 送福利 | 送书5本《ASP.NET C