EF Core Context引发内存泄漏问题排查求助
问题背景
开发的ASP.NET Core Web API采用依赖注入,结合EF Core与SQLite数据库,存在内存泄漏问题。应用通过Quartz实现的后台任务每9秒执行一次,运行一周后内存占用达1GB,内存转储分析显示,运行约8小时后生成了两千多个TeslaSolarChargerContext实例。
DbContext定义
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) { } }
DbContext接口定义
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依赖注入配置
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实现
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); } }
Quartz任务实现
[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); } }
业务服务构造注入DbContext
public ChargingCostService(ILogger<ChargingCostService> logger, ITeslaSolarChargerContext teslaSolarChargerContext) { _logger = logger; _teslaSolarChargerContext = teslaSolarChargerContext; }
服务中EF Core调用代码
var chargePrice = await _teslaSolarChargerContext.ChargePrices .FirstOrDefaultAsync().ConfigureAwait(false);
内存泄漏原因分析及解决办法
核心问题:单例Scheduler持有Transient JobFactory导致依赖无法回收
Quartz的Scheduler被单例JobManager持有,而你将IJobFactory注册为Transient生命周期。当_scheduler.JobFactory = _jobFactory;执行后,单例Scheduler会长期持有这个Transient的JobFactory实例,导致JobFactory无法被GC回收。JobFactory创建的Job实例、Job依赖的ChargingCostService以及服务持有的DbContext,都会被这个被持有的JobFactory间接引用,最终造成大量DbContext实例堆积。
解决步骤
调整JobFactory生命周期为Singleton
修改Program.cs中的注册代码:builder.Services .AddSingleton<JobManager>() .AddTransient<PowerDistributionAddJob>() .AddSingleton<IJobFactory, JobFactory>() // 改为Singleton .AddTransient<ISchedulerFactory, StdSchedulerFactory>();使用服务范围创建Job实例
自定义JobFactory时,通过IServiceScopeFactory创建独立的服务范围,确保每次Job执行时的依赖都能在执行完毕后被释放:public class JobFactory : IJobFactory { private readonly IServiceScopeFactory _scopeFactory; public JobFactory(IServiceScopeFactory scopeFactory) { _scopeFactory = scopeFactory; } public IJob NewJob(TriggerFiredBundle bundle, IScheduler scheduler) { var scope = _scopeFactory.CreateScope(); try { var job = scope.ServiceProvider.GetRequiredService(bundle.JobDetail.JobType) as IJob; // 绑定scope到Job数据映射,后续通过IJobListener释放 scheduler.Context.Put($"JobScope_{bundle.JobDetail.Key}", scope); return job; } catch { scope.Dispose(); throw; } } public void ReturnJob(IJob job) { // 无需手动释放,由IJobListener处理 } }同时添加JobListener在执行完毕后释放scope:
public class JobScopeCleanupListener : IJobListener { public string Name => "JobScopeCleanupListener"; public Task JobExecutionVetoed(IJobExecutionContext context, CancellationToken cancellationToken) { CleanupScope(context); return Task.CompletedTask; } public Task JobToBeExecuted(IJobExecutionContext context, CancellationToken cancellationToken) { return Task.CompletedTask; } public Task JobWasExecuted(IJobExecutionContext context, JobExecutionException jobException, CancellationToken cancellationToken) { CleanupScope(context); return Task.CompletedTask; } private void CleanupScope(IJobExecutionContext context) { if (context.Scheduler.Context.TryRemove($"JobScope_{context.JobDetail.Key}", out var scopeObj) && scopeObj is IServiceScope scope) { scope.Dispose(); } } }在JobManager的StartJobs方法中注册Listener:
await _scheduler.ListenerManager.AddJobListener(new JobScopeCleanupListener()).ConfigureAwait(false);恢复DbContext的Scoped生命周期
EF Core推荐使用Scoped生命周期,修改AddDbContext的配置:builder.Services.AddDbContext<ITeslaSolarChargerContext, TeslaSolarChargerContext>((provider, options) => { options.UseSqlite(provider.GetRequiredService<IDbConnectionStringHelper>().GetTeslaSolarChargerDbPath()); options.EnableSensitiveDataLogging(); options.EnableDetailedErrors(); }, ServiceLifetime.Scoped, ServiceLifetime.Scoped) .AddScoped<IChargingCostService, ChargingCostService>();修正Job中的日志注入类型
将PowerDistributionAddJob的日志注入改为匹配自身类型:public PowerDistributionAddJob(ILogger<PowerDistributionAddJob> logger, IChargingCostService service) { _logger = logger; _service = service; }
内容的提问来源于stack exchange,提问作者Patrick

