【问题标题】:MVC ASP.NET is using a lot of memoryMVC ASP.NET 正在使用大量内存
【发布时间】:2014-11-17 13:16:27
【问题描述】:

如果我只是浏览应用程序上的一些页面,它大约有 500MB。这些页面中的许多页面都访问数据库,但此时,我只有大约几行,每个 10 个表,主要存储字符串和一些小于 50KB 的小图标。

真正的问题发生在我下载文件时。该文件大约为 140MB,并以varbinary(MAX) 的形式存储在数据库中。内存使用量在一瞬间突然上升到 1.3GB,然后又回落到 1GB。该操作的代码在这里:

public ActionResult DownloadIpa(int buildId)
{
    var build = _unitOfWork.Repository<Build>().GetById(buildId);
    var buildFiles = _unitOfWork.Repository<BuildFiles>().GetById(buildId);
    if (buildFiles == null)
    {
        throw new HttpException(404, "Item not found");
    }

    var app = _unitOfWork.Repository<App>().GetById(build.AppId);
    var fileName = app.Name + ".ipa";

    app.Downloads++;
    _unitOfWork.Repository<App>().Update(app);
    _unitOfWork.Save();

    return DownloadFile(buildFiles.Ipa, fileName);
}

private ActionResult DownloadFile(byte[] file, string fileName, string type = "application/octet-stream")
{
    if (file == null)
    {
        throw new HttpException(500, "Empty file");
    }

    if (fileName.Equals(""))
    {
        throw new HttpException(500, "No name");
    }

    return File(file, type, fileName);            
}

在我的本地计算机上,如果我什么都不做,内存使用量将保持在 1GB。如果我然后返回并导航到某些页面,它会回落到 500MB。

在部署服务器上,无论我做什么,第一次下载后它都保持在 1.6GB。我可以通过不断下载文件来强制增加内存使用量,直到达到 3GB,然后又回落到 1.6GB。

在每个控制器中,我都重写了Dispose() 方法:

protected override void Dispose(bool disposing)
{
    _unitOfWork.Dispose();
    base.Dispose(disposing);
}

这里指的是:

public void Dispose()
{
    Dispose(true);
    GC.SuppressFinalize(this);
}

public void Dispose(bool disposing)
{
    if (!_disposed)
    {
        if (disposing)
        {
            _context.Dispose();
        }
    }

    _disposed = true;
}

所以我的工作单元应该在每次处置控制器时处置。我正在使用 Unity,并且向 Heirarchical Lifetime Manager 注册了工作单元。

以下是 Profiler 的一些屏幕截图:

我认为这可能是问题所在,或者我走错了路。为什么Find() 会使用 300MB?

编辑:

存储库:

public class Repository<TEntity> : IRepository<TEntity> where TEntity : class
{
    internal IDbContext Context;
    internal IDbSet<TEntity> DbSet;

    public Repository(IDbContext context)
    {
        Context = context;
        DbSet = Context.Set<TEntity>();
    }

    public virtual IEnumerable<TEntity> GetAll()
    {            
        return DbSet.ToList();
    }

    public virtual TEntity GetById(object id)
    {
        return DbSet.Find(id);
    }

    public TEntity GetSingle(Expression<Func<TEntity, bool>> predicate)
    {
        return DbSet.Where(predicate).SingleOrDefault();
    }

    public virtual RepositoryQuery<TEntity> Query()
    {
        return new RepositoryQuery<TEntity>(this);
    }

    internal IEnumerable<TEntity> Get(
        Expression<Func<TEntity, bool>> filter = null,
        Func<IQueryable<TEntity>, IOrderedQueryable<TEntity>> orderBy = null,
        List<Expression<Func<TEntity, object>>> includeProperties = null)
    {
        IQueryable<TEntity> query = DbSet;

        if (includeProperties != null)
        {
            includeProperties.ForEach(i => query.Include(i));
        }

        if (filter != null)
        {
            query = query.Where(filter);
        }

        if (orderBy != null)
        {
            query = orderBy(query);
        }

        return query.ToList();
    }

