Подтвердить что ты не робот

Очень высокое использование памяти в .NET 4.0

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

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

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

Я не уверен, что это связано с сборкой мусора или что-то в этом роде, но мне трудно понять это. В моем оконном сервисе используется GC-сервер "Сервер" через коммутатор конфигурационного файла, показанный ниже:

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

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

Идеи?

4b9b3361

Ответ 1

Хорошо, это было интересно.

Коренной причиной является изменение поведения класса 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);

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

Ответ 2

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

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

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

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

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

Ответ 3

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

  • профиль кучи
  • используйте WinDbg + SOS.dll, чтобы установить, какой ресурс просачивается и откуда находится ссылка.

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

Ответ 4

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

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

ExecuteReportInCurrentAppDomain работает одинаково.

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

Ответ 5

Кажется, 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 из основной программы.

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

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

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

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

Ответ 6

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

Оказывается, что 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, иначе может показаться, что использование памяти по какой-то причине все еще может возрасти.