CPU 100% 問題分析,我們把博客園踩過的坑又踩了一遍《一》


針對.net core 異常奇怪問題分析


以上問題都可能是因為線程池飢餓(ThreadPool starvation),理論上線程池飢餓一直是一個潛在的問題,在大規模異步服務出現之前,所需要的線程數是可以預測,因此這些問題很少發生(調試不容易發現)。但是隨着更大規模異步應用,此問題發生的機率增加。
在極端情況下,吞吐量損失是如此糟糕,服務似乎沒有取得進展。更常見的情況是,你只是得到你期望的,或者你不能處理負載的"突發"。(因此,在使用 20% CPU 時,在代替狀態下效果很好,但當您獲得突發時,它不會使用更多的 CPU 來為突發提供服務)。
這些一般症狀告訴你的是,你有一個'瓶頸',但它不是CPU。基本上,需要一些其他資源來服務請求,每個請求必須等待,而這種等待限制了吞吐量。現在最常見的原因是請求需要另一台計算機上的資源(通常是數據庫,但它可能是非計算機緩存(redis)或任何其他"進程外"資源)。
正如我們將看到的,這些常見問題相對簡單,因為我們可以"指責"適當的組成部分。當我們查看請求為何需要很長時間的細目時,我們會清楚地看到某些部分(例如等待數據庫的響應)需要很長時間,因此我們知道這就是問題所在。
然而,有一個陰險的情況,即"稀缺資源"本身是線程。這是一些操作(如數據庫查詢),完成,但當它這樣做時,沒有線程可以運行下一步服務請求,因此它只會停止,等待這樣的線程變為可用。這一次顯示為"較長"的數據庫查詢,但也會發生任何非 CPU 活動(例如,任何 I/O 或延遲),因此似乎非常 I/O 操作隨機需要的時間比它應該需要的時間長。我們稱此問題為 ThreadPool 飢餓,它是本文的重點。

從理論上講,線程池飢餓一直是一個潛在的問題,但正如所解釋的,在大規模異步服務出現之前,所需的線程數更可預測,因此問題很少發生。但是,隨着更多大規模異步應用,此問題的可能性已經增加,因此我編寫本文來描述如何診斷此問題是否影響您的服務,以及如果是此問題,該怎么辦。但在我們做到這一點之前,一些背景是有幫助的

什么是線程池?

在討論線程池之前,我們應該返回並描述什么是線程。線程是執行順序程序所需的狀態。對於一個很好的近似值,它是部分執行方法的"調用堆棧",包括每個方法的所有局部變量。關鍵是所有代碼都需要線程才能"運行"。當程序啟動時,會為它提供一個線程,並且多線程程序創建其他線程,每個線程彼此同時執行代碼。

