Очень высокое использование памяти в .NET 4.0 - PullRequest
61 голосов
/ 03 июня 2011

У меня есть служба Windows C #, которую я недавно переместил с .NET 3.5 на .NET 4.0. Других изменений кода не было.

При работе на 3.5 использование памяти для данной рабочей нагрузки составляло примерно 1,5 ГБ памяти, а пропускная способность - 20 Х в секунду. (X не имеет значения в контексте этого вопроса.)

Точно такая же служба, работающая в 4.0, использует от 3 до 5 ГБ памяти и получает менее 4 X в секунду. Фактически, служба обычно останавливается, так как использование памяти продолжает расти, пока моя система не использует 99%, а подкачка файла подкачки не происходит.

Я не уверен, имеет ли это отношение к сбору мусора или как, но у меня проблемы с его выяснением. Мой оконный сервис использует GC «Сервер» через переключатель конфигурационного файла, показанный ниже:

  <runtime>
    <gcServer enabled="true"/>
  </runtime>

Изменение этой опции на false, похоже, не имеет значения. Более того, из прочитанного мною чтения нового GC в 4.0 большие изменения влияют только на режим GC на рабочей станции, а не на режим GC на сервере. Так что, возможно, GC не имеет никакого отношения к проблеме.

Идеи

Ответы [ 6 ]

83 голосов
/ 03 июня 2011

Ну, это было интересно.

Причиной, по-видимому, является изменение в поведении класса LocalReport служб SQL Server Reporting Services (v2010) при работе над .NET 4.0.

По сути, Microsoft изменила поведение обработки RDLC так, чтобы каждый раз при обработке отчета это делалось в отдельной области приложения. Фактически это было сделано специально для устранения утечки памяти, вызванной невозможностью выгрузки сборок из доменов приложений. Когда класс LocalReport обработал файл RDLC, он фактически создает сборку на лету и загружает ее в домен приложения.

В моем случае из-за большого объема отчета, который я обрабатывал, это приводило к созданию очень большого числа объектов System.Runtime.Remoting.ServerIdentity. Это было моей подсказкой к причине, поскольку я был смущен тем, почему обработка RLDC требовала удаленного взаимодействия.

Конечно, для вызова метода в классе в другом домене приложения удаленное взаимодействие - это именно то, что вы используете. В .NET 3.5 в этом не было необходимости, поскольку по умолчанию RDLC-сборка загружалась в тот же домен приложения. Однако в .NET 4.0 новый домен приложения создается по умолчанию.

Исправить было довольно легко. Сначала мне нужно было включить устаревшую политику безопасности, используя следующую конфигурацию:

  <runtime>
    <NetFx40_LegacySecurityPolicy enabled="true"/>
  </runtime>

Затем мне нужно было заставить RDLC обрабатываться в том же домене приложения, что и моя служба, вызвав следующее:

myLocalReport.ExecuteReportInCurrentAppDomain(AppDomain.CurrentDomain.Evidence);

Это решило проблему.

11 голосов
/ 19 марта 2015

Я столкнулся с этой проблемой. И это правда, что домены приложений создаются, а не очищаются. Однако я бы не рекомендовал возвращаться к наследию. Их можно очистить с помощью ReleaseSandboxAppDomain ().

LocalReport report = new LocalReport();
...
report.ReleaseSandboxAppDomain();

Некоторые другие вещи, которые я также делаю для очистки:

Отписаться от любых событий SubreportProcessing, Очистить источники данных, Утилизируйте отчет.

Наша служба Windows обрабатывает несколько отчетов в секунду, утечек нет.

4 голосов
/ 03 июня 2011

Возможно, вы захотите

Возможно, некоторые API изменили семантику, или в версии 4.0 фреймворка может быть ошибка

2 голосов
/ 07 марта 2013

Просто для полноты, если кто-то ищет эквивалентную настройку ASP.Net web.config, это:

  <system.web>
    <trust legacyCasModel="true" level="Full"/>
  </system.web>

ExecuteReportInCurrentAppDomain работает так же.

Благодаря этому Ссылка на социальный MSDN .

1 голос
/ 06 октября 2017

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

Я пытался сделать то же самое: использовать отдельный домен приложения и направить в него отчет. Я думаю, что это ужасное решение, которое очень быстро создает беспорядок.

То, что я сделал вместо этого, похоже: разделить часть отчетности вашей программы на отдельную программу отчетов. В любом случае это хороший способ организовать ваш код.

Сложная часть - передача информации в отдельную программу. Используйте класс Process, чтобы запустить новый экземпляр программы отчетов и передать все необходимые параметры в командной строке. Первый параметр должен быть перечислением или аналогичным значением, указывающим отчет, который должен быть напечатан. Мой код для этого в основной программе выглядит примерно так:

const string sReportsProgram = "SomethingReports.exe";

