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