如何在.NET应用程序中分析CPU使用率过高的问题
原文來自互聯(lián)網(wǎng),由長(zhǎng)沙DotNET技術(shù)社區(qū)編譯。如譯文侵犯您的署名權(quán)或版權(quán),請(qǐng)聯(lián)系小編,小編將在24小時(shí)內(nèi)刪除。限于譯者的能力有限,個(gè)別語句翻譯略顯生硬,還請(qǐng)見諒。
作者:胡安·帕勃羅·希達(dá),JUAN PABLO SCIDA是一位軟件架構(gòu)師,在軟件開發(fā)方面擁有10多年的經(jīng)驗(yàn)。他是經(jīng)過認(rèn)證的.NET和Java開發(fā)人員。在過去的幾年中,他還熱衷于使用Node.js,MongoDB和Erlang。
原文來自:https://www.toptal.com/dot-net/hunting-high-cpu-usage-in-dot-net
軟件開發(fā)可能是一個(gè)非常復(fù)雜的過程。作為開發(fā)人員,我們需要考慮很多不同的變量。有些不在我們的控制之下,有些在實(shí)際代碼執(zhí)行時(shí)對(duì)我們來說是未知的,有些則由我們直接控制。?.NET開發(fā)人員[1]也毫不例外。
考慮到這樣的現(xiàn)實(shí)情況,當(dāng)我們?cè)谑芸丨h(huán)境中工作時(shí),事情通常會(huì)按計(jì)劃進(jìn)行。假設(shè)就是我們的開發(fā)機(jī)器或我們可以完全訪問的集成環(huán)境。我們可以使用工具來分析影響我們的代碼和軟件的不同變量。我們也不必處理服務(wù)器的繁重負(fù)載,也不必處理并發(fā)用戶嘗試同時(shí)執(zhí)行相同操作的情況。
在可描述和安全的情況下,我們的代碼通常可以正常工作,但是在生產(chǎn)環(huán)境下,如果處于過度負(fù)載或其他一些外部因素的影響,可能會(huì)發(fā)生意外問題。生產(chǎn)環(huán)境的軟件性能很難分析。在大多數(shù)情況下,我們必須在理論上處理潛在的問題:我們知道可能會(huì)發(fā)生問題,但無法測(cè)試。這就是為什么我們需要以我們所用語言的最佳實(shí)踐和文檔為基礎(chǔ)進(jìn)行開發(fā),并避免常見錯(cuò)誤[2]。
如前所述,當(dāng)軟件上線時(shí),可能會(huì)出錯(cuò),并且代碼可能會(huì)以我們未計(jì)劃的方式開始執(zhí)行。當(dāng)我們不得不處理問題而又無法調(diào)試或確定發(fā)生了什么情況時(shí),下我們?cè)撛趺崔k?
圖片如果某個(gè)進(jìn)程長(zhǎng)時(shí)間使用超過90%的CPU,則我們會(huì)遇到麻煩
在本文中,我們將分析基于Windows的服務(wù)器上. net web應(yīng)用程序的高CPU使用率的實(shí)際案例場(chǎng)景、涉及到的識(shí)別問題的過程,以及更重要的問題,為什么會(huì)出現(xiàn)這個(gè)問題以及我們?nèi)绾谓鉀Q它。
CPU使用率和內(nèi)存消耗是廣泛討論的主題。通常,很難確定某個(gè)特定進(jìn)程應(yīng)使用的資源(CPU,RAM,I / O)的正確數(shù)量以及持續(xù)的時(shí)間段。盡管可以肯定的是-如果某個(gè)進(jìn)程長(zhǎng)時(shí)間使用了超過90%的CPU,那么我們將特別麻煩,因?yàn)樵谶@種情況下服務(wù)器將無法處理任何其他請(qǐng)求。
這是否意味著流程本身存在問題?不必要。該過程可能需要更多的處理能力,或者正在處理大量數(shù)據(jù)。首先,我們唯一能做的就是嘗試確定發(fā)生這種情況的原因。
所有操作系統(tǒng)都有幾種不同的工具來監(jiān)視服務(wù)器中發(fā)生的事情。Windows服務(wù)器專門具有任務(wù)管理器Performance Monitor[3],在本例中,我們使用了New Relic Servers[4],它是監(jiān)視服務(wù)器的絕佳工具。
最初癥狀和問題分析
部署應(yīng)用程序后,在頭兩周的時(shí)間里,我們開始看到服務(wù)器的CPU使用率達(dá)到峰值,這使服務(wù)器無響應(yīng)。為了使其再次可用,我們必須重新啟動(dòng)它,并且該事件在該時(shí)間段內(nèi)發(fā)生了3次。如前所述,我們使用New Relic Servers作為服務(wù)器監(jiān)視器,它表明w3wp.exe在服務(wù)器崩潰時(shí),該進(jìn)程占用了94%的CPU。
Internet信息服務(wù)(IIS)工作進(jìn)程是Windows進(jìn)程(w3wp.exe),它運(yùn)行Web應(yīng)用程序,并負(fù)責(zé)處理發(fā)送到特定應(yīng)用程序池的Web服務(wù)器的請(qǐng)求。IIS服務(wù)器可能有多個(gè)應(yīng)用程序池(和幾個(gè)不同的w3wp.exe進(jìn)程),這些池可能會(huì)產(chǎn)生問題。根據(jù)該進(jìn)程具有的用戶(這在New Relic報(bào)告中顯示),我們確定問題出在我們的.NET C#Web表單舊版應(yīng)用程序。
.NET Framework與Windows調(diào)試工具緊密集成在一起,因此,我們要做的第一件事是查看事件查看器和應(yīng)用程序日志文件,以查找有關(guān)正在發(fā)生的事情的有用信息。無論我們是否在事件查看器中記錄了一些異常,它們都沒有提供足夠的數(shù)據(jù)來進(jìn)行分析。這就是為什么我們決定更進(jìn)一步并收集更多數(shù)據(jù)的原因,因此當(dāng)事件再次發(fā)生時(shí),我們將做好準(zhǔn)備。
數(shù)據(jù)采集
收集用戶模式進(jìn)程轉(zhuǎn)儲(chǔ)的最簡(jiǎn)單方法是使用Debug Diagnostic Tools v2.0[5]或僅使用DebugDiag。DebugDiag具有一組用于收集數(shù)據(jù)(DebugDiag集合)和分析數(shù)據(jù)(DebugDiag分析)的工具。
因此,讓我們開始定義使用調(diào)試診斷工具收集數(shù)據(jù)的規(guī)則:
1.打開DebugDiag集合,然后選擇Performance。
圖片2.選擇Performance Counters并單擊Next。3.點(diǎn)擊Add Perf Triggers。4.展開Processor(不是Process)對(duì)象,然后選擇% Processor Time。請(qǐng)注意,如果您使用的是Windows Server 2008 R2,并且具有64個(gè)以上的處理器,請(qǐng)選擇該P(yáng)rocessor Information對(duì)象而不是該P(yáng)rocessor對(duì)象。5.在實(shí)例列表中,選擇_Total。6.單擊Add,然后單擊確定OK。7.選擇新添加的觸發(fā)器,然后單擊確定Edit Thresholds。
圖片8.Above在下拉菜單中選擇。
9.將閾值更改為80。
10.輸入20秒數(shù)。
您可以根據(jù)需要調(diào)整該值,但請(qǐng)注意不要指定小數(shù)秒,以防止錯(cuò)誤觸發(fā)。
圖片11.點(diǎn)擊OK。
12.點(diǎn)擊Next。
13.點(diǎn)擊Add Dump Target。
14.Web Application Pool從下拉菜單中選擇。
15.從應(yīng)用程序池列表中選擇您的應(yīng)用程序池。
16.點(diǎn)擊OK。
17.點(diǎn)擊Next。
18.Next再點(diǎn)擊一次。
19.如果需要,請(qǐng)輸入規(guī)則名稱,并記下轉(zhuǎn)儲(chǔ)的保存位置。
您可以根據(jù)需要更改此位置。
20.點(diǎn)擊Next。
21.選擇Activate the Rule Now并單擊Finish。
描述的規(guī)則將創(chuàng)建一組小型轉(zhuǎn)儲(chǔ)文件,這些文件的大小將非常小。最終轉(zhuǎn)儲(chǔ)將是具有完整內(nèi)存的轉(zhuǎn)儲(chǔ),并且該轉(zhuǎn)儲(chǔ)會(huì)更大。現(xiàn)在,我們只需要等待高CPU事件再次發(fā)生即可。
將轉(zhuǎn)儲(chǔ)文件保存在所選文件夾中后,我們將使用DebugDiag Analysis工具來分析收集的數(shù)據(jù):
1.選擇性能分析器。
圖片2.添加轉(zhuǎn)儲(chǔ)文件。
圖片3.開始分析。
DebugDiag將花費(fèi)幾分鐘(或數(shù)分鐘)來解析轉(zhuǎn)儲(chǔ)并提供分析。完成分析后,您將看到一個(gè)網(wǎng)頁,其中包含摘要以及有關(guān)線程的大量信息,類似于以下內(nèi)容:
圖片正如您在摘要中看到的那樣,有一條警告說:“在一個(gè)或多個(gè)線程上檢測(cè)到轉(zhuǎn)儲(chǔ)文件之間的CPU使用率過高。” 如果單擊建議,我們將開始了解應(yīng)用程序存在問題的地方。我們的示例報(bào)告如下所示:
圖片正如我們?cè)趫?bào)告中看到的那樣,有一個(gè)關(guān)于CPU使用率的模式。所有CPU使用率高的線程都與同一類相關(guān)。在跳到代碼之前,讓我們看一下第一個(gè)。
圖片這是我們遇到的第一個(gè)線程的細(xì)節(jié)。對(duì)我們來說有趣的部分是:
圖片在這里,我們有一個(gè)代碼調(diào)用,GameHub.OnDisconnected()該代碼觸發(fā)了有問題的操作,但是在此調(diào)用之前,我們有兩個(gè)Dictionary調(diào)用,它們可以使您對(duì)發(fā)生的事情有所了解。讓我們看一下.NET代碼,看看該方法在做什么:
public override Task OnDisconnected() {
try{var userId = GetUserId();string connId;if(onlineSessions.TryGetValue(userId, out connId))onlineSessions.Remove(userId);}catch(Exception){// ignored}returnbase.OnDisconnected();}我們顯然在這里有問題。報(bào)告的調(diào)用堆棧說問題出在字典上,在這段代碼中我們正在訪問字典,特別是引起問題的那一行是:
if (onlineSessions.TryGetValue(userId, out connId))
這是字典聲明:
static Dictionary<int, string> onlineSessions = new Dictionary<int, string>();
.NET代碼有什么問題?
具有面向?qū)ο缶幊探?jīng)驗(yàn)的每個(gè)人都知道靜態(tài)變量將由此類的所有實(shí)例共享。讓我們更深入地了解.NET世界中靜態(tài)的含義。
根據(jù).NET C#規(guī)范:
使用static[6]修飾符聲明一個(gè)靜態(tài)成員,該成員屬于類型本身而不是特定對(duì)象。
這就是.NET C#語言規(guī)范關(guān)于靜態(tài)類和成員的說明[7]:
與所有類類型一樣,當(dāng)加載引用該類的程序時(shí),.NET Framework公共語言運(yùn)行庫(CLR)將加載靜態(tài)類的類型信息。程序無法確切指定何時(shí)加載類。但是,可以保證在程序中首次引用該類之前,將其加載并初始化其字段并調(diào)用其靜態(tài)構(gòu)造函數(shù)。靜態(tài)構(gòu)造函數(shù)僅被調(diào)用一次,并且靜態(tài)類在程序所在的應(yīng)用程序域的生存期內(nèi)保留在內(nèi)存中。非靜態(tài)類可以包含靜態(tài)方法,字段,屬性或事件。即使沒有創(chuàng)建該類的實(shí)例,該靜態(tài)成員也可以在該類上調(diào)用。始終通過類名稱而不是實(shí)例名稱訪問靜態(tài)成員。無論創(chuàng)建多少個(gè)類實(shí)例,靜態(tài)成員只有一個(gè)副本。靜態(tài)方法和屬性無法訪問其包含類型的非靜態(tài)字段和事件,并且除非在方法參數(shù)中顯式傳遞了實(shí)例變量,否則它們無法訪問任何對(duì)象的實(shí)例變量。
這意味著靜態(tài)成員屬于類型本身,而不是對(duì)象。它們也由CLR加載到應(yīng)用程序域中,因此靜態(tài)成員屬于承載應(yīng)用程序的進(jìn)程,而不是特定線程。
鑒于Web環(huán)境是多線程環(huán)境,因?yàn)槊總€(gè)請(qǐng)求都是由w3wp.exe進(jìn)程產(chǎn)生的新線程;考慮到靜態(tài)成員是該過程的一部分,我們可能會(huì)遇到以下情況:幾個(gè)不同的線程嘗試訪問靜態(tài)(由多個(gè)線程共享的)變量的數(shù)據(jù),這最終可能會(huì)導(dǎo)致多線程問題。
線程安全性下的Dictionary?文檔[8]聲明以下內(nèi)容:
Dictionary<TKey, TValue>只要不修改集合,A 就可以同時(shí)支持多個(gè)閱讀器。即使這樣,通過集合進(jìn)行枚舉本質(zhì)上也不是線程安全的過程。在極少的枚舉與寫訪問競(jìng)爭(zhēng)的情況下,必須在整個(gè)枚舉期間鎖定集合。要允許多個(gè)線程訪問該集合進(jìn)行讀寫,您必須實(shí)現(xiàn)自己的同步。
此聲明解釋了為什么我們可能會(huì)遇到此問題。根據(jù)轉(zhuǎn)儲(chǔ)信息,問題出在字典的FindEntry方法上:
圖片如果查看字典的FindEntry?實(shí)現(xiàn),[9]我們可以看到該方法遍歷內(nèi)部結(jié)構(gòu)(存儲(chǔ)桶)以查找值。
因此,以下.NET代碼枚舉了集合,這不是線程安全的操作。
publicoverrideTaskOnDisconnected() { try { var userId = GetUserId(); string connId; if(onlineSessions.TryGetValue(userId, out connId))onlineSessions.Remove(userId); } catch(Exception) { // ignored } returnbase.OnDisconnected(); }結(jié)論
正如我們?cè)谵D(zhuǎn)儲(chǔ)中看到的那樣,有多個(gè)線程試圖同時(shí)迭代和修改共享資源(靜態(tài)字典),最終導(dǎo)致迭代進(jìn)入無限循環(huán),從而導(dǎo)致線程消耗超過90%的CPU。。
有幾種可能的解決方案。我們首先實(shí)現(xiàn)的方法是鎖定和同步對(duì)字典的訪問,但會(huì)損失性能。那時(shí)服務(wù)器每天都崩潰,因此我們需要盡快解決此問題。即使這不是最佳解決方案,它也解決了該問題。
解決這個(gè)問題的下一步是分析代碼并找到最優(yōu)解決方案。重構(gòu)代碼是一個(gè)選項(xiàng):新的ConcurrentDictionary類可以解決這個(gè)問題,因?yàn)樗绘i定在一個(gè)桶級(jí)別,這將提高整體性能。盡管這是一大步,還需要進(jìn)一步的分析。
References
[1]?.NET開發(fā)人員:?https://www.toptal.com/dot-net
[2]?常見錯(cuò)誤:?https://www.toptal.com/c-sharp/top-10-mistakes-that-c-sharp-programmers-make
[3]?Performance Monitor:?https://technet.microsoft.com/en-us/library/cc749115.aspx
[4]?New Relic Servers:?http://newrelic.com/server-monitoring
[5]?Debug Diagnostic Tools v2.0:?https://www.microsoft.com/en-us/download/details.aspx?id=49924
[6]?static:?https://msdn.microsoft.com/en-us/library/98f28cdx.aspx
[7]?靜態(tài)類和成員的說明:?https://msdn.microsoft.com/en-us/library/79b3xss3.aspx
[8]?文檔:?https://msdn.microsoft.com/en-us/library/xfhwa508%28v=vs.100%29.aspx
[9]?實(shí)現(xiàn),:?http://referencesource.microsoft.com/#mscorlib/system/collections/generic/dictionary.cs,bcd13bb775d408f1
總結(jié)
以上是生活随笔為你收集整理的如何在.NET应用程序中分析CPU使用率过高的问题的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 【视频回放与课件】Build your
- 下一篇: 玩转控件:对Dev的GridContro