.net EF核心上下文导致内存泄漏

a8jjtwal  于 2022-11-26  发布在  .NET
关注(0)|答案(1)|浏览(180)

我的软件使用EFCore和ASP.NETCoreWebAPI中的SQLite数据库,使用依赖注入,有一个内存泄漏。
我有一个后台工作使用石英得到调用每9秒。
我的上下文如下所示:

public class TeslaSolarChargerContext : DbContext, ITeslaSolarChargerContext
{
    public DbSet<ChargePrice> ChargePrices { get; set; } = null!;
    public DbSet<HandledCharge> HandledCharges { get; set; } = null!;
    public DbSet<PowerDistribution> PowerDistributions { get; set; } = null!;

    public string DbPath { get; }

    public void RejectChanges()
    {
        foreach (var entry in ChangeTracker.Entries())
        {
            switch (entry.State)
            {
                case EntityState.Modified:
                case EntityState.Deleted:
                    entry.State = EntityState.Modified; //Revert changes made to deleted entity.
                    entry.State = EntityState.Unchanged;
                    break;

                case EntityState.Added:
                    entry.State = EntityState.Detached;
                    break;
            }
        }
    }

    public TeslaSolarChargerContext()
    {
    }

    public TeslaSolarChargerContext(DbContextOptions<TeslaSolarChargerContext> options)
        : base(options)
    {
    }
}

与接口

public interface ITeslaSolarChargerContext
{
    DbSet<ChargePrice> ChargePrices { get; set; }
    DbSet<HandledCharge> HandledCharges { get; set; }
    DbSet<PowerDistribution> PowerDistributions { get; set; }
    ChangeTracker ChangeTracker { get; }
    Task<int> SaveChangesAsync(CancellationToken cancellationToken = new CancellationToken());
    DatabaseFacade Database { get; }
    void RejectChanges();
}

在我的Program.cs中,我将上下文和Quartz作业添加到依赖注入中,如下所示:

builder.Services.AddDbContext<ITeslaSolarChargerContext, TeslaSolarChargerContext>((provider, options) =>
    {
        options.UseSqlite(provider.GetRequiredService<IDbConnectionStringHelper>().GetTeslaSolarChargerDbPath());
        options.EnableSensitiveDataLogging();
        options.EnableDetailedErrors();
    }, ServiceLifetime.Transient, ServiceLifetime.Transient)
    .AddTransient<IChargingCostService, ChargingCostService>();
builder.Services
    .AddSingleton<JobManager>()
    .AddTransient<PowerDistributionAddJob>()
    .AddTransient<IJobFactory, JobFactory>()
    .AddTransient<ISchedulerFactory, StdSchedulerFactory>();

我使用自己的JobManager,因为作业间隔可以通过各种方式进行配置,所以我将一个 Package 器注入到我的JobManager中,它是Singleton,因为我需要随时停止作业,因为作业间隔可以在运行时更新,所以我需要停止和启动作业:

public class JobManager
{
    private readonly ILogger<JobManager> _logger;
    private readonly IJobFactory _jobFactory;
    private readonly ISchedulerFactory _schedulerFactory;
    private readonly IConfigurationWrapper _configurationWrapper;

    private IScheduler _scheduler;

    
    public JobManager(ILogger<JobManager> logger, IJobFactory jobFactory, ISchedulerFactory schedulerFactory, IConfigurationWrapper configurationWrapper)
    {
        _logger = logger;
        _jobFactory = jobFactory;
        _schedulerFactory = schedulerFactory;
        _configurationWrapper = configurationWrapper;
    }

    public async Task StartJobs()
    {
        _logger.LogTrace("{Method}()", nameof(StartJobs));
        _scheduler = _schedulerFactory.GetScheduler().GetAwaiter().GetResult();
        _scheduler.JobFactory = _jobFactory;

        var powerDistributionAddJob = JobBuilder.Create<PowerDistributionAddJob>().Build();

        var powerDistributionAddTrigger = TriggerBuilder.Create()
            .WithSchedule(SimpleScheduleBuilder.RepeatSecondlyForever((int)_configurationWrapper.JobIntervall().TotalSeconds)).Build();

        var triggersAndJobs = new Dictionary<IJobDetail, IReadOnlyCollection<ITrigger>>
        {
            {powerDistributionAddJob, new HashSet<ITrigger> {powerDistributionAddTrigger}},
        };

        await _scheduler.ScheduleJobs(triggersAndJobs, false).ConfigureAwait(false);

        await _scheduler.Start().ConfigureAwait(false);
    }

    public async Task StopJobs()
    {
        await _scheduler.Shutdown(true).ConfigureAwait(false);
    }
}

作业如下所示:

[DisallowConcurrentExecution]
public class PowerDistributionAddJob : IJob
{
    private readonly ILogger<ChargeTimeUpdateJob> _logger;
    private readonly IChargingCostService _service;

