更新:后來升級至 .NET Core 2.2 Preview 3 ,並將 System.Net.Http 升級至 4.3.4 之后沒出現這個問題,問題與 https://github.com/dotnet/corefx/pull/32568 有關。
以下內容是當時的錯誤判斷。
今天上午的故障之后,我們 review 了代碼,通過壓力測試重現問題,分析驗證,最終找到了問題的真正原因 —— 在 ASP.NET Core 程序中調用 async 方法時沒加 await 。
public async Task<IActionResult> GetRecommDocuments()
{
//...
ShowItem(docs, app); //async方法,其中用到了HttpClient
return Ok(docs);
}
就是上面的代碼中“漏”寫了 await ,不是粗心漏寫,是故意為之。我們不關心調用 ShowItem 的結果,只要能執行就行,即使執行失敗也可以接受。不加 await 可以讓 GetRecommDocuments 方法調用 ShowItem 之后無需等待繼續執行從而提高響應速度,但是沒想到這一招偷工減料,竟然抽空了服務器的 TCP 連接資源,讓整個服務器大廈轟然倒塌。
以下刪除線部分的分析是錯誤的,真正原因有待進一步追蹤。
開始從 async/await 的角度怎么也想不通 —— 少一個 await 怎么會如此嚴重后果,后來突然想到罪魁禍首不在 async/await 而在依賴注入(DI)。GetRecommDocuments 中的 HttpClient 實例是通過構造函數的 IHttpClientFactory 注入的,注入的 HttpClient 實例的生命周期是 Scoped (當前請求范圍),本來正常情況下一個請求處理結束,HttpClient 實例會被 DI 容器 Dispose,所使用的 Socket 連接會被放回連接池,但是由於沒加 await ,讓 ShowItem 不走正常路,DI 容器在請求處理結束時跟蹤不到 ShowItem 中的 HttpClient 實例,也就不會進行 Dispose 。結果來一個請求,IHttpClientFactory 就創建一個 HttpClient 實例,每個 HttpClient 實例都占用一個新的 TCP 連接,直至拖垮服務器。
隨着軟件開發技術的發展,開發效率越來越高了,要寫的代碼越來越少了,但要考慮的問題卻越來越多了。雖然我們每天都在用着 .NET Core ,但由於其內部工作機制不夠熟悉,原以為“巧奪天工”的一招卻釀成大錯,我們會牢記這次教訓,進一步地對 .NET Core 刨根問底 。