如何分析EFCore引發的記憶體洩漏

如何分析EFCore引發的記憶體洩漏

術語「記憶體洩漏」和「.NET應用程式」不是經常一起使用。

最後更新 2022/5/4 下午4:47
Richard Brown DotNET技术圈
預計閱讀 5 分鐘
分類
EF Core
標籤
.NET C# EF Core ORM

不要讓記憶體洩漏成為洪水

術語「記憶體洩漏」和「.NET 應用程式」不常一起使用。但是,我們最近在一個 .NET Core Web 應用程式中遇到了一系列記憶體不足的例外狀況。事實證明,此問題是由 Entity Framework Core 中的行為變更引起的,儘管最終的解決方案非常簡單,但實現該目標的過程既充滿挑戰又有趣。

該系統本身託管在 Azure 中,由 Angular SPA 前端和後端的 .NET Core API 組成,使用 Entity Framework Core 與 Azure SQL 資料庫進行通訊。作為專門從事 .NET 開發的軟體顧問公司,我們之前已經撰寫了許多類似的應用程式。因此,記憶體不足崩潰是無法預料的,所以我們立刻知道這是需要認真對待的事情。使用 Azure 入口網站中的指標,我們可以看到記憶體使用率穩定上升,然後突然下降:這個下降是應用程式崩潰。

修復之前

因此,我們花了一些時間進行調查並逐步進行變更,以解決看似典型的記憶體洩漏問題。.NET 洩漏的常見原因是未正確處理某些問題,在我們的案例中很可能是 EF Core 資料庫上下文。因此,我們遍歷了原始程式碼,以尋找可能無法處理上下文的潛在原因。但這並無收穫。

我們將 Entity Framework Core 升級到了最新版本,因為最近的更新包括各種記憶體洩漏的修復程式和整體效率的提高。

我們也在使用的 Application Insights 版本中發現了可能的記憶體洩漏(請參閱https://github.com/microsoft/ApplicationInsights-dotnet/issues/594),因此我們也對該套件進行了升級。

這些都無法解決問題,因此我們解剖了從 Azure 應用服務中取得的記憶體傾印(請參閱https://blogs.msdn.microsoft.com/jpsanders/2017/02/02/how-to-get-a-full-memory-dump-in-azure-app-services/)。

我們注意到,絕大多數的受控記憶體最終都由 MemoryCache 類別使用。進一步深入研究顯示,大多數快取資料都是原始 SQL 查詢的形式。我們看到大量根本上相同的查詢事件被多次快取,且參數本身被硬編碼在查詢中而不是被參數化。

例如,與其像這樣快取查詢:

SELECT TOP (1) UserId, FirstName, LastName, EmailAddress
FROM Users
WHERE UserId = @param_1

我們發現了這樣的多個查詢:

SELECT TOP (1) UserId, FirstName, LastName, EmailAddress
FROM Users
WHERE UserId = 5

因此,我們進行了一些搜尋,尋找可能與此相關的 EF Core 問題,並遇到了這個問題:https://github.com/aspnet/EntityFrameworkCore/issues/10535。

關於這個問題的主題指出了問題所在:我們正在建立一個動態運算式樹,並使用它 Expressions.Expression.Constant 來為 where 子句提供參數。使用常數運算式意味著 Entity Framework Core 不會參數化 SQL 查詢,並且是 Entity Framework 6 的行為變更。

我們到處都使用這個運算式樹,透過它的 ID 來獲取某些東西,這就是為什麼它是一個大問題。

因此,這就是我們所做的變更:

// Before
var param = Expressions.Expression.Parameter(typeof(T));
Expression = Expressions.Expression.Lambda<Func<T, bool>>(
  Expressions.Expression.Call(
    Expressions.Expression.Constant(valuesToFilter),
    "Contains",
    Type.EmptyTypes,
    Expressions.Expression.Property(param, propertyName)),
  param);
// After
var param = Expressions.Expression.Parameter(typeof(T));
// This is what we added
Expression<Func<List<int>>> valuesToFilterLambda = () => valuesToFilter;
Expression = Expressions.Expression.Lambda<Func<T, bool>>(
  Expressions.Expression.Call(
    valuesToFilterLambda.Body,
    "Contains",
    Type.EmptyTypes,
    Expressions.Expression.Property(param, propertyName)),
  param);

使用 lambda 運算式取得運算式主體會讓 Entity Framework Core 對 SQL 查詢進行參數化,因此只會快取它的一個實例。

這是包含修訂版本在內的一段時間內的記憶體使用情況。該版本以紅色標記,您可以看到差異很大。穩定的記憶體使用量從未超過 200MB,而不斷攀升至超過 1GB,然後發生崩潰。

修復後

最初進行調查時,真正的解決方案不是我們會注意的事情,但是透過檢查記憶體傾印並遵循證據,我們最終到達了那裡。

從這次調查中可以學到的教訓是:

  • 記憶體傾印不會說謊——如果記憶體洩漏,請先查看證據。
  • 微軟已經開放了 EF Core 的原始碼,所有問題在那裡所有人都可以看到,對有需求的開發者來說非常方便。
  • 簡單的程式碼變更(在這種情況下為一行)可能會產生巨大的影響。
繼續探索

延伸閱讀

更多文章