    public virtual void Insert(TEntity entity)
    {
        DbSet.Add(entity);
    }

    public virtual void Update(TEntity entity)
    {
        DbSet.Attach(entity);
        Context.Entry(entity).State = EntityState.Modified;
    }

    public virtual void Delete(object id)
    {
        var entity = DbSet.Find(id);

        Delete(entity);
    }

    public virtual void Delete(TEntity entity)
    {
        if (Context.Entry(entity).State == EntityState.Detached)
        {
            DbSet.Attach(entity);
        }

        DbSet.Remove(entity);
    }
}

编辑 2:

我为各种场景运行 dotMemory,这就是我得到的。

红色圆圈表示有时一页访问会发生多次上升和下降。蓝色圆圈表示下载 40MB 文件。绿色圆圈表示下载 140MB 文件。此外,很多时候,即使页面立即加载,内存使用量也会持续增加几秒钟。

【问题讨论】:

  • 试用 Jetbrains dotMemory 4.1 beta www.jetbrains.com/dotmemory/download
  • 您能发布您的整个存储库吗? (有些人认为整个 Repository 模式是一种反模式,我就是其中之一,但这不是重点)。我偷偷怀疑_unitOfWork.Repository&lt;App&gt;().Update(app); 可能会导致整个 DbSet 被加载,但我无法从您发布的代码中看到它。
  • 如果将文件大小减半,内存使用峰值会下降吗?这将帮助您确定是文件本身还是调用文件检索的代码导致了问题。
  • @krisdyson 是的,内存使用峰值确实下降了。它不会破坏 1GB。
  • 将 GC.Collect() 添加到 Dispose 方法以进行测试。如果泄漏仍然存在,那就是真正的泄漏。如果它消失了,它只是延迟了 GC。

标签: c# asp.net asp.net-mvc entity-framework memory


【解决方案1】:

因为文件很大,所以它被分配在大对象堆上,该堆是使用 gen2 集合收集的(您在配置文件中看到,紫色块是大对象堆,您看到它在 10 秒后收集)。

在您的生产服务器上,您很可能拥有比本地机器更多的内存。因为内存压力较小,收集不会那么频繁,这解释了为什么它会增加一个更高的数字 - LOH 在收集之前有几个文件。

如果在 MVC 和 EF 中的不同缓冲区中,一些数据也被复制到不安全的块中,我一点也不会感到惊讶,这解释了非托管内存增长(EF 的细尖峰,MVC 的宽平台)

最后,一个 500MB 的基线对于一个大型项目来说并不完全令人惊讶(疯狂!但确实如此!)

因此,对于您的问题为什么它很可能使用这么多内存的答案是“因为它可以”,或者换句话说,因为执行 gen2 收集没有内存压力,并且下载的文件未使用您的大型对象堆,直到收集驱逐它们,因为您的生产服务器上的内存充足。

这甚至可能不是一个真正的问题:如果内存压力更大,就会有更多的收集,而您会看到内存使用率更低。