public static void RunReport1(DateTime pDate, int pSomeID, int pSomeOtherID) {
   RunWithArgs(ReportType.Report1, pDate, pSomeID, pSomeOtherID);
}

public static void RunReport2(int pSomeID) {
   RunWithArgs(ReportType.Report2, pSomeID);
}

// TODO: currently no support for quoted args
static void RunWithArgs(params object[] pArgs) {
   // .Join here is my own extension method which calls string.Join
   RunWithArgs(pArgs.Select(arg => arg.ToString()).Join(" "));
}

static void RunWithArgs(string pArgs) {
   Console.WriteLine("Running Report Program: {0} {1}", sReportsProgram, pArgs);
   var process = new Process();
   process.StartInfo.FileName = sReportsProgram;
   process.StartInfo.Arguments = pArgs;
   process.Start();
}

И программа отчетов выглядит примерно так:

[STAThread]
static void Main(string[] pArgs) {
   Application.EnableVisualStyles();
   Application.SetCompatibleTextRenderingDefault(false);

   var reportType = (ReportType)Enum.Parse(typeof(ReportType), pArgs[0]);
   using (var reportForm = GetReportForm(reportType, pArgs))
      Application.Run(reportForm);
}

static Form GetReportForm(ReportType pReportType, string[] pArgs) {
   switch (pReportType) {
      case ReportType.Report1: return GetReport1Form(pArgs);
      case ReportType.Report2: return GetReport2Form(pArgs);
      default: throw new ArgumentOutOfRangeException("pReportType", pReportType, null);
   }
}

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

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

Не переусердствуйте с аргументами. Выполните любые запросы к базе данных, которые вам нужны в программе отчетов; не передавайте огромный список объектов (которые, вероятно, не будут работать в любом случае). Вы должны просто передавать простые вещи, такие как поля идентификатора базы данных, диапазоны дат и т. Д. Если у вас есть особенно сложные параметры, вам может потребоваться передать эту часть пользовательского интерфейса в программу отчетов, а не передавать их в качестве аргументов в командной строке.

Вы также можете поместить ссылку на программу отчетов в вашей основной программе, и результирующий .exe и любые связанные с ним .dll будут скопированы в ту же папку вывода. Затем вы можете запустить его без указания пути и просто использовать исполняемое имя файла (то есть: «SomethingReports.exe»). Вы также можете удалить отчеты dll из основной программы.

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

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

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

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

0 голосов
/ 18 апреля 2019

Я довольно поздно опоздал, но у меня есть реальное решение, и я могу объяснить, почему!

Оказывается, что LocalReport здесь использует .NET Remoting для динамического создания дочернего домена приложения и запуска отчета вДля того, чтобы избежать утечки внутри где-то.Затем мы замечаем, что, в конце концов, отчет освободит всю память через 10-20 минут.Для людей с большим количеством создаваемых PDF-файлов это не сработает.Однако ключевой момент здесь заключается в том, что они используют .NET Remoting.Одна из ключевых частей Remoting - это то, что называется «Лизинг».Лизинг означает, что он будет держать этот объект Marshal некоторое время, поскольку удаленное взаимодействие, как правило, дорого в установке и, вероятно, будет использоваться более одного раза.LocalReport RDLC злоупотребляет этим.

По умолчанию время лизинга составляет ... 10 минут!Кроме того, если что-то делает различные звонки, это добавляет еще 2 минуты ко времени ожидания!Таким образом, случайным образом может быть от 10 до 20 минут, в зависимости от того, как выстроены вызовы.К счастью, вы можете изменить время ожидания.К сожалению, вы можете установить это только один раз для домена приложения ... Таким образом, если вам нужно удаленное взаимодействие, отличное от создания PDF, вам, вероятно, потребуется создать другой сервис, на котором он будет работать, чтобы вы могли изменить значения по умолчанию.Чтобы сделать это, все, что вам нужно сделать, это запустить эти 4 строки кода при запуске:

    LifetimeServices.LeaseTime = TimeSpan.FromSeconds(5);
    LifetimeServices.LeaseManagerPollTime = TimeSpan.FromSeconds(5);
    LifetimeServices.RenewOnCallTime = TimeSpan.FromSeconds(1);
    LifetimeServices.SponsorshipTimeout = TimeSpan.FromSeconds(5);

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

Вы не можете заключить ReportViewer в оператор using (Dispose crash), но вы должны это сделать, если вы используете LocalReport напрямую,После этого вы можете вызвать GC.Collect (), если хотите быть вдвойне уверены, что делаете все возможное, чтобы освободить эту память.

Надеюсь, это поможет!

Редактировать

По-видимому, вам следует вызывать GC.Collect (0) после создания отчета в формате PDF, иначе может показаться, что использование памяти по какой-то причине все еще может возрасти.

...