web-dev-qa-db-ja.com

.NET 4.0の非常に高いメモリ使用量

最近、.NET 3.5から.NET 4.0に移行したC#Windowsサービスがあります。他のコードの変更は行われませんでした。

3.5で実行する場合、特定の作業負荷のメモリ使用率は約1.5 GBのメモリで、スループットは1秒あたり20倍でした。 (この質問の文脈では、Xは重要ではありません。)

4.0で実行されているまったく同じサービスは、3GB〜5GB +のメモリを使用し、1秒あたり4 X未満になります。実際、システムの使用率が99%に達し、ページファイルのスワップが途切れるまで、メモリ使用量が増加し続けるため、通常、サービスは停止します。

これがガベージコレクションに関係するのかどうかはわかりませんが、それを理解するのに苦労しています。私のウィンドウサービスは、以下に示す構成ファイルスイッチを介して「サーバー」GCを使用します。

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

このオプションをfalseに変更しても違いはありませんでした。さらに、4.0の新しいGCで行った読み取りから、大きな変更はワークステーションGCモードにのみ影響し、サーバーGCモードには影響しません。したがって、GCは問題とは何の関係もない可能性があります。

アイデア?

61
RMD

これは興味深いものでした。

根本的な原因は、これを.NET 4.0上で実行した場合のSQL Server Reporting ServicesのLocalReportクラス(v2010)の動作の変更であることが判明しました。

基本的に、MicrosoftはRDLC処理の動作を変更して、レポートが処理されるたびに別のアプリケーションドメインで実行されるようにしました。これは実際には、アプリケーションドメインからアセンブリをアンロードできないことによって引き起こされるメモリリークに対処するために具体的に行われました。 LocalReportクラスがRDLCファイルを処理すると、実際にオンザフライでアセンブリが作成され、アプリドメインにロードされます。

私の場合、大量のレポートを処理していたため、非常に多くのSystem.Runtime.Remoting.ServerIdentityオブジェクトが作成されていました。これは、RLDCの処理にリモーティングが必要な理由について混乱していたため、原因への私のチップオフでした。

もちろん、別のアプリドメインのクラスのメソッドを呼び出すには、まさにリモート処理を使用します。 .NET 3.5では、RDLC-Assemblyが同じアプリドメインに読み込まれたため、これはデフォルトでは必要ありませんでした。ただし、.NET 4.0では、新しいアプリドメインがデフォルトで作成されます。

修正はかなり簡単でした。最初に、次の構成を使用してレガシーセキュリティポリシーを有効にする必要がありました。

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

次に、次を呼び出して、RDLCをサービスと同じアプリドメインで強制的に処理する必要がありました。

myLocalReport.ExecuteReportInCurrentAppDomain(AppDomain.CurrentDomain.Evidence);

これで問題は解決しました。

83
RMD

私はこの正確な問題に遭遇しました。そして、アプリドメインが作成され、クリーンアップされないことは事実です。ただし、レガシーに戻すことはお勧めしません。 ReleaseSandboxAppDomain()でクリーンアップできます。

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

私がクリーンアップするために行う他のいくつかのこと:

SubreportProcessingイベントのサブスクライブ解除、データソースのクリア、レポートの破棄。

Windowsサービスは1秒間にいくつかのレポートを処理し、リークはありません。

11
sues999

あなたはしたいかもしれない

おそらくいくつかのAPIがセマンティクスを変更したか、フレームワークの4.0バージョンにバグがある可能性があります

4
sehe

完全を期すために、誰かが同等のASP.Net web.config設定、次のとおりです。

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

ExecuteReportInCurrentAppDomainは同じように機能します。

おかげで ソーシャルMSDNリファレンス

2
StuartLC

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メソッドは、レポート定義を取得し、関連する引数を使用してデータセットを取得し、データやその他の引数をレポートに渡し、レポートをフォームのレポートビューアーに配置して、フォームへの参照。このプロセスの多くを抽出して、基本的に「このデータとこれらの引数を使用して、このアセンブリからこのレポートのフォームをください」と言うことができることに注意してください。