至于该怎么做,恐怕你对实体框架不走运。据我所知,它没有流式 API。顺便说一句,WebAPI 确实允许流式传输响应,但是如果您将整个大对象都放在内存中,这对您没有多大帮助(尽管它可能对一些(由我)未探索的 MVC 部分中的非托管内存有所帮助.

【讨论】:

    【解决方案2】:

    将 GC.Collect() 添加到 Dispose 方法以进行测试。如果泄漏仍然存在,那就是真正的泄漏。如果它消失了,它只是延迟了 GC。

    你这样做并说:

    @usr 内存使用量现在几乎没有达到 600MB。所以真的只是延迟了吗?

    显然,如果 GC.Collect 删除了您担心的内存,则不会发生内存泄漏。如果您想真正确定,请运行您的测试 10 次。内存使用应该是稳定的。

    在文件通过不同的组件和框架传输时,以单个块处理此类大文件可能会导致成倍的内存使用。切换到流式传输方法可能是个好主意。

    【讨论】:

      【解决方案3】:

      显然,它由 System.Web 及其所有子项组成,占用大约 200MB。这被引用为您的应用程序池的绝对最小值。

      我们使用 EF 6 的 Web 应用程序在 .Net 4.0 中包含由 220 多个实体组成的模型,其启动时空闲大约 480MB。我们在启动时执行一些 AutoMapper 操作。内存消耗达到峰值,然后在日常使用中恢复到 500MB 左右。我们刚刚接受了这一点。

      现在,对于您的文件下载高峰。在这个问题中探讨了使用 ashx 处理程序等时 Web 表单下的问题:ASP.net memory usage during download

      我不知道这与 MVC 中的 FileActionResult 有何关系,但您可以看到需要手动控制缓冲区大小以最小化内存峰值。尝试通过以下方式应用该问题答案背后的原则:

      Response.BufferOutput = false;
      var stream = new MemoryStream(file);
      stream.Position = 0;
      return new FileStreamResult(stream, type); // Or just pass the "file" parameter as a stream
      

      应用此更改后,内存行为是什么样的?

      更多详情请见'Debugging memory problems (MSDN)'

      【讨论】:

        【解决方案4】:

        您可能需要分块读取数据并写入输出流。 看看 SqlDataReader.GetBytes http://msdn.microsoft.com/en-us/library/system.data.sqlclient.sqldatareader.getbytes(v=vs.110).aspx

        【讨论】:

        • 这并没有解决他的具体问题。
        • 当您执行 DbSet.Find 时,它实际上会加载结果。来自微软“查找具有给定主键值的实体。如果上下文中存在具有给定主键值的实体,则立即返回它而不向商店发出请求。否则,向商店发出请求具有给定主键值的实体,如果找到该实体,则将其附加到上下文并返回。如果在上下文或存储中未找到实体,则返回 null。" msdn.microsoft.com/en-us/library/…
        • 他的问题是为什么内存使用量比他的文件大得多。此外,内存使用量似乎保持不变。流式传输大文件确实是可取的,但这不是问题所在。
        【解决方案5】:

        这可能是以下几种情况之一:

        由于您的文件相当大并且存储在您的数据库中,并且您通过Entity Framework 获取它,因此您将这些数据缓存在几个地方。每个EF 请求都会缓存该数据,直到您的上下文被释放。当您从操作返回文件时,数据会再次加载,然后流式传输到客户端。正如已经解释的那样,所有这些都发生在ASP .NET

        解决此问题的方法是不要使用EFASP .NET 直接从数据库中流式传输大文件。更好的解决方案是使用后台进程将大文件缓存到网站本地,然后让客户端使用直接 URL 下载它们。这允许IIS 管理流式传输,为您的网站节省请求并节省大量内存。

        或(不太可能)

        看到您正在使用Visual Studio 2013,这听起来非常像Page Inspector 问题。

        当您使用来自Visual StudioIIS Express 运行您的网站时会发生什么,Page Inspector 缓存所有响应数据 - 包括您的文件 - 导致大量内存用过的。尝试添加:

        <appSettings>
            <add key="PageInspector:ServerCodeMappingSupport" value="Disabled" />
        </appSettings>
        

        到您的web.config 禁用Page Inspector 看看是否有帮助。

        TL;DR

        将大文件缓存到本地,让客户端直接下载文件。让 IIS 为您处理繁重的工作。

        【讨论】:

          【解决方案6】:

          我建议尝试 Ionic.Zip 库。我在我们的一个站点中使用它,需要将多个文件下载到一个单元中。

          我最近用一组文件测试了它,其中一个文件大到 600MB:

          • 压缩/压缩文件夹的总大小:260MB
          • 解压文件夹总大小:630MB
          • 下载期间内存使用量从 350MB 飙升至 650MB
          • 总时间:1m 10s 下载,无VPN

          【讨论】:

            猜你喜欢
            • 1970-01-01
            • 2014-06-30
            • 1970-01-01
            • 1970-01-01
            • 2013-11-24
            • 2019-05-16
            • 2012-01-13
            • 2023-03-24
            • 1970-01-01
            相关资源
            最近更新 更多