AspNet Core Web API EF Core 内存使用率高

AspNet Core Web API high memory usage with EF Core

我在调用某些端点后发现诊断工具上的内存使用率过高。

我试图尽可能地在最小的块中隔离问题,这样我就可以排除所有其他因素,最后得到以下结果:

    [HttpGet("test")]
    public ActionResult Test()
    {
        var results = _context.Products
            .Include(x => x.Images)
            .Include(x => x.Options)
                .ThenInclude(x => x.Lists)
                    .ThenInclude(x => x.PriceChangeRule)
            .Include(x => x.Options)
                .ThenInclude(x => x.Lists)
                    .ThenInclude(x => x.Items)
                        .ThenInclude(x => x.PriceChangeRule)
            .Include(x => x.Options)
                .ThenInclude(x => x.Lists)
                    .ThenInclude(x => x.Items)
                        .ThenInclude(x => x.SupplierFinishingItem)
                            .ThenInclude(x => x.Parent)
            .Include(x => x.Category)
                .ThenInclude(x => x.PriceFormation)
                    .ThenInclude(x => x.Rules)
            .Include(x => x.Supplier)
                .ThenInclude(x => x.PriceFormation)
                    .ThenInclude(x => x.Rules)
            .Include(x => x.PriceFormation)
                .ThenInclude(x => x.Rules)
                .AsNoTracking().ToList();

        return Ok(_mapper.Map<List<AbstractProductListItemDto>>(results));
    }

这是一个很大的查询,有很多包含,但是从数据库返回的数据量并不大,大约 10.000 项。当我序列化这个结果时,它只有 3.5Mb。

我的 API 使用了大约 300Mb 的内存,然后当我调用这个测试端点时,这个值变成了大约 1.2Gb。我认为这对于只有 3.5Mb 的数据来说太多了,但我不知道 EF Core 内部是如何工作的,所以我将忽略它。

我的问题是,据我所知,DbContext 是作为范围服务添加的,因此它在请求开始时创建,然后在请求完成时终止。以下是我的注册方式:

    services.AddDbContext<DatabaseContext>(options =>
            options.UseSqlServer(
                Configuration.GetConnectionString("DefaultConnection")));

如果我的理解是正确的,当请求结束时,那么大的内存应该被释放了吧?

问题是我的内存使用量再也回不去了,我尝试手动处理上下文,也手动调用垃圾收集器,但内存保持在 1.2Gb。

我是不是漏掉了什么?

我能看到的一个潜在领域是,您从数据库中加载的数据可能比序列化到 AbstractProductListItemDto 所需的数据多得多。例如,您的 Product 可能有如下字段:

public class Product
{
    public string Id { get; set; }
    public string Name { get; set; }
    public string Description { get; set; }
}

但是,您的最终 DTO 可能只有其中一个或两个属性,例如:

public class AbstractProductListItemDto
{
    public string ProductId { get; set; }
    public string ProductName { get; set; }
}

对于您包括的其他 table(OptionsListsRules 等),这可能也是正确的,尤其是 tables 是 one-to-many,这很容易使查询的数字 rows/columns 爆炸。

一种可能的优化方法是在 LINQ 查询中自己进行投影。这将利用 EF Core 的一项功能,它仅 selects 您指定的数据库中的列。例如:

这将 select 产品 table

中的所有列
var results = _context.Products.ToList();

这将 select 仅来自产品 table 的 Id 和 Name 列,从而减少内存使用量

var results = _context.Products.Select(x => new ProductDto { 
    Id = x.Id,
    Name = x.Name,
}

根据这个问题,我不知道您正在映射的所有项目的所有属性,所以如果您想手动进行映射,则由您决定。关键部分是您需要在调用 Select() 之前 在您的查询中调用 ToList()

但是,如果您使用的是 Automapper

,则有一个潜在的捷径

Automapper 包含一个快捷方式,它会尝试为您编写这些查询预测。它可能无法工作,具体取决于 Automapper 中发生了多少额外的逻辑,但它可能值得一试。 You would want to read up on the ProjectTo<>() method。如果您使用投影,代码可能看起来像这样:

编辑:评论中正确指出使用 ProjectTo<>() 时不需要调用 Include()。这是一个较短的样本,其下方包含原始样本

更新:

using AutoMapper.QueryableExtensions;
// ^^^ Added to your usings
// 

    [HttpGet("test")]
    public ActionResult Test()
    {
        var projection = _context.Products.ProjectTo<AbstractProductListItemDto>(_mapper.ConfigurationProvider);

        return Ok(projection.ToList());
    }

原文:

using AutoMapper.QueryableExtensions;
// ^^^ Added to your usings
// 

    [HttpGet("test")]
    public ActionResult Test()
    {
        var results = _context.Products
            .Include(x => x.Images)
            .Include(x => x.Options)
                .ThenInclude(x => x.Lists)
                    .ThenInclude(x => x.PriceChangeRule)
            .Include(x => x.Options)
                .ThenInclude(x => x.Lists)
                    .ThenInclude(x => x.Items)
                        .ThenInclude(x => x.PriceChangeRule)
            .Include(x => x.Options)
                .ThenInclude(x => x.Lists)
                    .ThenInclude(x => x.Items)
                        .ThenInclude(x => x.SupplierFinishingItem)
                            .ThenInclude(x => x.Parent)
            .Include(x => x.Category)
                .ThenInclude(x => x.PriceFormation)
                    .ThenInclude(x => x.Rules)
            .Include(x => x.Supplier)
                .ThenInclude(x => x.PriceFormation)
                    .ThenInclude(x => x.Rules)
            .Include(x => x.PriceFormation)
                .ThenInclude(x => x.Rules)
                .AsNoTracking(); // Removed call to ToList() to keep it as IQueryable<>

        var projection = results.ProjectTo<AbstractProductListItemDto>(_mapper.ConfigurationProvider);

        return Ok(projection.ToList());
    }