11

.NET IHttpAsyncHandler(および程度は低いですがIHttpHandler)が、同時Web要求にさらされると、メモリをリークすることに気づきました。

私のテストでは、Visual Studio Webサーバー(Cassini)が6MBのメモリから100MBを超えるメモリにジャンプし、テストが終了すると、再利用されることはありません。

問題は簡単に再現できます。2つのプロジェクトで新しいソリューション(LeakyHandler)を作成します。

  1. ASP.NET Webアプリケーション(LeakyHandler.WebApp)
  2. コンソールアプリケーション(LeakyHandler.ConsoleApp)

LeakyHandler.WebAppの場合:

  1. IHttpAsyncHandlerを実装するTestHandlerというクラスを作成します。
  2. リクエスト処理で、短いスリープを実行し、応答を終了します。
  3. HTTPハンドラーをtest.ashxとしてWeb.configに追加します。

LeakyHandler.ConsoleAppの場合:

  1. test.ashxへの多数のHttpWebRequestを生成し、それらを非同期で実行します。

HttpWebRequests(sampleSize)の数が増えると、メモリリークがますます明らかになります。

LeakyHandler.WebApp> TestHandler.cs

namespace LeakyHandler.WebApp
{
    public class TestHandler : IHttpAsyncHandler
    {
        #region IHttpAsyncHandler Members

        private ProcessRequestDelegate Delegate { get; set; }
        public delegate void ProcessRequestDelegate(HttpContext context);

        public IAsyncResult BeginProcessRequest(HttpContext context, AsyncCallback cb, object extraData)
        {
            Delegate = ProcessRequest;
            return Delegate.BeginInvoke(context, cb, extraData);
        }

        public void EndProcessRequest(IAsyncResult result)
        {
            Delegate.EndInvoke(result);
        }

        #endregion

        #region IHttpHandler Members

        public bool IsReusable
        {
            get { return true; }
        }

        public void ProcessRequest(HttpContext context)
        {
            Thread.Sleep(10);
            context.Response.End();
        }

        #endregion
    }
}

LeakyHandler.WebApp> Web.config

<?xml version="1.0"?>

<configuration>
    <system.web>
        <compilation debug="false" />
        <httpHandlers>
            <add verb="POST" path="test.ashx" type="LeakyHandler.WebApp.TestHandler" />
        </httpHandlers>
    </system.web>
</configuration>

LeakyHandler.ConsoleApp> Program.cs

namespace LeakyHandler.ConsoleApp
{
    class Program
    {
        private static int sampleSize = 10000;
        private static int startedCount = 0;
        private static int completedCount = 0;

        static void Main(string[] args)
        {
            Console.WriteLine("Press any key to start.");
            Console.ReadKey();

            string url = "http://localhost:3000/test.ashx";
            for (int i = 0; i < sampleSize; i++)
            {
                HttpWebRequest request = (HttpWebRequest)WebRequest.Create(url);
                request.Method = "POST";
                request.BeginGetResponse(GetResponseCallback, request);

                Console.WriteLine("S: " + Interlocked.Increment(ref startedCount));
            }

            Console.ReadKey();
        }

        static void GetResponseCallback(IAsyncResult result)
        {
            HttpWebRequest request = (HttpWebRequest)result.AsyncState;
            HttpWebResponse response = (HttpWebResponse)request.EndGetResponse(result);
            try
            {
                using (Stream stream = response.GetResponseStream())
                {
                    using (StreamReader streamReader = new StreamReader(stream))
                    {
                        streamReader.ReadToEnd();
                        System.Console.WriteLine("C: " + Interlocked.Increment(ref completedCount));
                    }
                }
                response.Close();
            }
            catch (Exception ex)
            {
                System.Console.WriteLine("Error processing response: " + ex.Message);
            }
        }
    }
}

デバッグの更新

WinDbgを使用してダンプファイルを調べましたが、いくつかの疑わしいタイプがメモリに保持されており、解放されていません。10,000のサンプルサイズでテストを実行するたびに、これらのオブジェクトがさらに10,000個メモリに保持されることになります。

  • System.Runtime.Remoting.ServerIdentity
  • System.Runtime.Remoting.ObjRef
  • Microsoft.VisualStudio.WebHost.Connection
  • System.Runtime.Remoting.Messaging.StackBuilderSink
  • System.Runtime.Remoting.ChannelInfo
  • System.Runtime.Remoting.Messaging.ServerObjectTerminatorSink

これらのオブジェクトは第2世代のヒープにあり、強制的に完全なガベージコレクションを実行した後でも収集されません。

重要な注意点

この問題は、シーケンシャルリクエストを強制する場合でも存在し、Thread.Sleep(10)inがなくてもProcessRequest、はるかに微妙です。この例は、問題をより簡単に明らかにすることで問題を悪化させますが、基本は同じです。

4

4 に答える 4

14

私はあなたのコードを見て(そしてそれを実行して)、あなたが見ているメモリの増加が実際にはメモリリークであるとは思わない。

