【问题标题】:Memory management in Entity Framework CoreEntity Framework Core 中的内存管理
【发布时间】:2017-08-05 14:19:41
【问题描述】:

我试图请求大量数据,然后将其解析为报告。问题是我请求的数据有 2700 万行记录,每行有 6 个连接,当通过实体框架加载时会使用所有服务器 RAM。我已经实现了一个分页系统来将处理缓冲成更小的块,就像你对 IO 操作所做的那样。

我请求 10,000 条记录,将它们写入文件流(写入磁盘),我正尝试从内存中清除 10,000 条记录,因为它们不再需要。

我在垃圾收集数据库上下文时遇到了麻烦。我已经尝试处理对象,将引用清空,然后在下一批 10,000 条记录上创建新上下文。这似乎不起作用。 (这是 ef core 上的一位开发人员推荐的:https://github.com/aspnet/EntityFramework/issues/5473

我看到的唯一其他选择是使用原始 SQL 查询来实现我想要的。我正在尝试构建系统来处理任何请求大小,唯一的可变因素是生成报告所需的时间。我可以用 EF 上下文做些什么来摆脱加载的实体吗?

 private void ProcessReport(ZipArchive zip, int page, int pageSize)
        {
            using (var context = new DBContext(_contextOptions))
            {
                var batch = GetDataFromIndex(page, pageSize, context).ToArray();
                if (!batch.Any())
                {
                    return;
                }

                var file = zip.CreateEntry("file_" + page + ".csv");
                using (var entryStream = file.Open())
                using (var streamWriter = new StreamWriter(entryStream))
                {
                    foreach (var reading in batch)
                    {
                        try
                        {
                            streamWriter.WriteLine("write data from record here.")
                        }
                        catch (Exception e)
                        {
                            //handle error
                        }
                    }
                }
                batch = null;
            }
            ProcessReport(zip, page + 1, pageSize);
        }

private IEnumerable<Reading> GetDataFromIndex(int page, int pageSize, DBContext context)
        {

            var batches = (from rb in context.Reading.AsNoTracking()
                //Some joins
                select rb)
                .Skip((page - 1) * pageSize)
                .Take(pageSize);

                return batches
                    .Includes(x => x.Something)

        }

【问题讨论】:

  • “数据”是什么意思?如果您对某种 DTO 对象使用投影查询,或者不使用跟踪查询,DbContext 将不会在内部存储任何内容。也不要使用ToListToArray等。只需枚举结果即可。
  • 我已使用 .AsNoTracking() 关闭查询的更改跟踪。我还删除了ToArray(),但垃圾收集器仍然没有释放任何上下文。 prntscr.com/g4q0h3 至于我所说的数据,我的意思是我希望将记录从数据库中获取到 C# 模型中,以便临时使用然后处理掉。我有一种感觉,由于递归循环,对象没有从内存中删除?
  • 不要使用 EF Core,这不是合适的情况。使用原始查询,您始终可以读取数据的子集,即使用数据阅读器

标签: c# entity-framework asp.net-core


【解决方案1】:

除了您的内存管理问题之外,您将很难为此使用分页。在服务器上运行分页查询会变得很昂贵。你不需要翻页。只需迭代查询结果(即不要调用 ToList() 或 ToArray())。

另外,当分页时,您必须为查询添加排序,否则 SQL 可能会返回重叠的行,或者有间隙。请参阅 SQL Server,例如:https://docs.microsoft.com/en-us/sql/t-sql/queries/select-order-by-clause-transact-sql EF Core 不强制执行此操作,因为某些提供程序可能会保证分页查询始终以相同的顺序读取行。

以下是 EF Core(.NET Core 上的 1.1)在不增加内存使用量的情况下通过庞大的结果集的示例:

using Microsoft.EntityFrameworkCore;
using System.Linq;
using System;
using System.ComponentModel.DataAnnotations.Schema;

namespace efCoreTest
{
    [Table("SomeEntity")]
    class SomeEntity
    {

        public int Id { get; set; }
        public string Name { get; set; }
        public string Description { get; set; }

        public DateTime CreatedOn { get; set; }
        public int A { get; set; }
        public int B { get; set; }
        public int C { get; set; }
        public int D { get; set; }

        virtual public Address Address { get; set; }
        public int AddressId { get; set; }

    }

    [Table("Address")]
    class Address
    {
        [DatabaseGenerated(DatabaseGeneratedOption.None)]
        public int Id { get; set; }
        public string Line1 { get; set; }
        public string Line2 { get; set; }
        public string Line3 { get; set; }

    }
    class Db : DbContext
    {
        public DbSet<SomeEntity> SomeEntities { get; set; }

        protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        {
            optionsBuilder.UseSqlServer("Server=.;Database=efCoreTest;Integrated Security=true");
        }

    }
    class Program
    {
        static void Main(string[] args)
        {
            using (var db = new Db())
            {
                db.Database.EnsureDeleted();
                db.Database.EnsureCreated();

                db.Database.ExecuteSqlCommand("alter database EfCoreTest set recovery simple;");

                var LoadAddressesSql = @"

with N as
(
   select top (10) cast(row_number() over (order by (select null)) as int) i
   from sys.objects o, sys.columns c, sys.columns c2
)
insert into Address(Id, Line1, Line2, Line3)
select i Id, 'AddressLine1' Line1,'AddressLine2' Line2,'AddressLine3' Line3
from N;
";

                var LoadEntitySql = @"

with N as
(
   select top (1000000) cast(row_number() over (order by (select null)) as int) i
   from sys.objects o, sys.columns c, sys.columns c2
)
insert into SomeEntity (Name, Description, CreatedOn, A,B,C,D, AddressId)
select  concat('EntityName',i) Name,
        concat('Entity Description which is really rather long for Entity whose ID happens to be ',i) Description,
        getdate() CreatedOn,
        i A, i B, i C, i D, 1+i%10 AddressId
from N

";
                Console.WriteLine("Generating Data ...");
                db.Database.ExecuteSqlCommand(LoadAddressesSql);
                Console.WriteLine("Loaded Addresses");

                for (int i = 0; i < 10; i++)
                {
                    var rows = db.Database.ExecuteSqlCommand(LoadEntitySql);
                    Console.WriteLine($"Loaded Entity Batch {rows} rows");
                }


                Console.WriteLine("Finished Generating Data");

                var results = db.SomeEntities.AsNoTracking().Include(e => e.Address).AsEnumerable();

                int batchSize = 10 * 1000;
                int ix = 0;
                foreach (var r in results)
                {
                    ix++;

                    if (ix % batchSize == 0)
                    {
                        Console.WriteLine($"Read Entity {ix} with name {r.Name}.  Current Memory: {GC.GetTotalMemory(false) / 1024}kb GC's Gen0:{GC.CollectionCount(0)} Gen1:{GC.CollectionCount(1)} Gen2:{GC.CollectionCount(2)}");

                    }

                }

                Console.WriteLine($"Done.  Current Memory: {GC.GetTotalMemory(false)/1024}kb");

                Console.ReadKey();
            }
        }
    }
}

输出

Generating Data ...
Loaded Addresses
Loaded Entity Batch 1000000 rows
Loaded Entity Batch 1000000 rows
. . .
Loaded Entity Batch 1000000 rows
Finished Generating Data
Read Entity 10000 with name EntityName10000.  Current Memory: 2854kb GC's Gen0:7 Gen1:1 Gen2:0
Read Entity 20000 with name EntityName20000.  Current Memory: 4158kb GC's Gen0:14 Gen1:1 Gen2:0
Read Entity 30000 with name EntityName30000.  Current Memory: 2446kb GC's Gen0:22 Gen1:1 Gen2:0
. . .
Read Entity 9990000 with name EntityName990000.  Current Memory: 2595kb GC's Gen0:7429 Gen1:9 Gen2:1
Read Entity 10000000 with name EntityName1000000.  Current Memory: 3908kb GC's Gen0:7436 Gen1:9 Gen2:1
Done.  Current Memory: 3916kb

注意,EF Core 中内存消耗过多的另一个常见原因是“Mixed client/server evaluation”查询。有关更多信息以及如何禁用自动客户端查询评估,请参阅文档。

【讨论】:

  • 我尝试了这种方法,但即使没有更改跟踪,ef 也会执行查询并将所有数据拉回内存中。 Read Entity 250000 with name 575430. Current Memory: 3678275kb GC's Gen0:16 Gen1:9 Gen2:6
  • 似乎只有当我添加包含时内存使用量才会增加。关于我为什么以及如何克服这个问题的任何想法?
  • 你能制作一个复制品,或者修改我发布的那个以显示内存使用量增加吗?作为一种解决方法,您始终可以展平查询中的对象图。
  • 我拿了你的例子,首先用我的数据库的脚手架替换了代码。我有一个 author 表,它与 book 表具有一对多的关系。我添加了 550,000 个作者和 2,750,000 本书,并给每个作者 5 本书(没有书籍与作者重叠)。然后我选择了所有作者并加入了书桌。我试图创建一个 repo,但我没有很好的方法来获得一个对你来说不是代码优先的大小的数据库。
  • 复制品主要适合您。在列举作者时,您是否发现内存利用率在增加?
【解决方案2】:

这是由于 MARS(多个活动结果集被禁用)造成的。

https://github.com/aspnet/EntityFrameworkCore/issues/9367

【讨论】:

    猜你喜欢
    • 2018-03-24
    • 2020-08-06
    • 1970-01-01
    • 2020-03-07
    • 2018-04-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多