また、両方のプログラムがこのプロジェクトに関連するデータ型を表示できる必要があるため、両方のプログラムが参照を共有できる独自のライブラリにデータクラスを抽出したことを確認してください。メインプログラムとレポートプログラムの間に循環依存関係があるため、すべてのデータクラスをメインプログラムに含めることはできません。

引数でやりすぎないでください。レポートプログラムで必要なデータベースクエリを実行します。オブジェクトの膨大なリストを渡さないでください(おそらく動作しません)。データベースIDフィールド、日付範囲などの単純なものを渡す必要があります。特に複雑なパラメーターがある場合は、UIのその部分もレポートプログラムにプッシュし、コマンドラインで引数として渡さないようにする必要があります。

メインプログラムにレポートプログラムへの参照を配置することもできます。結果の.exeおよび関連する.dllは同じ出力フォルダーにコピーされます。その後、パスを指定せずに実行し、実行可能ファイル名を単独で使用できます(例: "SomethingReports.exe")。メインプログラムからレポートdllを削除することもできます。

これに関する1つの問題は、レポートプログラムを実際に公開したことがない場合、マニフェストエラーが発生することです。マニフェストを生成するために一度だけダミーで公開すると、動作します。

これが機能するようになったら、レポートを印刷するときに通常のプログラムのメモリが一定であるのを見るのは非常にいいことです。レポートプログラムが表示され、メインプログラムよりも多くのメモリを消費し、その後消えて、メインプログラムがすでに使用していたメモリを消費しないように完全にクリーンアップします。

別の問題として、各レポートインスタンスが以前よりも多くのメモリを占有するようになる可能性があります。これは、それらが完全に別個のプログラムになったためです。ユーザーが大量のレポートを印刷し、それらを一度も閉じない場合、大量のメモリが非常に速く消費されます。しかし、レポートを閉じるだけで簡単にメモリを取り戻すことができるため、これはさらに優れていると思います。

これにより、レポートはメインプログラムから独立します。メインプログラムを閉じた後でも開いたままにすることができ、コマンドラインまたは他のソースから手動で生成することもできます。

1
Dave Cousineau

私はこれにかなり遅れていますが、本当の解決策があり、理由を説明できます!

LocalReportはここで.NET Remotingを使用して動的にサブappdomainを作成し、どこかで内部的にリークを回避するためにレポートを実行することがわかります。その後、最終的に、レポートは10〜20分後にすべてのメモリを解放します。多数のPDFが生成される場合、これは機能しません。ただし、ここで重要なのは、.NET Remotingを使用していることです。 Remotingの重要な部分の1つは、「リース」と呼ばれるものです。リモーティングは通常セットアップに費用がかかり、おそらく複数回使用されるため、リースはそのマーシャルオブジェクトをしばらく保持します。 LocalReport RDLCはこれを悪用しています。

デフォルトでは、リース時間は... 10分です!また、何かがさまざまな呼び出しを行うと、待機時間にさらに2分が追加されます。したがって、コールの並び方に応じて、ランダムに10〜20分になることがあります。幸いなことに、このタイムアウトが発生する時間を変更できます。残念ながら、これはアプリドメインごとに1回しか設定できません。したがって、PDF生成以外のリモート処理が必要な場合は、デフォルトを変更できるように別のサービスを実行する必要があります。これを行うには、起動時に次の4行のコードを実行するだけです。

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

メモリ使用量が増加し始め、数秒以内にメモリ使用量が減少し始めます。これを実際に追跡し、何が起きているのかを理解するために、メモリプロファイラーで何日もかかった.

UsingステートメントでReportViewerをラップすることはできません(Disposeがクラッシュします)が、LocalReportを直接使用すればできるはずです。破棄した後、GC.Collect()を呼び出して、メモリを解放するためにできることをすべて確実に実行することを二重に確認したい場合は、GC.Collect()を呼び出すことができます。

お役に立てれば!

編集

どうやら、PDFレポートを生成した後にGC.Collect(0)を呼び出す必要があります。そうしないと、何らかの理由でメモリ使用量が高くなる可能性があります。

0
Daniel Lorenz