MVC ASP.NET использует много памяти

Если я просто просматриваю некоторые страницы в приложении, он сидит около 500 МБ. Многие из этих страниц обращаются к базе данных, но на данный момент у меня есть примерно одна пара строк для 10 таблиц, в основном хранящая строки и некоторые небольшие значки размером менее 50 КБ.

Реальная проблема возникает, когда я загружаю файл. Файл составляет около 140 МБ и хранится как varbinary(MAX) в базе данных. Использование памяти внезапно возрастает до 1,3 ГБ на долю секунды, а затем падает до 1 ГБ. Код для этого действия находится здесь:

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);            
}

На моем локальном компьютере, если я ничего не делаю, использование памяти остается на уровне 1 ГБ. Если я затем вернусь и перейду к некоторым страницам, он опустится до 500 МБ.

На сервере развертывания он остается на уровне 1,6 ГБ после первой загрузки независимо от того, что я делаю. Я могу заставить использование памяти увеличиваться, постоянно загружая файлы, пока не достигнет 3 ГБ, где он упадет до 1,6 ГБ.

В каждом контроллере я превзошел метод 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.

Вот несколько скриншотов из Профайлера:

enter image description here

enter image description here

enter image description here

Я считаю, что это может быть проблемой, или я иду по неверному пути. Почему Find() использовать 300 МБ?

EDIT:

Repository:

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

Красные круги указывают на то, что иногда на одной странице посещают несколько повышений и капель. Синий круг указывает на загрузку 40 МБ файла. Зеленый круг указывает загрузку 140 МБ файла. Более того, время от времени использование памяти продолжает расти еще на несколько секунд даже после мгновенной загрузки страницы.

Ответ 1

Добавьте GC.Collect() в метод Dispose для целей тестирования. Если утечка остается реальной утечкой. Если он исчезнет, ​​это просто задержит GC.

Вы сделали это и сказали:

@usr Использование памяти почти не достигает 600 МБ. Так что действительно просто отложено?

Очевидно, что утечка памяти отсутствует, если GC.Collect удаляет память, о которой вы беспокоились. Если вы хотите сделать действительно уверенным, проведите тест 10 раз. Использование памяти должно быть стабильным.

Обработка таких больших файлов в одиночных фрагментах может привести к умножению использования памяти, поскольку файл перемещается через различные компоненты и рамки. Может быть хорошей идеей переключиться на потоковый подход.

Ответ 2

Поскольку файл большой, он выделяется в большой куче объекта, который собирается с помощью коллекции gen2 (которую вы видите в своем профиле, фиолетовые блоки - это куча больших объектов, и вы видите, что она собрана через 10 секунд).

На вашем рабочем сервере у вас, скорее всего, гораздо больше памяти, чем на вашей локальной машине. Из-за меньшего давления памяти коллекции не будут встречаться так часто, что объясняет, почему это будет увеличиваться до большего числа - на LOH есть несколько файлов, прежде чем они будут собраны.

Я бы не удивился, если бы через разные буферы в MVC и EF некоторые данные также копировались в небезопасных блоках, что объясняет неуправляемый рост памяти (тонкий шип для EF, широкое плато для MVC)

Наконец, базовая линия в 500 Мбайт для большого проекта не совсем неожиданна (безумие!, но верно!)

Итак, ответ на ваш вопрос, почему он использует так много памяти, что вполне вероятно, "потому что он может", или, другими словами, потому что нет никакого давления памяти для выполнения коллекции gen2, и загруженные файлы не используются в ваша куча больших объектов до тех пор, пока коллекция не выселяет их, потому что память на вашем рабочем сервере изобилует.

Это, вероятно, даже не настоящая проблема: если бы было больше давления памяти, было бы больше сбора, и вы видели бы более низкое использование памяти.

Что касается того, что делать с этим, я боюсь, что вам не повезло с Entity Framework. Насколько я знаю, у него нет потокового API. WebAPI действительно позволяет потоковой передачи ответа, но это не поможет вам, если у вас есть все большой объект, сидящий в памяти в любом случае (хотя он может помочь некоторым с неуправляемой памятью в (по мне) неисследованными частями MVC.

Ответ 3

По-видимому, он состоит из System.Web и всех его детей, занимающих около 200 МБ. Это указано как абсолютный минимум для вашего пула приложений.

Наше веб-приложение, использующее EF 6, с моделью, состоящей из 220+ объектов в .Net 4.0, запускается на 480 мбайт бездействия. При запуске мы выполняем некоторые операции AutoMapper. Пики памяти потребляют, а затем ежедневно возвращаются к 500 МБ. Мы только что приняли это как норму.

Теперь, для ваших загрузок загрузки файла. Проблема в веб-формах при использовании обработчика ashx или тому подобного была изучена в этом вопросе: Использование памяти ASP.net во время загрузки

Я не знаю, как это относится к FileActionResult в MVC, но вы можете видеть, что размер буфера необходимо контролировать вручную, чтобы минимизировать всплеск памяти. Попытайтесь применить принципы ответа на этот вопрос:

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).

Ответ 5

Это может быть одна из нескольких вещей:

Поскольку ваш файл довольно большой и хранится в вашей базе данных, и вы получаете его через Entity Framework, вы кешируете эти данные в нескольких местах. Каждый запрос EF кэширует данные до тех пор, пока ваш контекст не будет удален. Когда вы возвращаете файл из действия, данные затем снова загружаются и затем передаются клиенту. Все это происходит в ASP .NET, как уже объяснено.

Решение этой проблемы - не передавать большие файлы непосредственно из базы данных с помощью EF и ASP .NET. Лучшим решением является использование фонового процесса для кэширования больших файлов локально на веб-сайт, а затем загрузка клиентом с прямым URL. Это позволяет IIS управлять потоковой передачей, сохраняет ваш сайт в запросе и экономит много памяти.

ИЛИ (менее вероятно)

Увидев, что вы используете Visual Studio 2013, это звучит ужасно как проблема Page Inspector.

Что происходит, когда вы запускаете свой сайт с помощью IIS Express из Visual Studio, Page Inspector, кэширует все данные ответа, в том числе файлы вашего файла, в результате чего используется большая часть памяти. Попробуйте добавить:

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

на web.config, чтобы отключить Page Inspector, чтобы узнать, помогает ли это.

TL; DR

Загрузите большой файл локально и дайте клиенту возможность напрямую загрузить файл. Пусть IIS справится с тяжелой работой для вас.

Ответ 6

Я предлагаю попробовать библиотеку Ionic.Zip. Я использую его на одном из наших сайтов с требованием загрузить несколько файлов в один блок.

Недавно я тестировал его с группой файлов, в то время как один из файлов имеет размер 600 МБ:

  • Общий размер сжатой/сжатой папки: 260 МБ
  • Общий размер распакованной папки: 630 МБ
  • Использование памяти увеличилось с 350 МБ до 650 МБ во время загрузки.
  • Общее время: 1 м 10 секунд для загрузки, нет VPN