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(Options
、Lists
、Rules
等),这可能也是正确的,尤其是 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());
}
我在调用某些端点后发现诊断工具上的内存使用率过高。
我试图尽可能地在最小的块中隔离问题,这样我就可以排除所有其他因素,最后得到以下结果:
[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(Options
、Lists
、Rules
等),这可能也是正确的,尤其是 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());
}