あなたが抱えている問題は、あなたの呼び出しコード(コンソールアプリ)が本質的にタイトなループで実行されているということです。

ただし、ハンドラーは各要求を処理する必要があり、さらに。によって「ノブル」されますThread.Sleep(10)。これの実際的な結果は、ハンドラーが入ってくるリクエストに追いつくことができないということです。そのため、その「ワーキングセット」は、処理されるのを待ってキューに入るリクエストが増えるにつれて大きくなります。

私はあなたのコードを取り、コンソールアプリにAutoResetEventを追加し、

.WaitOne()request.BeginGetResponse(GetResponseCallback, request);

.Set()streamReader.ReadToEnd();

これには、呼び出しを同期する効果があるため、最初の呼び出しがコールバック(および完了)するまで次の呼び出しを行うことはできません。あなたが見ている行動は消えます。

要約すると、これは純粋に暴走した状況であり、実際にはメモリリークではないと思います。

注:GetResponseCallbackメソッドで、次の方法でメモリを監視しました。

 GC.Collect();
 GC.WaitForPendingFinalizers();
 Console.WriteLine(GC.GetTotalMemory(true));

[アントンからのコメントに応じて編集] ここではまったく問題がないことを示唆しているわけではありません。ハンドラーのこのハンマーが実際の使用シナリオであるような使用シナリオの場合、明らかに問題があります。私のポイントは、それはメモリリークの問題ではなく、容量の問題であるということです。これを解決するためのアプローチは、おそらく、より高速に実行できるハンドラーを作成するか、複数のサーバーにスケールアウトすることなどです。

リークとは、リソースが終了した後に保持され、ワー​​キングセットのサイズが大きくなることです。これらのリソースは「終了」しておらず、キューに入れられ、サービスを受けるのを待っています。それらが完成したら、正しくリリースされていると思います。

[アントンのさらなるコメントに応じて編集] OK-私は何かを発見しました!これは、IISでは発生しないカッシーニの問題だと思います。カッシーニ(Visual Studio開発Webサーバー)でハンドラーを実行していますか?

Cassiniでのみ実行している場合、これらのリークのあるSystem.Runtime.Remoting名前空間インスタンスも表示されます。ハンドラーをIISで実行するように設定した場合、それらは表示されません。これがあなたに当てはまるかどうか確認できますか?

これは、私が見た他のリモーティング/カッシーニの問題を思い出させます。IIRCは、モジュールのBeginRequestに存在する必要があり、モジュールのライフサイクルの最後にも存在する必要があるIPrincipalのようなインスタンスを持ち、IISではなくカッシーニのMarshalByRefObjectから派生する必要があります。何らかの理由で、カッシーニはIISではないリモーティングを内部で行っているようです。

于 2010-05-19T16:50:03.197 に答える
4

測定しているメモリは割り当てられている可能性がありますが、CLRによって使用されていません。確認するには、電話してみてください。

GC.Collect();
context.Response.Write(GC.GetTotalMemory(true));

ProcessRequest()。コンソールアプリにサーバーからの応答を報告させて、ライブオブジェクトによって実際に使用されているメモリの量を確認します。この数がかなり安定している場合、CLRは、十分なRAMがそのまま使用可能であると見なすため、GCの実行を無視しているだけです。その数が着実に増加している場合は、実際にメモリリークが発生しています。これは、WinDbgやSOS.dll、またはその他の(商用)ツールで問題が発生する可能性があります。

編集: OK、そのときは実際のメモリリークがあるようです。次のステップは、それらのオブジェクトを保持しているものを把握することです。そのためにSOS !gcrootコマンドを使用できます。ここに.NET2.0の適切な説明がありますが、これを.NET 4.0で再現できる場合は、そのSOS.dllに役立つツールがはるかに優れています。http://blogs.msdn.com/tess/archive/2010を参照してください。 /03/01/new-commands-in-sos-for-net-4-0-part-1.aspx

于 2010-05-18T23:29:38.200 に答える
1

コードに問題はなく、メモリリークもありません。コンソールアプリケーションを数回続けて実行してみてください(またはサンプルサイズを増やしますが、スリープしている同時リクエストが多すぎると、最終的にhttpリクエストが拒否されることに注意してください)。

あなたのコードで、Webサーバーのメモリ使用量が約130MBに達すると、ガベージコレクションが開始されて約60MBに減少するのに十分なプレッシャーがあったことがわかりました。

おそらくそれはあなたが期待する振る舞いではないかもしれませんが、ランタイムは、GCで時間を費やすよりも、迅速な着信要求に迅速に応答することが重要であると判断しました。

于 2010-05-18T23:16:41.700 に答える
-2

コードに問題がある可能性が非常に高くなります。

最初に確認するのは、コード内のすべてのイベントハンドラーを切り離すかどうかです。すべての+=は、イベントハンドラーの-=によってミラーリングされる必要があります。これは、ほとんどの.Netアプリケーションがメモリリークを作成する方法です。

于 2010-05-13T06:23:00.247 に答える