    public PowerDistributionAddJob(ILogger<ChargeTimeUpdateJob> logger, IChargingCostService service)
    {
        _logger = logger;
        _service = service;
    }
    public async Task Execute(IJobExecutionContext context)
    {
        _logger.LogTrace("{method}({context})", nameof(Execute), context);
        await _service.AddPowerDistributionForAllChargingCars().ConfigureAwait(false);
    }
}

将上下文注入到服务中,如下所示:

public ChargingCostService(ILogger<ChargingCostService> logger,
        ITeslaSolarChargerContext teslaSolarChargerContext)
{
    _logger = logger;
    _teslaSolarChargerContext = teslaSolarChargerContext;
}

我在服务中使用上下文并调用此方法:

var chargePrice = await _teslaSolarChargerContext.ChargePrices
                                                 .FirstOrDefaultAsync().ConfigureAwait(false);

调用此命令会导致应用程序在一周后使用1GB RAM。
在分析内存转储后,我发现大约8小时后,我有超过2000个TeslaSolarChargerContext示例。

2skhul33

2skhul331#

您是否运行了任何内存探查器来确认泄漏的位置?
我创造了一个小的最低限度的复制:

using Microsoft.EntityFrameworkCore;
using Quartz;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddDbContext<MyContext>(ServiceLifetime.Transient, ServiceLifetime.Transient); // works even without changing the lifetime
builder.Services.AddQuartz((IServiceCollectionQuartzConfigurator quartzOptions) =>
{
    quartzOptions.UseMicrosoftDependencyInjectionJobFactory();
    quartzOptions.ScheduleJob<MyJob>(job => job.WithSimpleSchedule(x => x.WithIntervalInSeconds(3).RepeatForever()));
}).AddQuartzServer();

EnsureDb(builder.Services); // to make HasData work
var app = builder.Build();
app.Run();

void EnsureDb(IServiceCollection builderServices)
{
    using var scope = builderServices.BuildServiceProvider().CreateScope();
    var context = scope.ServiceProvider.GetRequiredService<MyContext>();
    context.Database.EnsureCreated();
}

class MyContext : DbContext
{
    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        base.OnConfiguring(optionsBuilder);
        optionsBuilder.UseInMemoryDatabase("products");
    }

    public DbSet<Product> Products
    {
        get;
        set;
    }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<Product>()
            .HasData(new Product(1, "Test"));

        base.OnModelCreating(modelBuilder);
    }

    public override void Dispose()
    {
        base.Dispose(); // put a breakpoint here
    }
}
record Product(int Id, string Name);

class MyJob : IJob, IDisposable
{
    private readonly MyContext _ctx;
    private readonly ILogger<MyJob> _logger;

    public MyJob(MyContext ctx, ILogger<MyJob> logger)
    {
        _ctx = ctx;
        _logger = logger;
        
        _logger.LogInformation("Creating job");
    }
    
    public async Task Execute(IJobExecutionContext context)
    {
        _logger.LogInformation("Product {Product}", await _ctx.Products.FirstOrDefaultAsync());
    }

    public void Dispose()
    {
        _ctx.Dispose(); // totally not needed, but you can try this approach
    }
}

首先,它完全起作用,这意味着Quartz正在为每个作业创建作用域- DbContext在每次运行作业时创建,并被正确处理。dotMemory证实了这一点。quartzOptions.UseMicrosoftDependencyInjectionJobFactory();是关键:https://www.quartz-scheduler.net/documentation/quartz-3.x/packages/microsoft-di-integration.html#installation
我注意到您将上下文注册为transient,可能是因为您发现无法将作用域依赖项注入到transient服务中。在DbContext.Dispose方法中放置一个断点,看看它是否被命中。如果没有,您在注册或保持示例时搞砸了一些事情-这就是您的漏洞。
容器应该处理你的暂时依赖,但是我真的不知道你是如何注册Quartz的--也许你在那里的某个地方配置错了。看看我的复制品,看看有什么不同。
编辑:
如果您确定问题是由DbContext引起的,则可以随时切换到注入DbContextFactory(docs):

// writen out of memory 

// in your startup
services.AddDbContextFactory<ApplicationDbContext>(
    options => 
    // your config
);

class YourService
{
    private readonly IDbContextFactory<ApplicationDbContext> _contextFactory;

    public YourService(IDbContextFactory<ApplicationDbContext> contextFactory)
    {
        _contextFactory = contextFactory;
    }

    public async Task Execute()
    {
        ChargePrice price;

        using (var ctx = _contextFactory.CreateDbContext())
        {
            price = await ctx.ChargePrices.FirstAsync();
        } // this will dispose the DbContext

        // continue
    }
}

相关问题