如何分析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 核心問題,並遇到了這個問題: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 的原始碼,所有問題在那裡所有人都可以看到,對有需求的開發者來說非常方便。
  • 簡單的代碼更改(在這種情況下為一行)可能會產生巨大的影響。
Keep Exploring

延伸阅读

更多文章