深入研究EF Core AddDbContext 引起的内存泄露的原因
1 var services = new ServiceCollection(); 2 //方式一 AddDbContext注册方式,会引起内存泄露 3 //services.AddDbContext<EFCoreDbContext>(options => options.UseSqlServer("connectionString")); 4 5 //方式二 使用AddScoped模拟AddDbContext注册方式,new EFCoreDbContext()时参数由DI提供,会引起内存泄露 6 services.AddMemoryCache(); // 手动高亮点1 7 8 Action<DbContextOptionsBuilder> optionsAction = o => o.UseSqlServer("connectionString"); 9 Action<IServiceProvider, DbContextOptionsBuilder> optionsAction2 = (sp, o) => optionsAction.Invoke(o); 10 11 services.TryAdd(new ServiceDescriptor(typeof(DbContextOptions<EFCoreDbContext>), 12 p => DbContextOptionsFactory<EFCoreDbContext>(p, optionsAction2), 13 ServiceLifetime.Scoped)); 14 services.Add(new ServiceDescriptor(typeof(DbContextOptions), 15 p => p.GetRequiredService<DbContextOptions<EFCoreDbContext>>(), 16 ServiceLifetime.Scoped)); 17 18 services.AddScoped(s => new EFCoreDbContext(s.GetRequiredService<DbContextOptions<EFCoreDbContext>>())); 19 20 //方式三 直接使用AddScoped,new EFCoreDbContext()时参数自己提供。不会引起内存泄露 21 //var options = new DbContextOptionsBuilder<EFCoreDbContext>() 22 // .UseSqlServer("connectionString") 23 // .Options; 24 //services.AddScoped(s => new EFCoreDbContext(options)); 25 26 //为了排除干扰,去掉静态ServiceLocator 27 //ServiceLocator.Init(services); 28 //for (int i = 0; i < 1000; i++) 29 //{ 30 // var test = new TestUserCase(); 31 // test.InvokeMethod(); 32 //} 33 34 //去掉静态ServiceLocator后的代码 35 var rootServiceProvider = services.BuildServiceProvider(); // 这一句放在循环外就可避免内存泄露,挪到循环内就会内存泄露 36 for (int i = 0; i < 1000; i++) 37 { 38 using (var test = new TestUserCase(rootServiceProvider)) 39 { 40 test.InvokeMethod(); 41 } 42 }
二、上一步中引用的DbContextOptionsFactory<T>方法,放到Main方法后面即可
private static DbContextOptions<TContext> DbContextOptionsFactory<TContext>(IServiceProvider applicationServiceProvider, Action<IServiceProvider, DbContextOptionsBuilder> optionsAction) where TContext : DbContext { var builder = new DbContextOptionsBuilder<TContext>( new DbContextOptions<TContext>(new Dictionary<Type, IDbContextOptionsExtension>())); builder.UseApplicationServiceProvider(applicationServiceProvider); // 手动高亮点2 optionsAction?.Invoke(applicationServiceProvider, builder); return builder.Options; }
三、EFCoreDbContext也做一些更改,不需要重写OnConfiguring方法,构造方法参数类型改为DbContextOptions<EFCoreDbContext>
public class EFCoreDbContext : DbContext { public EFCoreDbContext(DbContextOptions<EFCoreDbContext> options) : base(options) { } public DbSet<TestA> TestA { get; set; } }
services.AddMemoryCache()
和
builder.UseApplicationServiceProvider(applicationServiceProvider);
他的原话是“我测试过,Asp.net core并没有这个问题,EF6.x和EF core1.0也没这个问题,只有.net core console + EF core2.0会出现内存泄露。
经过测试是Microsoft.Extensions.DependencyInjection1.0升级到Microsoft.Extensions.DependencyInjection2.0造成的,只在console出现。”
这句话中的Asp.net core没有这个问题是有误导的,经测试,这个问题在ASP.NET Core中照样是有的,只不过平时大家在使用ASP.NET Core使用DI时一般都是直接获取IServiceProvider的实例,而不会直接用到ServiceCollection,更不会循环多次调用BuildServiceProvider。
就算在ASP.NET Core内部使用了ServiceCollection,一般也是用户自己新创建的,和Startup.ConfigureServices(IServiceCollection services)内的services没有关系。
而EF Core的注册到一般也是注册到services的,所以用户自己创建的ServiceCollection也就和EF Core扯不上关系,更不会引起EF Core内存泄露了。
关于,ASP.NET Core中复现内存泄露,我后面会给出测试代码。
因为微软在官方给出的使用依赖注入的建议其中有两项就是: 避免静态访问服务 应用程序代码中避免服务位置(ServiceLocator) 文档地址:https://docs.microsoft.com/zh-cn/aspnet/core/fundamentals/dependency-injection?view=aspnetcore-2.1
- 循环内多次调用BuildServiceProvider();
- services.AddMemoryCache()
- builder.UseApplicationServiceProvider(applicationServiceProvider);
//EF Core内部生成缓存key的代码 //代码位置:Microsoft.EntityFrameworkCore.Internal.ServiceProviderCache //所在方法:IServiceProvider GetOrAdd(IDbContextOptions options, bool providerRequired) var key = options.Extensions .OrderBy(e => e.GetType().Name) .Aggregate(0L, (t, e) => (t * 397) ^ ((long)e.GetType().GetHashCode() * 397) ^ e.GetServiceProviderHashCode());
可以看到方法签名的其中一个参数是IDbContextOptions类型,而且key也是用它计算的。
var key = options.Extensions .OrderBy(e => e.GetType().Name) .Aggregate(0L, (t, e) => (t * 397) ^ ((long)e.GetType().GetHashCode() * 397) ^ e.GetServiceProviderHashCode()); Console.WriteLine($"EF Core当前DbContextOptions实例生成的缓存key为:{key}");
果然,得到的结果是:内存泄露时每次打印到的key值都不一样,没有内存泄露时打印出来的都一样(测试代码快速切换内存泄露/没有内存泄露方法,将前面提到的 var rootServiceProvider = services.BuildServiceProvider() 这句移动到循环内/外即可)。
详细信息如下图:
- 内存泄露时(BuildServiceProvider语句位于循环内)
- 没有内存泄露时(BuildServiceProvider语句位于循环外)
public class EFCoreDbContext : DbContext { public EFCoreDbContext(DbContextOptions<EFCoreDbContext> options) : base(options) { //模拟生成EF Core 缓存key var key = options.Extensions .OrderBy(e => e.GetType().Name) .Aggregate(0L, (t, e) => (t * 397) ^ ((long)e.GetType().GetHashCode() * 397) ^ e.GetServiceProviderHashCode()); Console.WriteLine($"EF Core当前DbContextOptions实例生成的缓存key为:{key}"); //打印一下影响生成缓存key值的对象名、HashCore、自定义的ServiceProviderHashCode var oExtensions = options.Extensions.OrderBy(e => e.GetType().Name); Console.WriteLine($"打印引起key变化的IDbContextOptionsExtension实例列表"); foreach (var item in oExtensions) { Console.WriteLine($"item name:{item.GetType().Name} HashCode:{item.GetType().GetHashCode()} ServiceProviderHashCore:{item.GetServiceProviderHashCode()}"); } //从上一步打印结果来看,oExtensions内包含两个对象,SqlServerOptionsExtension和CoreOptionsExtension //SqlServerOptionsExtension的HashCode和ServiceProviderHashCode每次都一样,不是变化因素,不再跟踪 //CoreOptionsExtension 用来表示由EF Core 管理的选项,而不是由数据库提供商或扩展管理的选项。 //前面提到过的 builder.UseApplicationServiceProvider(applicationServiceProvider); //就是把当前使用的 ServiceProvider 赋值到 CoreOptionsExtension .ApplicationServiceProvider var coreOptionsExtension = options.FindExtension<CoreOptionsExtension>(); if (coreOptionsExtension != null) { var x = coreOptionsExtension; Console.WriteLine($"\n打印CoreOptionsExtension的一些HashCode\n" + $"GetServiceProviderHashCode:{x.GetServiceProviderHashCode()} \n" + $"HashCode:{x.GetHashCode()} \n" + $"ApplicationServiceProvider HashCode:{x.ApplicationServiceProvider?.GetHashCode()} \n" + $"InternalServiceProvider HashCode:{x.InternalServiceProvider?.GetHashCode()}"); //模拟GetServiceProviderHashCode的生成过程 var memoryCache = x.MemoryCache ?? x.ApplicationServiceProvider?.GetService<IMemoryCache>(); var loggerFactory = x.LoggerFactory ?? x.ApplicationServiceProvider?.GetService<ILoggerFactory>(); var isSensitiveDataLoggingEnabled = x.IsSensitiveDataLoggingEnabled; var warningsConfiguration = x.WarningsConfiguration; var hashCode = loggerFactory?.GetHashCode() ?? 0L; hashCode = (hashCode * 397) ^ (memoryCache?.GetHashCode() ?? 0L); hashCode = (hashCode * 397) ^ isSensitiveDataLoggingEnabled.GetHashCode(); hashCode = (hashCode * 397) ^ warningsConfiguration.GetServiceProviderHashCode(); if (x.ReplacedServices != null) { hashCode = x.ReplacedServices.Aggregate(hashCode, (t, e) => (t * 397) ^ e.Value.GetHashCode()); } Console.WriteLine($"\n模拟生成GetServiceProviderHashCode:{hashCode}"); if (x.GetServiceProviderHashCode() == hashCode) { Console.WriteLine($"模拟生成的GetServiceProviderHashCode和GetServiceProviderHashCode()获取的一致"); } //打印GetServiceProviderHashCode的生成步骤,对比差异 Console.WriteLine($"\n影响GetServiceProviderHashCode值的因素"); Console.WriteLine($"loggerFactory:{loggerFactory?.GetHashCode() ?? 0L}"); Console.WriteLine($"memoryCache:{memoryCache?.GetHashCode() ?? 0L}"); Console.WriteLine($"isSensitiveDataLoggingEnabled:{isSensitiveDataLoggingEnabled.GetHashCode()}"); Console.WriteLine($"warningsConfiguration:{warningsConfiguration.GetServiceProviderHashCode()}"); } } public DbSet<TestA> TestA { get; set; } }
View Code
再次运行项目,截图如下:
- 内存泄露时(BuildServiceProvider语句位于循环内)
第二次
第四次
- 没有内存泄露时(BuildServiceProvider语句位于循环外)
可以看到,虽然也有一些变化的地方,但变动的值没有参与计算key,只有上图我圈的部分才参与了key生成,所以缓存可以得到重用。
class Program { static void Main(string[] args) { var services = new ServiceCollection(); services.AddLogging(); //test 1 var options = new DbContextOptionsBuilder<EFCoreDbContext>() .UseSqlServer(Config.connectionString) .Options; ////test 2 模拟 AddDbContext //services.AddMemoryCache(/*c=> { c.ExpirationScanFrequency = new TimeSpan(0,0,5);c.CompactionPercentage = 1;c.SizeLimit = 20000; }*/); //Action<DbContextOptionsBuilder> optionsAction = o => o.UseSqlServer(Config.connectionString); //Action<IServiceProvider, DbContextOptionsBuilder> optionsAction2 = (sp, o) => optionsAction.Invoke(o); //services.TryAdd(new ServiceDescriptor(typeof(DbContextOptions<EFCoreDbContext>), p => //{ // Console.WriteLine($"正在从ServiceProvider[{p.GetHashCode().ToString()}]中获取/创建DbContextOptions<EFCoreDbContext>实例"); // //Console.ReadKey(); // return DbContextOptionsFactory<EFCoreDbContext>(p, optionsAction2); //}, ServiceLifetime.Scoped)); //services.Add(new ServiceDescriptor(typeof(DbContextOptions), p => p.GetRequiredService<DbContextOptions<EFCoreDbContext>>(), ServiceLifetime.Scoped)); //这两个注册方式二选一, 使用第一行表示启用test 1, 使用第二行表示启用test 2 //services.AddScoped(s => new EFCoreDbContext(options)); //services.AddScoped(s => new EFCoreDbContext(s.GetRequiredService<DbContextOptions<EFCoreDbContext>>())); services.AddScoped<IMemoryCacheTest, MemoryCacheTest>(); services.AddDbContext<EFCoreDbContext>((p, o) => { Console.WriteLine($"UseInternalServiceProvider[{p.GetHashCode().ToString()}]"); o.UseSqlServer(Config.connectionString); }); //services.AddEntityFrameworkSqlServer().AddDbContext<EFCoreDbContext>((p,o)=> { // Console.WriteLine($"UseInternalServiceProvider[{p.GetHashCode().ToString()}]"); // o.UseSqlServer(Config.connectionString).UseInternalServiceProvider(p); }); ILogger log; var rootServiceProvider = services.BuildServiceProvider(); for (int i = 0; i < 10; i++) { Console.WriteLine($"rootServiceProvider[{rootServiceProvider.GetHashCode().ToString()}]"); //log = rootServiceProvider.GetService<ILoggerFactory>().AddConsole().CreateLogger<Program>(); //log.LogInformation("日志输出正常"); using (var test = new TestUserCase(rootServiceProvider)) { test.InvokeMethod(); } } //rootServiceProvider.Dispose(); //rootServiceProvider = null; Console.WriteLine("执行完毕,请按任意键继续..."); Console.ReadKey(); } private static DbContextOptions<TContext> DbContextOptionsFactory<TContext>(IServiceProvider applicationServiceProvider, Action<IServiceProvider, DbContextOptionsBuilder> optionsAction) where TContext : DbContext { var builder = new DbContextOptionsBuilder<TContext>( new DbContextOptions<TContext>(new Dictionary<Type, IDbContextOptionsExtension>())); Console.WriteLine($"将ServiceProvider[{applicationServiceProvider.GetHashCode().ToString()}]设置为ApplicationServiceProvider"); //Console.ReadKey(); builder.UseApplicationServiceProvider(applicationServiceProvider); optionsAction?.Invoke(applicationServiceProvider, builder); return builder.Options; } } //调试时使用查看一下当前系统内的缓存状态 public interface IMemoryCacheTest { void Test(); } public class MemoryCacheTest : IMemoryCacheTest { private IMemoryCache _cache; public MemoryCacheTest(IMemoryCache memoryCache) { _cache = memoryCache; } public void Test() { var x = _cache.GetType(); } } public class TestUserCase : IDisposable { //private IServiceCollection services; private IServiceScope serviceScope; private IServiceProvider _serviceProvider; private EFCoreDbContext _context; public TestUserCase(/*IServiceCollection services,*/IServiceProvider serviceProvider) { //this.services = services; _serviceProvider = serviceProvider; } public void InvokeMethod() { //_serviceProvider = services.BuildServiceProvider(); using (serviceScope = _serviceProvider.GetRequiredService<IServiceScopeFactory>().CreateScope()) { var internalServiceProvider = serviceScope.ServiceProvider; Console.WriteLine($"获取一个新的ServiceProvider[{internalServiceProvider.GetHashCode().ToString()}]"); Console.WriteLine($"获取一个新的ServiceProviderType[{internalServiceProvider.GetType().GetHashCode().ToString()}]"); var memoryCache = internalServiceProvider?.GetService<IMemoryCache>(); var loggerFactory = internalServiceProvider?.GetService<ILoggerFactory>(); Console.WriteLine($"当前ServiceProvider.GetService<IMemoryCache>():{memoryCache.GetHashCode().ToString()}"); Console.WriteLine($"当前ServiceProvider.GetService<ILoggerFactory>():{loggerFactory.GetHashCode().ToString()}"); //using (_context = internalServiceProvider.GetRequiredService<EFCoreDbContext>()) //{ _context = internalServiceProvider.GetRequiredService<EFCoreDbContext>(); Printf(_serviceProvider, internalServiceProvider, _context, serviceScope); //} var cacheTest = _serviceProvider.GetRequiredService<IMemoryCacheTest>(); cacheTest.Test(); //(internalServiceProvider as IDisposable)?.Dispose(); //internalServiceProvider = null; } } public void Printf(IServiceProvider sp, IServiceProvider _serviceProvider, EFCoreDbContext _context, IServiceScope _serviceScope) { for (var i = 0; i < 100; i++) { var testA = _context.TestA.AsNoTracking().FirstOrDefault(); //_context.TestA.Add(new TestA() { Name = "test"}); //_context.SaveChanges(); Console.WriteLine($"RootSP:{sp.GetHashCode()} CurrentSP:{_serviceProvider.GetHashCode()} DbContext:{_context?.GetHashCode()} Index:{i}"); } } private bool disposed = false; public void Dispose() { Console.WriteLine($"{this.GetType().Name}.Dispose()..."); Dispose(true); GC.SuppressFinalize(this); } protected virtual void Dispose(bool disposing) { if (!this.disposed) { // Note disposing has been done. disposed = true; //serviceScope?.Dispose(); //serviceScope = null; //// ////(_serviceProvider as IDisposable)?.Dispose(); ////_serviceProvider = null; //_context?.Dispose(); //_context = null; } } } public class EFCoreDbContext : DbContext { public EFCoreDbContext(DbContextOptions<EFCoreDbContext> options) : base(options) { //模拟生成EF Core 缓存key var key = options.Extensions .OrderBy(e => e.GetType().Name) .Aggregate(0L, (t, e) => (t * 397) ^ ((long)e.GetType().GetHashCode() * 397) ^ e.GetServiceProviderHashCode()); Console.WriteLine($"EF Core当前DbContextOptions实例生成的缓存key为:{key}"); //打印影响生成缓存key值的对象名、HashCore、自定义的ServiceProviderHashCode var oExtensions = options.Extensions.OrderBy(e => e.GetType().Name); Console.WriteLine($"打印引起key变化的IDbContextOptionsExtension实例列表"); foreach (var item in oExtensions) { Console.WriteLine($"item name:{item.GetType().Name} HashCode:{item.GetType().GetHashCode()} ServiceProviderHashCore:{item.GetServiceProviderHashCode()}"); } //从上一步打印结果来看,oExtensions内包含两个对象,SqlServerOptionsExtension和CoreOptionsExtension //SqlServerOptionsExtension的HashCode和ServiceProviderHashCode每次都一样,不是变化因素,不再跟踪 //CoreOptionsExtension 用来表示由EF Core 管理的选项,而不是由数据库提供商或扩展管理的选项。 //上面的代码中 builder.UseApplicationServiceProvider(applicationServiceProvider); 这句就是把当前 ServiceProvider 设置到该类型实例的 ApplicationServiceProvider 属性 var coreOptionsExtension = options.FindExtension<CoreOptionsExtension>(); if (coreOptionsExtension != null) { var x = coreOptionsExtension; Console.WriteLine($"\n打印CoreOptionsExtension的一些HashCode\n" + $"GetServiceProviderHashCode:{x.GetServiceProviderHashCode()} \n" + $"HashCode:{x.GetHashCode()} \n" + $"ApplicationServiceProvider HashCode:{x.ApplicationServiceProvider?.GetHashCode()} \n" + $"InternalServiceProvider HashCode:{x.InternalServiceProvider?.GetHashCode()}"); //模拟GetServiceProviderHashCode的生成过程 var memoryCache = x.MemoryCache ?? x.ApplicationServiceProvider?.GetService<IMemoryCache>(); var loggerFactory = x.LoggerFactory ?? x.ApplicationServiceProvider?.GetService<ILoggerFactory>(); var isSensitiveDataLoggingEnabled = x.IsSensitiveDataLoggingEnabled; var warningsConfiguration = x.WarningsConfiguration; var hashCode = loggerFactory?.GetHashCode() ?? 0L; hashCode = (hashCode * 397) ^ (memoryCache?.GetHashCode() ?? 0L); hashCode = (hashCode * 397) ^ isSensitiveDataLoggingEnabled.GetHashCode(); hashCode = (hashCode * 397) ^ warningsConfiguration.GetServiceProviderHashCode(); if (x.ReplacedServices != null) { hashCode = x.ReplacedServices.Aggregate(hashCode, (t, e) => (t * 397) ^ e.Value.GetHashCode()); } Console.WriteLine($"\n模拟生成GetServiceProviderHashCode:{hashCode}"); if (x.GetServiceProviderHashCode() == hashCode) { Console.WriteLine($"模拟生成的GetServiceProviderHashCode和GetServiceProviderHashCode()获取的一致"); } //打印GetServiceProviderHashCode的生成步骤,对比差异 Console.WriteLine($"\n影响GetServiceProviderHashCode值的因素"); Console.WriteLine($"loggerFactory:{loggerFactory?.GetHashCode() ?? 0L}"); Console.WriteLine($"memoryCache:{memoryCache?.GetHashCode() ?? 0L}"); Console.WriteLine($"isSensitiveDataLoggingEnabled:{isSensitiveDataLoggingEnabled.GetHashCode()}"); Console.WriteLine($"warningsConfiguration:{warningsConfiguration.GetServiceProviderHashCode()}"); } } public DbSet<TestA> TestA { get; set; } } public class TestA { public long Id { get; set; } public string Name { get; set; } }
View Code