線程在並發量適中、每個線程都執行復雜操作的世界里有意義。但是,某些工作負載具有完全相反的特征:發生了許多並發事件,並且每個工作負載都在執行簡單的事情。由於所有執行都需要一個線程,因此對於此工作負荷,重用線程是有意義的,因為它要執行許多小型(不相關的)工作項。這是一個線程池。它有一個非常簡單的API。在.NET中,它是線程池.QueueuserWorkItem,它需要委托(方法)才能運行。當您調用 QueueUserWorkItem 時,線程池承諾運行您將來經過一段時間的委托。(注意 .NET 不鼓勵直接使用線程池。Task.Factory.StartNew(操作)還對方法進行排隊到 .NET 線程池,但可以更輕松地處理錯誤條件和等待結果。

什么是異步編程?

過去,服務使用"多線程"執行模型,其中服務將為處理的每個並發請求創建一個線程。每個此類線程將從頭到尾為該特定請求執行所有工作,然后轉到下一個。這適用於中低比例服務,但由於線程是一個相對昂貴的項目(通常您希望少於 1000 個,最好是 < 100),如果您希望服務能夠同時處理 1000s 或 10000s 的請求,則此多線程模型不能很好地工作。

要處理此類規模,您需要一種異步編程ASP.NET此體系結構構建的。在此模型中,您不是具有"每個並發請求的線程",而是在執行長操作(通常是 I/O)時注冊回調,並在等待時重用線程執行其他處理。此體系結構意味着只有少數線程(理想情況下是關於物理處理器的數量)可以處理大量(1000 個或更多)並發請求。您還可以查看異步編程需要線程池,因為當 I/O 完成時,您需要運行下一個"塊"代碼,因此需要一個線程。Asynchrounous 代碼使用線程池來讓此線程運行此小塊代碼,然后將線程返回到池中,以便線程可以運行一些其他(不相關的)塊。

為什么大規模異步服務存在線程池飢餓問題?

當整個服務統一使用異步編程樣式時,您的服務可以很好地擴展。線程在運行服務代碼時從不阻塞,因為如果操作需要一段時間,則代碼應該調用一個采用回調的版本(或返回 System.Threading.Tasks.Task),並導致代碼的其余部分在 I/O 完成時運行(C# 有一個神奇的"await"語句,如下所示是塊的,但實際上,當 await 語句完成時,會安排回調來運行下一個語句)。因此,線程永遠不會阻塞,除非在線程池中等待更多的工作,因此只需要適量的線程(基本上是計算機上的 CPU 數量)。

不幸的是,當您的服務以大規模運行時,它不會花太多時間來破壞您的 perf。例如,假設您有 1000 個並發請求正在由 16核處理器計算機處理。使用異步,線程池只需要 16 個線程來為這 1000 個請求提供服務。為了簡單起見,該計算機具有 16 個 CPU,並且線程池每個 CPU 只有一個線程(因此為 16),現在允許映像某人在請求處理開始時將 Sleep (200) 進行映像。理想情況下,您認為這只會導致每個請求延遲 200 毫秒,因此響應時間為 400 毫秒。但是,線程池只有 16 個線程(當一切都是異步時,這就足夠了),這突然之間是不夠的。只有前 16 個線程運行, 200 毫秒只是睡覺。只有在 200 毫秒后,這 16 個線程才能再次可用,因此另外 16 個請求可以轉到等。因此,第一個請求延遲 200 毫秒,第二組延遲 400 毫秒,第三組延遲 600 毫秒。對於 1000 個同時請求的負載,平均響應時間高達 6 秒以上。現在,您可以了解為什么在某些情況下,吞吐量會從精細到可怕,只需非常少量的阻塞。

此示例是精心創建,但它說明了要點。如果添加任何阻塞,則線程池中所需的線程數會顯著跳(從 16 到 1000)。.NET Threadpool 確實嘗試注入更多線程,但以適中的速度(例如 1 或 2 秒)進行,因此在短期內幾乎沒有差別(您需要許多分鍾才能到達 1000)。此外,您已經失去了異步的好處(因為現在每個請求都需要一個線程,這是您試圖避免的)。(詳細示列請參考

因此,您可以看到有一個"克利夫"與異步代碼。只有"幾乎從不阻止"時,才能獲得異步代碼的好處。如果 Do 塊,並且以高比例運行,則很可能快速耗盡 Threadpool 的線程。線程池將嘗試補償,但它需要一段時間,坦率地說,你失去了異步的好處。正確的解決方案是避免在大規模服務中的"熱"路徑上阻塞。

什么通常會導致阻塞?

阻塞的常見原因包括

調用任何具有 I/O(因此可能會阻止)但不是異步(因為它不是異步 API,因此如果 I/O 不能快速完成,則必須進行阻止)
呼叫 Task. wait () 或 Task. GetResult (調用 Task. wait) 。使用這些 API 是危險信號。理想情況下,您采用異步方法,而是使用"await"。
因此,簡言之,這就是為什么線程池飢餓變得越來越普遍。大規模異步應用程序變得越來越常見,並且很容易將阻塞引入到服務代碼中,這迫使您離開異步的"黃金路徑",這需要許多線程池線程,從而導致飢餓。

我如何知道線程池缺少線程?

你怎么知道這件壞事正在發生?您從上述症狀開始,即您的 CPU 沒有您想要的飽和。從這里,我會告訴你一些症狀,你可以檢查,給這個問題更明確的答案。詳細信息確實會根據操作系統的變化而更改。我將展示窗口,案例,但我也將描述在Linux上做什么。

與一如既往,每當您有一個服務的問題,它是有用的得到詳細的性能跟蹤。在窗口中, 這意味着下載 PerfView采取 PerfView 跟蹤

PerfView /線程時間收集
在 Linux 上,它當前意味着使用 perfCollect 進行跟蹤,但是在 .NET Core 的第 2.2 版中,您將能夠使用"dotnet 配置文件"命令收集跟蹤(詳細信息為 TBD,當發生這種情況時我將更新)。

如果使用應用程序見解,也可以使用應用程序見解分析器捕獲跟蹤。

當服務負載不足但性能不佳時,您只需要 60 秒的跟蹤。

查找不斷增加的線程計數。

線程池飢餓的一個關鍵症狀是,線程池確實檢測到它處於飢餓狀態(有工作但沒有線程),並且試圖通過注入更多線程來修復它,但(根據設計)速度較慢(大約 1-2 次/秒)。因此,在 PerfView 的"事件視圖"(在窗口中),您會看到新線程的 OS 內核事件以該速率顯示。請注意,它大約以秒添加大約 2 個線程。

 

Linux 跟蹤不包括"事件"視圖中的 OS 事件,但每次創建線程時都會有 .NET 運行時事件告訴您。您應該查看以下事件

Microsoft-Windows-DotNETRuntime/IOThreadCreation/Start - 僅窗口(對於某些 I/O 上阻止的線程,它有一個特殊隊列),用於新的 I/O 工作
微軟-視窗-DotNETRuntime/線程池工人線程/開始- 登錄到 Linux 和 Windows 的新工作人員
Microsoft-Windows-DotNETRuntime/ThreadPoolWorker 編輯調整/調整- 指示正常工作人員調整(將顯示增加計數)
下面是您可能在 Linux 上看到的示例。

 

 

如果負載足夠高(因此對更多線程的需要足夠高),這將無限期地繼續。它還可能出現在計算給定進程中線程的操作系統性能指標中。(因此,如有必要,您可以在沒有 PerfView 的情況下進行操作)。

查找阻塞 API

如果您已經確定您確實有線程池飢餓問題,如前所述,可能的問題是您調用了占用線程的時間過長的阻塞 API。在窗口上,PerfView 可以通過"線程時間(具有啟動活動)"視圖顯示要阻止的地點。此視圖應顯示"活動"節點中的所有服務請求,如果您查看這些請求,應看到正在使用BLOCKED_TIME模式。導致阻止的 API 是問題所在。

不幸的是,此視圖目前在 Linux 上不可用,使用"perfCollect"。但是,應用程序見解分析器應該工作並顯示等效信息。在運行時的 2.2 版本中,"dotnet 配置文件"還應用於臨時集合 (TBD)。

積極主動。

並不是說您不需要等待服務融化,以發現代碼中的"壞阻塞"。沒有什么能阻止您在任何負載上運行 PerfView /Collect 在開發框中運行,只需查找阻止即可。這應該被修復。您不需要實際誘導大規模環境並看到線程池飢餓,您知道,如果您有大規模(1000 個並發請求),並且您阻止任何時間長度(即使 10 毫秒太多,如果每個請求都發生)。因此,您可以積極主動地解決問題,甚至在問題出現之前。

方法:強制線程池中有更多的線程

如前所述,線程池飢餓的真正解決方案是刪除該消耗線程的阻塞。但是,您可能無法修改代碼以輕松完成此工作,並且需要一些在短期內會有所幫助的東西。ThreadPool.SetMinThreads 可以設置 ThreadPool 的最小線程數(在窗口中有 I/O 線程池和所有其他工作的池,您必須從跟蹤中查看不斷創建的線程種類,以知道要設置哪一個線程)。也可以使用環境變量 COMPlus_ForceMinWorkerThreads 設置普通輔助線程最小值,但 I/O 線程池沒有環境變量(但該變量僅存在於 Windows 上)。通常,這是一個糟糕的解決方案,因為您可能需要許多線程(例如 1000 或更多),而且效率很低。它只能用作臨時差距措施。

總結:

現在您知道 .NET 線程池飢餓的基礎知識了。

當您的服務性能不佳且 CPU 未飽和時,需要查找。
主要症狀是線程數量不斷增加(線程池嘗試修復飢餓)
通過查看顯示線程池添加線程的 .NET 運行時事件,可以更明確地確定問題。
然后,您可以使用正常的"線程時間"視圖來找出請求期間阻止的是什么。
您可以積極主動,在大規模部署之前查找阻塞時間,並解決這些類型的可伸縮性問題。
刪除阻塞是最好的,但如果不可能,增加 ThreadPool 的大小將至少使服務在短期內運行。
你擁有它。可悲的是,這變得比我們想象的更為普遍。我們可能添加更好的診斷功能,使這個更容易找到,但這個博客條目有助於在一段時間。

引用:https://docs.microsoft.com/zh-cn/archive/blogs/vancem/diagnosing-net-core-threadpool-starvation-with-perfview-why-my-service-is-not-saturating-all-cores-or-seems-to-stall?WT.mc_id=DT-MVP-5003325


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM