Programing

.NET 4.0에서 매우 높은 메모리 사용량

lottogame 2020. 11. 28. 08:32
반응형

.NET 4.0에서 매우 높은 메모리 사용량


최근에 .NET 3.5에서 .NET 4.0으로 이동 한 C # Windows 서비스가 있습니다. 다른 코드는 변경되지 않았습니다.

3.5에서 실행할 때 주어진 작업 부하에 대한 메모리 사용률은 대략 1.5GB의 메모리 였고 처리량은 초당 20 배였습니다. (X는이 질문의 맥락에서 중요하지 않습니다.)

4.0에서 실행되는 똑같은 서비스는 3GB에서 5GB 이상의 메모리를 사용하며 초당 4 배 미만입니다. 실제로 내 시스템이 99 % 사용률을 유지하고 페이지 파일 스와핑이 열광 할 때까지 메모리 사용량이 계속 증가함에 따라 서비스는 일반적으로 중단됩니다.

이것이 가비지 컬렉션과 관련이 있는지 또는 무엇과 관련이 있는지 확실하지 않지만 알아내는 데 어려움이 있습니다. 내 창 서비스는 아래에 표시된 구성 파일 스위치를 통해 "서버"GC를 사용합니다.

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

이 옵션을 false로 변경해도 차이가없는 것 같습니다. 더욱이 4.0의 새로운 GC에 대해 읽은 내용에서 큰 변화는 서버 GC 모드가 아닌 워크 스테이션 GC 모드에만 영향을 미칩니다. 따라서 GC는 문제와 관련이 없습니다.

아이디어?


글쎄 이것은 흥미로운 것입니다.

근본 원인은 SQL Server Reporting Services의 LocalReport 클래스 (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);

이것은 문제를 해결했습니다.


이 정확한 문제가 발생했습니다. 그리고 앱 도메인이 생성되고 정리되지 않는 것은 사실입니다. 그러나 레거시로 되 돌리는 것은 권장하지 않습니다. ReleaseSandboxAppDomain ()으로 정리할 수 있습니다.

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

정리하기 위해 다른 작업도 수행합니다.

SubreportProcessing 이벤트를 구독 취소하고 데이터 소스를 지우고 보고서를 폐기하십시오.

Windows 서비스는 1 초에 여러 보고서를 처리하며 누출이 없습니다.


당신은

일부 API가 의미를 변경했거나 프레임 워크 4.0 버전에 버그가있을 수도 있습니다.


완전성을 위해 누군가 동등한 ASP.Net web.config설정을 찾고 있다면 다음과 같습니다 .

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

ExecuteReportInCurrentAppDomain 동일하게 작동합니다.

Social MSDN 참조 덕분 입니다.


마이크로 소프트가 모든 메모리 누수 문제를 해결하기보다는 해결하기 위해 보고서를 별도의 메모리 공간에 넣는 것처럼 보입니다. 그렇게하면서 일부 하드 크래시가 발생했고 어쨌든 더 많은 메모리 누수가 발생 했습니다 . 그들은 보고서 정의를 캐시하는 것처럼 보이지만 그것을 사용하거나 정리하지 않으며, 모든 새로운 보고서는 새로운 보고서 정의를 만들어 점점 더 많은 메모리를 차지합니다.

나는 똑같은 일을하면서 놀았다. 별도의 앱 도메인을 사용하고 보고서를 마샬링했다. 나는 그것이 끔찍한 해결책이라고 생각하고 매우 빨리 엉망으로 만듭니다.

대신 내가 한 일은 비슷합니다. 프로그램의보고 부분을 별도의 보고서 프로그램으로 분리했습니다. 이것은 어쨌든 코드를 구성하는 좋은 방법으로 밝혀졌습니다.

까다로운 부분은 정보를 별도의 프로그램에 전달하는 것입니다. 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방법은 데이터 집합을 얻기 위해 관련 인수의 보고서 정의, 메이크업 사용을 당겨 보고서에 데이터 및 기타 인수를 전달하고 폼에 보고서 뷰어에서 보고서를 배치하고 양식에 대한 참조를 반환해야합니다. 기본적으로 '이 데이터와 이러한 인수를 사용하여이 어셈블리에서이 보고서에 대한 양식을주세요'라고 말할 수 있도록이 프로세스의 대부분을 추출 할 수 있습니다.

Also note that both programs must be able to see your data types that are relevant to this project, so hopefully you have extracted your data classes into their own library, which both of these programs can share a reference to. It would not work to have all of the data classes in the main program, because you would have a circular dependency between the main program and the report program.

Don't over do it with the arguments, either. Do any database querying you need in the reports program; don't pass a huge list of objects (which probably wouldn't work anyway). You should just be passing simple things like database ID fields, date ranges, etc. If you have particularly complex parameters, you might need to push that part of the UI to the reports program too and not pass them as arguments on the command line.

You can also put a reference to the reports program in your main program, and the resulting .exe and any related .dlls will be copied to the same output folder. You can then run it without specifying a path and just use the executable filename by itself (ie: "SomethingReports.exe"). You can also remove the reporting dlls from the main program.

One issue with this is that you will get a manifest error if you've never actually published the reports program. Just dummy publish it once, to generate a manifest and then it will work.

Once you have this working, it's very nice to see your regular program's memory stay constant when printing a report. The reports program appears, taking up more memory than your main program, and then disappears, cleaning it up completely with your main program taking up no more memory than it already had.

Another issue might be that each report instance will now take up more memory than before, since they are now entire separate programs. If the user prints a lot of reports and never closes them, it will use up a lot of memory very fast. But I think this is still much better since that memory can easily be reclaimed simply by closing the reports.

This also makes your reports independent of your main program. They can stay open even after closing the main program, and you can generate them from the command line manually, or from other sources as well.


I'm pretty late to this, but I have a real solution and can explain why!

It turns out that LocalReport here is using .NET Remoting to dynamically create a sub appdomain and run the report in order to avoid a leak internally somewhere. We then notice that, eventually, the report will release all the memory after 10 to 20 minutes. For people with a lot of PDFs being generated, this isn't going to work. However, the key here is that they are using .NET Remoting. One of the key parts to Remoting is something called "Leasing". Leasing means that it will keep that Marshal Object around for a while since Remoting is usually expensive to setup and its probably going to be used more than once. LocalReport RDLC is abusing this.

By default, the leasing time is... 10 minutes! Also, if something makes various calls into it, it adds another 2 minutes to the wait time! Thus, it can randomly be between 10 and 20 minutes depending how the calls line up. Luckily, you can change how long this timeout happens. Unluckily, you can only set this once per app domain... Thus, if you need remoting other than PDF generation, you will probably need to make another service running it so you can change the defaults. To do this, all you need to do is run these 4 lines of code at startup:

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

You'll see the memory use start to rise and then within a few seconds you should see the memory start coming back down. Took me days with a memory profiler to really track this down and realize what was happening.

You can't wrap ReportViewer in a using statement (Dispose crashes), but you should be able to if you use LocalReport directly. After that disposes, you can call GC.Collect() if you want to be doubly sure you are doing everything you can to free up that memory.

Hope this helps!

Edit

Apparently, you should call GC.Collect(0) after generating a PDF report or else it appears the memory use could still get high for some reason.

참고URL : https://stackoverflow.com/questions/6220915/very-high-memory-usage-in-net-4-0

반응형