優(yōu)化.NET 應(yīng)用程序 CPU 和內(nèi)存的11 個實踐
前言
凡事都有其限度,對吧?汽車只能開這么快,進程只能使用這么多內(nèi)存,程序員只能喝這么多咖啡。我們的生產(chǎn)力受到資源的限制,我們有能力更好或更差地利用它們。盡可能接近其極限使用我們的每一種資源是我們的目標,我們希望使用我們的 CPU 和內(nèi)存的每一點,否則我們會為昂貴的機器多付錢。然而,若是我們使用了過多的資源,我們就有可能導(dǎo)致性能問題、服務(wù)不可用問題和程序宕機底崩潰問題。軟件開發(fā)看似簡單,但一旦遇到性能問題,就會變得非常棘手,這就是我們今天要討論的內(nèi)容。
定義最佳基準
讓我們嘗試描述我們的最佳應(yīng)用程序行為。假設(shè)我們有許多服務(wù)器機器需要處理高吞吐量的請求。為簡單起見,讓我們暫時忘記高峰時間或周末。我們的服務(wù)器負載在一天中的所有時間都或多或少相同。我們?yōu)檫@些服務(wù)器機器支付了很多錢,我們希望從它們那里獲得盡可能多的價值,這意味著處理盡可能多的請求。按照我們對簡單性的承諾,我們還假設(shè)服務(wù)器僅使用內(nèi)存和 CPU 來處理所述請求,并且沒有其他瓶頸,例如慢速網(wǎng)絡(luò)或鎖爭用。
在所描述的場景中,我們的最佳行為是在任何給定時間使用盡可能多的 CPU 和內(nèi)存,對嗎?這樣,我們可以用更少的機器來處理相同數(shù)量的請求。但是你可能不想利用這些資源中的 99.9%,因為負載的輕微增加可能會導(dǎo)致性能問題、服務(wù)器崩潰、數(shù)據(jù)丟失和其他令人頭疼的問題。所以我們應(yīng)該選擇一個有足夠緩沖問題的數(shù)值。平均 85% 或 90% 的 CPU 和內(nèi)存利用率聽起來是正確的。
我們應(yīng)該首先優(yōu)化什么?
我們的應(yīng)用程序不是為平等利用 CPU 和內(nèi)存而構(gòu)建的。或者到它托管的機器的確切限制。因此,你首先應(yīng)該查看的是你的服務(wù)器是CPU-bound還是Memory-bound。當(dāng)服務(wù)器受 CPU 限制時,這意味著服務(wù)器可以處理的吞吐量受到其 CPU 的限制。換句話說,如果你嘗試處理更多請求,CPU 將在其他資源(如內(nèi)存)達到其限制之前達到 100%。同樣的邏輯也適用于Memory-bound服務(wù)器。
服務(wù)器的吞吐量將受到它可以分配的內(nèi)存的限制,當(dāng)嘗試處理更多負載時,在其他資源(如 CPU)達到其限制之前,該內(nèi)存將達到 100%。還有其他資源可以限制服務(wù)器,例如I/O,在這種情況下,吞吐量會受到磁盤或網(wǎng)絡(luò)的讀取或?qū)懭胂拗啤?/span>但是我們將在這篇文章中忽略這一點,樂觀地假設(shè)我們的 I/O 是快速且無限的。一旦你知道是什么限制了你的服務(wù)器的性能,你就會知道首先要嘗試和優(yōu)化什么。
如果你的服務(wù)器受 CPU 限制,那么優(yōu)化內(nèi)存使用沒有意義,因為它不會提高處理的吞吐量。事實上,它可能會損害吞吐量,因為你可能會因為更多的 CPU 利用率而提高內(nèi)存使用率。對于內(nèi)存受限的服務(wù)器也是如此,在這種情況下,你應(yīng)該在查看 CPU 之前優(yōu)化內(nèi)存使用。
測量 .NET 服務(wù)器中的 CPU 和內(nèi)存消耗
CPU 和內(nèi)存的實際測量最簡單的是使用Performance Counters完成。CPU 使用率的指標是Process | % 處理器時間。內(nèi)存有幾個指標,但我建議查看Process | 私有字節(jié)。你可能還對**.NET CLR 內(nèi)存感興趣 | # 代表托管內(nèi)存的所有堆中的字節(jié)**(CLR 占用的部分,而不是所有內(nèi)存,即托管 + 本機內(nèi)存)。要查看性能計數(shù)器,你可以在 Windows 計算機上使用Process Explorer或 PerfMon,或者在 .NET Core 服務(wù)器上使用dotnet-counters 。如果你的應(yīng)用程序部署在云中,你可以使用像Application Insights(Azure Monitor的一部分)這樣的 APM 工具來顯示這些信息。或者,你可以在代碼中獲取性能計數(shù)器值并每 10 秒左右記錄一次,使用Azure 數(shù)據(jù)資源管理器之類的工具在圖表中顯示數(shù)據(jù)。
一旦確定了哪些資源限制了你的 .NET 服務(wù)器,就該優(yōu)化該資源消耗了。如果你受 CPU 限制,讓我們減少 CPU 使用率。如果你受內(nèi)存限制,讓我們減少內(nèi)存使用量。至少如果你在云中運行,一種簡單的方法是更改機器規(guī)格。如果你受內(nèi)存限制,請增加內(nèi)存。如果你受 CPU 限制,請增加內(nèi)核數(shù)量或獲得更快的 CPU。這將提高成本,但在此之前,你可以檢查一些容易實現(xiàn)的目標,以優(yōu)化 CPU 或內(nèi)存消耗。在更改機器規(guī)格之前嘗試進行這些優(yōu)化,因為優(yōu)化后一切都會改變。你可能會優(yōu)化 CPU 使用率并變得受內(nèi)存限制。然后優(yōu)化內(nèi)存使用并再次成為 CPU 密集型。因此,如果你想避免不得不不斷更改機器資源以適應(yīng)最新的優(yōu)化,最好把它留到最后。所以讓我們談?wù)勔恍﹥?nèi)存優(yōu)化。 優(yōu)化內(nèi)存使用 有很多方法可以優(yōu)化 .NET 中的內(nèi)存使用。深入討論它們需要一整本書,而且已經(jīng)有好幾本了。但我會盡量給你一些方向和想法。 1、了解什么占用了你的內(nèi)存 嘗試優(yōu)化內(nèi)存時,你應(yīng)該做的第一件事是了解全局。什么占用了大部分內(nèi)存?有哪些數(shù)據(jù)類型?它們分配在哪里?它們會在記憶中停留多久?有幾種工具可以獲取此信息:?捕獲轉(zhuǎn)儲文件并使用內(nèi)存分析器或WinDbg打開它。?使用新的GC 轉(zhuǎn)儲(.NET Core 3.1+) 并使用 Visual Studio 進行調(diào)查。?捕獲堆快照并使用內(nèi)存分析器、PerfView或Visual Studio 診斷工具對其進行探索。此分析將顯示哪些對象占用了你的大部分內(nèi)存。如果你發(fā)現(xiàn)它被采取了 2、了解誰把內(nèi)存放在了哪里 找出誰引用了最大的內(nèi)存塊很棒,但這可能還不夠。有時你需要知道這些內(nèi)存是如何分配的。你可能從引用路徑中知道,一些占用大部分內(nèi)存的對象位于緩存中,但誰將它們放在那里?來自單個時間點的內(nèi)存快照無法提供該答案。為此,你需要分配堆棧跟蹤。分析器使你能夠記錄你的應(yīng)用程序并在每次分配時保存調(diào)用堆棧。例如,你可能會發(fā)現(xiàn)創(chuàng)建有問題 ?使用 PerfView 的 GC Heap [] Stacks 之一 分配讓你全面了解占用大部分內(nèi)存的內(nèi)容以及它是如何產(chǎn)生的。一旦你知道了這一點,你就可以開始切割最大的塊并優(yōu)化它們以減少內(nèi)存使用。 3、檢查內(nèi)存泄漏 在 .NET 中導(dǎo)致內(nèi)存泄漏非常容易。有了足夠多的泄漏,內(nèi)存消耗會隨著時間的推移而增加,你會遇到各種各樣的問題。內(nèi)存瓶頸就是其中之一,但由于 GC 壓力,你最終也會遇到 CPU 問題。當(dāng)你不再需要對象但由于某種原因它們?nèi)匀槐灰貌⑶依占饔肋h不會釋放它們時,就會發(fā)生內(nèi)存泄漏。發(fā)生這種情況的原因有很多。要了解你是否有嚴重的內(nèi)存泄漏,請查看一段時間內(nèi)的內(nèi)存消耗圖表(進程 | 私有字節(jié)計數(shù)器)。如果內(nèi)存一直在增加,而沒有偏離某個水平,則可能存在內(nèi)存泄漏。 使用內(nèi)存分析器調(diào)試泄漏相當(dāng)簡單。 4、切換到 GC 工作站模式 .NET 中有幾種垃圾收集器模式。主要的兩種模式是Workstation GC和Server GC。Workstation GC 針對更短的 GC 暫停和更快的交互性進行了優(yōu)化,非常適合桌面應(yīng)用程序。服務(wù)器 GC 具有更長的 GC 暫停時間,并且針對更高的吞吐量進行了優(yōu)化。 在 Server GC 模式下,應(yīng)用程序可以在垃圾回收之間處理更多數(shù)據(jù)。服務(wù)器 GC 為每個 CPU 核心創(chuàng)建不同的托管堆。這意味著不同的 X 代內(nèi)存空間需要更長的時間才能填滿,因此內(nèi)存消耗會更高。你基本上是在用內(nèi)存換取吞吐量。從 GC 服務(wù)器模式(.NET 服務(wù)器的默認模式)更改為 GC 工作站模式將減少內(nèi)存使用量。這在請求負載不重的小型應(yīng)用程序中可能是合理的。也許在與主應(yīng)用程序一起運行的 IIS 主機中的輔助進程中。Sergey Tepliakov對此有一篇很棒的文章。 5、檢查你的緩存 在第 1 步之后,你應(yīng)該能夠看到哪些對象占用了你的內(nèi)存,但我想特別強調(diào)緩存。每當(dāng)涉及到高內(nèi)存消耗時,根據(jù)我的經(jīng)驗,它總是最終成為內(nèi)存泄漏或緩存。緩存似乎是許多問題的神奇解決方案。當(dāng)你可以將結(jié)果保存在內(nèi)存中并重新使用它時,為什么要執(zhí)行兩次?但是緩存是有代價的。一個簡單的實現(xiàn)會將對象永遠保存在內(nèi)存中。你應(yīng)該按時間限制或以其他方式使緩存無效。緩存還會將臨時對象留在內(nèi)存中相對較長的時間,這會導(dǎo)致更多的 Gen 1 和 Gen 2 收集,進而導(dǎo)致GC 壓力。以下是一些優(yōu)化內(nèi)存緩存的想法: ?使用.NET 中的現(xiàn)有緩存實現(xiàn)可以輕松創(chuàng)建失效策略。 ?考慮為某些事情選擇不緩存。你可能會用 CPU 或 IO 換取內(nèi)存,但是當(dāng)你受到內(nèi)存限制時,你應(yīng)該這樣做。 ?考慮使用內(nèi)存不足緩存。這可能是將數(shù)據(jù)保存在文件或本地數(shù)據(jù)庫中?;蛘呤褂孟馬edis這樣的分布式緩存解決方案。 6、定期調(diào)用GC.Collect() 這條建議是違反直覺的,因為最好的做法是永遠不要調(diào)用 因此,GC 的自私本性可能是生活在同一臺機器上的**其他進程的問題,可能托管在同一個 IIS 上。 這種多余的內(nèi)存可能會導(dǎo)致其他進程更快地達到它們的極限,或者導(dǎo)致它們各自的垃圾收集器更加努力地工作,因為它們可能錯誤地認為它們即將耗盡內(nèi)存。你可能會認為,如果其他進程的 GC 會達到認為我們內(nèi)存不足并因此更加努力地工作的程度,那么我們自己的進程也會這樣認為并觸發(fā)垃圾收集來解決問題。但我們不能做出這樣的假設(shè)。一方面,這些進程可能運行不同的 GC 實現(xiàn)版本(因為不同的 CLR 版本)。此外,你有不同的應(yīng)用程序行為可以使 GC 以不同的方式工作。例如,一個進程可能會以更高的速率分配內(nèi)存,因此 GC 將更快地開始“強調(diào)”可用內(nèi)存。底線是軟件很困難,當(dāng)你在一臺機器上有多個進程時,就像 IIS 一樣,你需要考慮到這一點,并可能采取一些不尋常的步驟。 優(yōu)化 CPU 使用率 硬幣的另一面是 CPU 使用率。一旦你發(fā)現(xiàn) CPU 是應(yīng)用程序吞吐量的瓶頸,就需要做很多事情。 1、分析你的應(yīng)用程序 優(yōu)化 CPU 的第一步是了解它。究竟是什么原因造成的?哪些方法負責(zé)?哪些請求是最大的 CPU 消耗者,哪些是流量?這一切都可以通過分析應(yīng)用程序來解決。分析允許你記錄執(zhí)行范圍并顯示所有被調(diào)用的方法以及它們在記錄期間使用了多少 CPU。分析器通常允許將這些結(jié)果視為普通列表、調(diào)用樹甚至火焰圖。這是 PerfView 中的簡單列表視圖: 這是相同場景的火焰圖: 你可以通過以下方式分析你的應(yīng)用: ?如果場景在本地重現(xiàn),請使用性能分析器,如PerfView、dotTrace、ANTS perf profiler,或在你的開發(fā)計算機上使用 Visual Studio 。 ?在生產(chǎn)環(huán)境中,最簡單的分析方法是使用應(yīng)用程序性能監(jiān)控 (APM) 工具,例如Azure Application Insights profiler或RayGun。 ?你可以通過將代理復(fù)制到生產(chǎn)機器并記錄快照來分析沒有 APM 的生產(chǎn)環(huán)境。使用 PerfView,你應(yīng)該復(fù)制整個程序。它結(jié)構(gòu)緊湊,無需安裝。使用 dotTrace,你可以復(fù)制允許在生產(chǎn)中記錄快照的輕量級代理。 ?在 .NET Core 3.0+ 應(yīng)用程序中,你可以安裝 .NET Core 3.0 SDK 并使用 dotnet-trace 命令行工具記錄快照,然后使用 PerfView 將其復(fù)制到開發(fā)機器并進行分析。 2、檢查垃圾收集器的使用情況 我想說優(yōu)化 .NET CPU 使用最重要的一點是正確的內(nèi)存管理。在這方面要問的重要問題是:“垃圾收集浪費了多少 CPU?”。GC 的工作方式是在收集期間,你的執(zhí)行線程被凍結(jié)。這意味著垃圾收集直接影響性能。因此,如果你受 CPU 限制,我建議你檢查的第一件事是性能計數(shù)器。NET CLR 內(nèi)存 | % GC 時間。我不能給你一個指示問題的神奇數(shù)字,但根據(jù)經(jīng)驗,當(dāng)這個值超過 20% 時,你可能會遇到問題。如果超過 40%,那么你肯定有問題。如此高的百分比表明 GC 壓力,并且有辦法處理它。 3、使用數(shù)組和對象池來重用內(nèi)存 陣列的分配和不可避免的解除分配可能非常昂貴。高頻率執(zhí)行這些分配會造成 GC 壓力并消耗大量 CPU 時間。解決這個問題的一個好方法是使用內(nèi)置的 我們已經(jīng)討論過轉(zhuǎn)移到GC 工作站模式以節(jié)省內(nèi)存。但如果你受 CPU 限制,請考慮切換到服務(wù)器模式以節(jié)省 CPU。權(quán)衡是服務(wù)器模式以更多內(nèi)存為代價允許更高的吞吐量。 因此,如果你保持相同的吞吐量,你最終將節(jié)省 CPU 時間,否則垃圾收集會花費這些時間。默認情況下,.NET 服務(wù)器很可能具有 GC 服務(wù)器模式,因此可能不需要此更改。但是可能有人之前將其更改為工作站模式,在這種情況下,你應(yīng)該小心將其更改回來,因為他們可能有充分的理由。 更改時,請務(wù)必監(jiān)控內(nèi)存消耗和 GC 中的 % Time。你可能想查看第 2 代回收率,但如果這個數(shù)字很高,它將反映在更高的 GC 時間百分比中。 5、檢查其他進程 當(dāng)試圖將你的服務(wù)器發(fā)揮到最佳極限時,你可能想要徹底了解它,這意味著不要放棄存在于你的進程之外的問題。很有可能其他進程不時消耗一堆CPU,并導(dǎo)致一段時間的性能下降。這些可能是你在 IIS 上部署的其他應(yīng)用程序、定期 Web 作業(yè)、由操作系統(tǒng)觸發(fā)的東西、防病毒程序或其他一千種東西。對此進行分析的一種方法是使用 PerfView 記錄整個系統(tǒng)中的 ETW 事件。PerfView 從所有進程中捕獲 CPU 堆棧。你可以以很小的性能開銷運行它很長時間。你可以在達到某個 CPU 峰值時自動停止收集并進行挖掘。你可能會對結(jié)果感到驚訝。 總結(jié) 在我看來,從自上而下的層面處理大規(guī)模的性能問題是令人著迷的。你可能有一個團隊花費數(shù)月時間優(yōu)化一段代碼,相比之下,資源分配的簡單更改將產(chǎn)生更大的影響。而且,如果你的業(yè)務(wù)足夠大,那么這個微小的變化就會轉(zhuǎn)化為一大筆錢。你記得在你的合同中要求一個傭金條款嗎?無論如何,我希望這篇文章對你有用。提示:檢查機器級指標和進程級指標。你可能會發(fā)現(xiàn)其他進程正在限制你的性能。
MyProgram.CustomerData
那就更好了。但通常,最大的對象類型是string
、byte[]
或byte[][]
。由于應(yīng)用程序中的幾乎所有內(nèi)容都可以使用這些類型,因此你需要找到引用它們的人。為此,查看所占用的包容性內(nèi)存(又名保留內(nèi)存)很重要。這個指標不僅包括對象本身占用的內(nèi)存,還包括它引用的對象占用的內(nèi)存。例如,你可能會發(fā)現(xiàn)它MyProgram.Inventory.Item
本身并不占用太多內(nèi)存,但它引用了一個byte[]
它保存內(nèi)存中的圖像并占用高達 70% 的內(nèi)存。上面描述的所有工具都可以顯示包含最多字節(jié)的對象和到 GC 根的引用路徑(也就是到根的最短路徑)。MyProgram.Inventory.Item
對象的流程將它們分配到調(diào)用堆棧App.OnShowHistoryClicked | App.SeeItemHistory | App.GetItemFromDatabase
中。要獲得分配堆棧,你可以:?使用商業(yè)內(nèi)存分析器來顯示分配。GC.Collect()
. 垃圾收集器很聰明,它應(yīng)該自己知道何時觸發(fā)收集。但問題是垃圾收集器只考慮自己的進程。如果它沒有足夠的內(nèi)存,它會小心觸發(fā)收集并騰出空間。但如果它確實有足夠的內(nèi)存,GC 會非常樂意忍受過多的內(nèi)存消耗。ArrayPool
ObjectPool (僅限 .NET Core)。這個想法很簡單。為數(shù)組或?qū)ο蠓峙湟粋€共享緩沖區(qū),然后在不分配和取消分配新內(nèi)存的情況下重復(fù)使用。這是一個簡單的使用示例ArrayPool
:public void Foo()
{
var pool = ArrayPool<int>.Shared;
int[] array = pool.Rent(ArraySize);// do stuf
pool.Return(array);
}4、切換到 GC 服務(wù)器模式