MVC ASP.NET正在占用大量内存。

12

如果我只是在应用程序上浏览一些页面,它的占用空间大约是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的一些屏幕截图:

enter image description here

enter image description here

enter image description here

我认为这可能是问题所在,或者我走错了方向。为什么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来测试各种情况,以下是我的结果。

enter image description here

红色圆圈表示在某些页面访问中有时会出现多个上升和下降。蓝色圆圈表示下载一个40MB的文件。绿色圆圈表示下载一个140MB的文件。此外,很多时候,即使页面已经立即加载完成,内存使用量仍会继续增加几秒钟。


尝试使用Jetbrains dotMemory 4.1 beta版本,网址为www.jetbrains.com/dotmemory/download。 - leppie
1
你能发一下你整个仓储库吗?(有些人认为整个仓储库模式是一个反模式,我也是这样认为的,但这并不是重点)。我有一种隐约的感觉_unitOfWork.Repository<App>().Update(app);可能会导致整个DbSet被加载,但从你发布的代码中我无法看出来。 - Martijn
1
如果你把文件大小减半,峰值内存使用量会降低吗?这将帮助你确定是文件本身还是调用文件检索的代码引起了问题。 - krisdyson
@krisdyson 是的,峰值内存使用量确实降低了。它不会超过1GB。 - Kevin Lee
4
为了测试目的,在Dispose方法中加入GC.Collect()。如果内存泄漏仍然存在,则是真正的内存泄漏。如果内存泄漏消失了,那么它只是被延迟的垃圾回收。 - usr
显示剩余6条评论
6个回答

9
由于文件很大,所以它被分配在大对象堆上,这个堆与gen2集合相关(您可以在配置文件中看到紫色块是大对象堆,在10秒后进行收集)。在生产服务器上,您的内存很可能比本地机器多得多。由于内存压力较小,集合不会像本地机器那样频繁发生,这就解释了为什么 LOH 中会累积更多文件,直到它们被收集之前。我完全不会感到惊讶,如果在 MVC 和 EF 的不同缓冲区之间复制了一些数据,并使用不安全块造成了未受管理的内存增长(EF 的细尖峰和 MVC 的宽高原因)。最后,对于一个大型项目来说,500MB的基线并不完全令人惊讶(疯狂!但是真的!),因此对于为什么会使用如此多的内存的问题的一个相当可能的答案是“因为它可以”,或者换句话说,因为没有内存压力执行gen2集合,并且下载的文件闲置在大对象堆中,直到收集将其驱逐,因为生产服务器上的内存丰富无比。这可能甚至不是一个真正的问题:如果有更多内存压力,则会更多地进行收集,并且您将看到更低的内存使用率。至于如何解决这个问题,恐怕您在 Entity Framework 方面没有什么办法。据我所知,它没有流 API。顺便说一下,WebAPI 允许流式传输响应,但如果整个大对象仍然停留在内存中,则帮助不大(尽管它可能对 MVC 的未探索部分中的未受管理的内存有所帮助)。

4
在测试目的下,将GC.Collect()添加到Dispose方法中。如果泄漏仍然存在,则是真正的泄漏。如果消失了,那就只是延迟了GC。
你这么做并说道:
@usr 现在内存使用量几乎不到600MB。所以只是延迟了?
显然,如果GC.Collect可以清除你担心的内存,则没有内存泄漏。如果你想确保,可以运行你的测试10次。内存使用应该是稳定的。
在单个块中处理如此大的文件可能会导致内存使用量增加,因为文件经过不同的组件和框架。将其更改为流式处理方式可能是一个好主意。

2

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

我们的Web应用程序使用EF 6,在.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

应用这个改变后,内存的行为会有什么变化?
更多详情请参考《调试内存问题(MSDN)》

0

这并没有解决他具体的问题。 - usr
当您执行DbSet.Find时,它会实际加载结果。来自Microsoft的解释是“查找具有给定主键值的实体。如果上下文中存在具有给定主键值的实体,则立即返回该实体,而不会向存储发出请求。否则,将向存储发送请求以获取具有给定主键值的实体,并将找到的此实体连接到上下文并返回。如果在上下文或存储中找不到实体,则返回null。”http://msdn.microsoft.com/en-us/library/system.data.entity.dbset.find(v=vs.113).aspx - Aladin Hdabe
他的问题是为什么内存使用量比文件大小要大得多。而且,内存使用量似乎一直保持在这个水平。虽然流式传输大文件确实是可取的,但这不是问题所在。 - usr

0

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

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

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

0

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

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

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

或者(可能性较小)

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

当你使用Visual Studio中的IIS Express运行网站时,Page Inspector会缓存所有响应数据 - 包括文件的数据 - 导致大量内存被使用。尝试添加以下代码:

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

将以下内容添加到您的 web.config 中以禁用 Page Inspector ,看看是否有所帮助。

太长不想读(TL;DR)

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


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接