Log4j2 漏洞檢測工具清單
凡事都有其限度,對吧?汽車只能開這么快,進程只能使用這么多內(nèi)存,程序員只能喝這么多咖啡。我們的生產(chǎn)力受到資源的限制,我們有能力更好或更差地利用它們。盡可能接近其極限使用我們的每一種資源是我們的目標(biāo),我們希望使用我們的 CPU 和內(nèi)存的每一點,否則我們會為昂貴的機器多付錢。
然而,若是我們使用了過多的資源,我們就有可能導(dǎo)致性能問題、服務(wù)不可用問題和程序宕機底崩潰問題。軟件開發(fā)看似簡單,但一旦遇到性能問題,就會變得非常棘手,這就是我們今天要討論的內(nèi)容。
定義最佳基準(zhǔn)
讓我們嘗試描述我們的最佳應(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)建的?;蛘叩剿泄艿臋C器的確切限制。因此,您首先應(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ū)懭胂拗?。但是我們將在這篇文章中忽略這一點,樂觀地假設(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[1]完成。CPU 使用率的指標(biāo)是Process | % 處理器時間。內(nèi)存有幾個指標(biāo),但我建議查看Process | 私有字節(jié)。您可能還對.NET CLR 內(nèi)存感興趣 | # 代表托管內(nèi)存的所有堆中的字節(jié)(CLR 占用的部分,而不是所有內(nèi)存,即托管 + 本機內(nèi)存)。
要查看性能計數(shù)器,您可以在 Windows 計算機上使用Process Explorer[2]或 PerfMon,或者在 .NET Core 服務(wù)器上使用dotnet-counters 。[3]如果您的應(yīng)用程序部署在云中,您可以使用像Application Insights[4](Azure Monitor[5]的一部分)這樣的 APM 工具來顯示這些信息?;蛘?,您可以在代碼中獲取性能計數(shù)器值并每 10 秒左右記錄一次,使用Azure 數(shù)據(jù)資源管理器[6]之類的工具在圖表中顯示數(shù)據(jù)。
提示:檢查機器級指標(biāo)和進程級指標(biāo)。您可能會發(fā)現(xiàn)其他進程正在限制您的性能。
一旦確定了哪些資源限制了您的 .NET 服務(wù)器,就該優(yōu)化該資源消耗了。如果您受 CPU 限制,讓我們減少 CPU 使用率。如果您受內(nèi)存限制,讓我們減少內(nèi)存使用量。
至少如果您在云中運行,一種簡單的方法是更改機器規(guī)格。如果您受內(nèi)存限制,請增加內(nèi)存。如果您受 CPU 限制,請增加內(nèi)核數(shù)量或獲得更快的 CPU。這將提高成本,但在此之前,您可以檢查一些容易實現(xiàn)的目標(biāo),以優(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)儲文件[7]并使用內(nèi)存分析器[8]或WinDbg[9]打開它。
- 使用新的GC 轉(zhuǎn)儲[10](.NET Core 3.1+) 并使用 Visual Studio 進行調(diào)查。
- 捕獲堆快照并使用內(nèi)存分析器[11]、PerfView[12]或Visual Studio 診斷工具[13]對其進行探索。
此分析將顯示哪些對象占用了您的大部分內(nèi)存。如果你發(fā)現(xiàn)它被采取了MyProgram.CustomerData那就更好了。但通常,最大的對象類型是string、byte[]或byte[][]。由于應(yīng)用程序中的幾乎所有內(nèi)容都可以使用這些類型,因此您需要找到引用它們的人。為此,查看所占用的包容性內(nèi)存(又名保留內(nèi)存)很重要。這個指標(biāo)不僅包括對象本身占用的內(nèi)存,還包括它引用的對象占用的內(nèi)存。例如,您可能會發(fā)現(xiàn)它MyProgram.Inventory.Item本身并不占用太多內(nèi)存,但它引用了一個byte[]它保存內(nèi)存中的圖像并占用高達 70% 的內(nèi)存。上面描述的所有工具都可以顯示包含最多字節(jié)的對象和到 GC 根的引用路徑(也就是到根的最短路徑[14])。
2. 了解誰把內(nèi)存放在了哪里
找出誰引用了最大的內(nèi)存塊很棒,但這可能還不夠。有時您需要知道這些內(nèi)存是如何分配的。您可能從引用路徑中知道,一些占用大部分內(nèi)存的對象位于緩存中,但誰將它們放在那里?來自單個時間點的內(nèi)存快照無法提供該答案。為此,您需要分配堆棧跟蹤。分析器使您能夠記錄您的應(yīng)用程序并在每次分配時保存調(diào)用堆棧。例如,您可能會發(fā)現(xiàn)創(chuàng)建有問題MyProgram.Inventory.Item對象的流程將它們分配到調(diào)用堆棧App.OnShowHistoryClicked | App.SeeItemHistory | App.GetItemFromDatabase中。
要獲得分配堆棧,您可以:
- 使用商業(yè)內(nèi)存分析器來顯示分配[15]。
- 使用 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ā)生這種情況的原因[16]有很多。
要了解您是否有嚴(yán)重的內(nèi)存泄漏,請查看一段時間內(nèi)的內(nèi)存消耗圖表(進程 | 私有字節(jié)計數(shù)器)。如果內(nèi)存一直在增加,而沒有偏離某個水平,則可能存在內(nèi)存泄漏。
使用內(nèi)存分析器調(diào)試泄漏[17]相當(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[18]對此有一篇很棒的文章。
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 壓力[19]。
以下是一些優(yōu)化內(nèi)存緩存的想法:
- 使用.NET 中的現(xiàn)有緩存實現(xiàn)[20]可以輕松創(chuàng)建失效策略。
- 考慮為某些事情選擇不緩存。您可能會用 CPU 或 IO 換取內(nèi)存,但是當(dāng)您受到內(nèi)存限制時,您應(yīng)該這樣做。
- 考慮使用內(nèi)存不足緩存。這可能是將數(shù)據(jù)保存在文件或本地數(shù)據(jù)庫中?;蛘呤褂孟馬edis[21]這樣的分布式緩存解決方案。
6.定期調(diào)用GC.Collect()
這條建議是違反直覺的,因為最好的做法是永遠不要調(diào)用GC.Collect(). 垃圾收集器很聰明,它應(yīng)該自己知道何時觸發(fā)收集。但問題是垃圾收集器只考慮自己的進程。如果它沒有足夠的內(nèi)存,它會小心觸發(fā)收集并騰出空間。但如果它確實有足夠的內(nèi)存,GC 會非常樂意忍受過多的內(nèi)存消耗。因此,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[22]、dotTrace[23]、ANTS perf profiler[24],或在您的開發(fā)計算機上使用 Visual Studio 。[25]
- 在生產(chǎn)環(huán)境中,最簡單的分析方法是使用應(yīng)用程序性能監(jiān)控 (APM) 工具,例如Azure Application Insights profiler[26]或RayGun[27]。
- 您可以通過將代理復(fù)制到生產(chǎn)機器并記錄快照來分析沒有 APM 的生產(chǎn)環(huán)境。使用 PerfView,您應(yīng)該復(fù)制整個程序。它結(jié)構(gòu)緊湊,無需安裝。使用 dotTrace,您可以復(fù)制允許在生產(chǎn)中記錄快照的輕量級代理。[28]
- 在 .NET Core 3.0+ 應(yīng)用程序中,您可以安裝 .NET Core 3.0 SDK 并使用 dotnet-trace 命令行工具記錄快照[29],然后使用 PerfView 將其復(fù)制到開發(fā)機器并進行分析。
2.檢查垃圾收集器的使用情況
我想說優(yōu)化 .NET CPU 使用最重要的一點是正確的內(nèi)存管理。在這方面要問的重要問題是:“垃圾收集浪費了多少 CPU?”。GC 的工作方式是在收集期間,您的執(zhí)行線程被凍結(jié)。這意味著垃圾收集直接影響性能。因此,如果您受 CPU 限制,我建議您檢查的第一件事是性能計數(shù)器[30]。NET CLR 內(nèi)存 | % GC 時間。
我不能給你一個指示問題的神奇數(shù)字,但根據(jù)經(jīng)驗,當(dāng)這個值超過 20% 時,你可能會遇到問題。如果超過 40%,那么你肯定有問題。如此高的百分比表明 GC 壓力,并且有辦法處理它[31]。
3.使用數(shù)組和對象池來重用內(nèi)存
陣列的分配和不可避免的解除分配可能非常昂貴。高頻率執(zhí)行這些分配會造成 GC 壓力并消耗大量 CPU 時間。解決這個問題的一個好方法是使用內(nèi)置的ArrayPoolObjectPool ([32]僅限 .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ù)器模式
我們已經(jīng)討論過轉(zhuǎn)移到GC 工作站模式[33]以節(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 峰值時自動停止收集[34]并進行挖掘。您可能會對結(jié)果感到驚訝。
總結(jié)
在我看來,從自上而下的層面處理大規(guī)模的性能問題是令人著迷的。您可能有一個團隊花費數(shù)月時間優(yōu)化一段代碼,相比之下,資源分配的簡單更改將產(chǎn)生更大的影響。而且,如果您的業(yè)務(wù)足夠大,那么這個微小的變化就會轉(zhuǎn)化為一大筆錢。你記得在你的合同中要求一個傭金條款嗎?無論如何,我希望這篇文章對你有用,如果你發(fā)現(xiàn)了,你可能會對我的書Practical Debugging for .NET 開發(fā)人員[35]感興趣,我在其中深入討論了性能和內(nèi)存問題的故障排除。
References
[1] Performance Counters: https://michaelscodingspot.com/performance-counters/
[2] Process Explorer: https://docs.microsoft.com/en-us/sysinternals/downloads/process-explorer
[3] dotnet-counters 。: https://docs.microsoft.com/en-us/dotnet/core/diagnostics/dotnet-counters
[4] Application Insights: https://docs.microsoft.com/en-us/azure/cloud-services/diagnostics-performance-counters#application-insights
[5] Azure Monitor: https://azure.microsoft.com/en-us/services/monitor/#overview
[6] Azure 數(shù)據(jù)資源管理器: https://azure.microsoft.com/en-us/services/data-explorer/#getting-started
[7] 捕獲轉(zhuǎn)儲文件: https://michaelscodingspot.com/how-to-create-use-and-debug-net-application-crash-dumps-in-2019/
[8] 內(nèi)存分析器: https://memprofiler.com/online-docs/manual/importmemorydumpfiles.html
[9] WinDbg: https://michaelscodingspot.com/how-to-create-use-and-debug-net-application-crash-dumps-in-2019/#Investigate-Dumps-with-WinDbg
[10] GC 轉(zhuǎn)儲: https://devblogs.microsoft.com/dotnet/collecting-and-analyzing-memory-dumps/
[11] 內(nèi)存分析器: https://michaelscodingspot.com/memory-profilers-principles#snapshots
[12] PerfView: https://bennettadelson.wordpress.com/2013/04/11/using-perfview-to-diagnose-a-net-memory-leak-2/
[13] Visual Studio 診斷工具: https://docs.microsoft.com/en-us/visualstudio/profiling/memory-usage?view=vs-2022
[14] 到根的最短路徑: https://www.jetbrains.com/help/dotmemory/Shortest_Paths_to_Roots.html
[15] 內(nèi)存分析器來顯示分配: https://www.jetbrains.com/help/dotmemory/Analyze_Memory_Allocation.html#types
[16] 發(fā)生這種情況的原因: https://michaelscodingspot.com/ways-to-cause-memory-leaks-in-dotnet/
[17] 使用內(nèi)存分析器調(diào)試泄漏: https://michaelscodingspot.com/find-fix-and-avoid-memory-leaks-in-c-net-8-best-practices/#profiler
[18] Sergey Tepliakov: https://devblogs.microsoft.com/premier-developer/understanding-different-gc-modes-with-concurrency-visualizer/
[19] GC 壓力: https://michaelscodingspot.com/avoid-gc-pressure/
[20] .NET 中的現(xiàn)有緩存實現(xiàn): https://michaelscodingspot.com/cache-implementations-in-csharp-net/
[21] Redis: https://redis.io/
[22] PerfView: https://github.com/microsoft/perfview
[23] dotTrace: https://www.jetbrains.com/profiler/
[24] ANTS perf profiler: https://www.red-gate.com/products/dotnet-development/ants-performance-profiler/
[25] 使用 Visual Studio 。: https://docs.microsoft.com/en-us/visualstudio/profiling/beginners-guide-to-performance-profiling?view=vs-2022
[26] Azure Application Insights profiler: https://docs.microsoft.com/en-us/azure/azure-monitor/app/profiler-overview
[27] RayGun: https://raygun.com/for/dotnet-performance-monitoring
[28] 的輕量級代理。: https://blog.jetbrains.com/dotnet/2012/09/10/dottrace-remote-profiling/
[29] 使用 dotnet-trace 命令行工具記錄快照: https://michaelscodingspot.com/dotnet-trace/
[30] 性能計數(shù)器: https://michaelscodingspot.com/performance-counters/
[31] 辦法處理它: https://michaelscodingspot.com/avoid-gc-pressure/
[32] (: https://docs.microsoft.com/en-us/aspnet/core/performance/objectpool?view=aspnetcore-6.0
[33] GC 工作站模式: https://docs.microsoft.com/en-us/dotnet/standard/garbage-collection/workstation-server-gc
[34] 在達到某個 CPU 峰值時自動停止收集: https://www.drware.com/perfview-command-for-capturing-automated-high-cpu-dumps/
[35] Practical Debugging for .NET 開發(fā)人員: https://practicaldebugging.net/