如果获取无作用域的 DbContext,则 EF Core 内存泄漏

问题描述 投票:0回答:1

我的 ASP.NET Core 应用程序(.NET 7、EF Core 7、Npgsql PostgreSQL 7)有问题。

作为示例,我编写了一个简单的 Web 服务来重现该问题:

  1. 在循环中的控制器中,我们从 ServiceProvider 获取上下文,我们看到内存泄漏:

  2. 同样的事情,但是随着范围的创建 - 没有内存泄漏:

为什么会发生这种情况?创建范围是唯一正确的解决方案吗?

控制器:

[ApiController]
[Route("[controller]/[action]")]
public class TestController : ControllerBase
{
    private IServiceProvider ServiceProvider { get; }

    public TestController(IServiceProvider serviceProvider)
    {
        ServiceProvider = serviceProvider;
    }

    private const int IterationsCount = 20_000;

    [HttpGet]
    public async Task<IActionResult> Leak()
    {
        for (var i = 0; i < IterationsCount; i++)
        {
            using var context = ServiceProvider.GetRequiredService<TestContext>();
            var a = await context.TestEntitys.FirstOrDefaultAsync();
        }

        GC.Collect();

        return Ok("There is leak");
    }

    [HttpGet]
    public async Task<IActionResult> NoLeak()
    {
        for (var i = 0; i < IterationsCount; i++)
        {
            using var scope = ServiceProvider.CreateScope();
            using var context = scope.ServiceProvider.GetRequiredService<TestContext>();
            var a = await context.TestEntitys.FirstOrDefaultAsync();
        }

        GC.Collect();

        return Ok("There is NO leak");
    }
}

背景:

public class TestContext : DbContext
{
    public TestContext(DbContextOptions<TestContext> options) : base(options)
    {
    }

    public DbSet<TestEntity> TestEntitys { get; set; } = null!;
}

public class TestEntity
{
    public int Id { get; set; }
}

程序.cs:

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers();
builder.Services.AddSwaggerGen();

builder.Services.AddDbContext<TestContext>(opts =>
{
    var connectionString = builder.Configuration.GetConnectionString(typeof(TestContext).Name);
    opts.UseNpgsql(connectionString).UseSnakeCaseNamingConvention();
}, ServiceLifetime.Transient, ServiceLifetime.Singleton);

var app = builder.Build();

var context = app.Services.GetRequiredService<TestContext>();
await context.Database.MigrateAsync();

if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

app.MapControllers();

app.Run();

项目文件:

<Project Sdk="Microsoft.NET.Sdk.Web">

  <PropertyGroup>
    <TargetFramework>net7.0</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="EFCore.NamingConventions" Version="7.0.2" />
    <PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="7.0.7" />
    <PackageReference Include="Microsoft.EntityFrameworkCore" Version="7.0.15" />
    <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="7.0.15">
      <PrivateAssets>all</PrivateAssets>
      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
    </PackageReference>
    <PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="7.0.15">
      <PrivateAssets>all</PrivateAssets>
      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
    </PackageReference>
    <PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="7.0.4" />
    <PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0" />
  </ItemGroup>

</Project>
c# postgresql asp.net-core entity-framework-core memory-leaks
1个回答
0
投票

您的两个示例将具有完全不同的行为,因为您将 DbContext 的范围设定为 Transient。

在使用公共注入作用域的第一个示例中,如果您有 20,000 次迭代,您将实例化 20,000 个 DbContext 实例,每个实例都跟踪正在加载的实体。在收集请求范围之前,不会收集这些上下文。当您在循环中处置每个 DbContext 实例时,生命周期作用域仍然跟踪对 Transient 实例的引用,因此 GC 仅在作用域处置后才会释放它们。

在第二个示例中,您将在每次迭代中构造和处置内部作用域。这意味着在这种情况下,外部作用域正在跟踪每个内部作用域的引用,但在每次迭代中,都会创建、处置单个 DbContext 实例,并且可以在处置迭代本身内的作用域时收集该实例,从而释放任何引用。

如果切换到使用共享生命周期作用域而不是瞬态作用域,第一个示例的内存使用将更接近第二个示例。不过,行为上会有所不同,因为循环中对同一项目的请求将返回跟踪的实例,除非使用

AsNoTracking
进行查询或在 DbContext 上禁用跟踪。

© www.soinside.com 2019 - 2024. All rights reserved.