2014-12-19 4 views
9

Si consideri il seguente programma, with all of HttpRequestMessage, and HttpResponseMessage, and HttpClient disposed properly. Alla fine finisce sempre con circa 50 MB di memoria, dopo la raccolta. Aggiungi uno zero al numero di richieste e la memoria non reclamata raddoppia.HttpClient con conseguente perdita del nodo in mscorlib

class Program 
    { 
     static void Main(string[] args) 
     { 
      var client = new HttpClient { 
        BaseAddress = new Uri("http://localhost:5000/")}; 

      var t = Task.Run(async() => 
      { 
       var resps = new List<Task<HttpResponseMessage>>(); 
       var postProcessing = new List<Task>(); 

       for (int i = 0; i < 10000; i++) 
       { 
        Console.WriteLine("Firing.."); 
        var req = new HttpRequestMessage(HttpMethod.Get, 
                 "test/delay/5"); 
        var tsk = client.SendAsync(req); 
        resps.Add(tsk); 
        postProcessing.Add(tsk.ContinueWith(async ts => 
        { 
         req.Dispose(); 
         var resp = ts.Result; 
         var content = await resp.Content.ReadAsStringAsync(); 
         resp.Dispose(); 
         Console.WriteLine(content); 
        })); 
       } 

       await Task.WhenAll(resps); 
       resps.Clear(); 
       Console.WriteLine("All requests done."); 
       await Task.WhenAll(postProcessing); 
       postProcessing.Clear(); 
       Console.WriteLine("All postprocessing done."); 
      }); 

      t.Wait(); 
      Console.Clear(); 

      var t2 = Task.Run(async() => 
      { 
       var resps = new List<Task<HttpResponseMessage>>(); 
       var postProcessing = new List<Task>(); 

       for (int i = 0; i < 10000; i++) 
       { 
        Console.WriteLine("Firing.."); 
        var req = new HttpRequestMessage(HttpMethod.Get, 
                 "test/delay/5"); 
        var tsk = client.SendAsync(req); 
        resps.Add(tsk); 
        postProcessing.Add(tsk.ContinueWith(async ts => 
        { 
         var resp = ts.Result; 
         var content = await resp.Content.ReadAsStringAsync(); 
         Console.WriteLine(content); 
        })); 
       } 

       await Task.WhenAll(resps); 
       resps.Clear(); 
       Console.WriteLine("All requests done."); 
       await Task.WhenAll(postProcessing); 
       postProcessing.Clear(); 
       Console.WriteLine("All postprocessing done."); 
      }); 

      t2.Wait(); 
      Console.Clear(); 
      client.Dispose(); 

      GC.Collect(); 
      Console.WriteLine("Done"); 
      Console.ReadLine(); 
     } 
    } 

Su una rapida indagine con un profiler di memoria, sembra che gli oggetti che occupano la memoria sono tutti di tipo Node<Object> all'interno mscorlib.

La mia però iniziale era quello, era un po 'dizionario interno o di una pila, dal momento che sono i tipi che utilizza il nodo come una struttura interna, ma io era in grado di girare su tutti i risultati per un generico Node<T> nel fonte di riferimento poiché questo è in realtà il tipo Node<object>.

Si tratta di un bug o di qualche tipo di ottimizzazione prevista (non considererei un consumo proporzionale di memoria sempre considerato un'ottimizzazione in alcun modo)? E puramente accademico, qual è il Node<Object>.

Qualsiasi aiuto nella comprensione di questo sarebbe molto apprezzato. Grazie :)

Aggiornamento: Per estrapolare i risultati per un set di test molto più ampio, l'ho ottimizzato leggermente limitandolo.

Ecco il programma modificato. E ora, it seems to stay consistent at 60-70MB, per un set di richieste da 1 milione. Sono ancora sconcertato da quello che sono veramente quei Node<object> e che è autorizzato a mantenere un numero così elevato di oggetti irrecuperabili.

E la conclusione logica delle differenze in questi due risultati mi porta a indovinare, questo potrebbe non essere un problema con HttpClient o WebRequest, piuttosto qualcosa radicato direttamente con async - Dal momento che la vera variante in questi due test è la numero di compiti asincroni incompleti esistenti in un dato momento. Questa è solo una speculazione dell'ispezione rapida.

static void Main(string[] args) 
{ 

    Console.WriteLine("Ready to start."); 
    Console.ReadLine(); 

    var client = new HttpClient { BaseAddress = 
        new Uri("http://localhost:5000/") }; 

    var t = Task.Run(async() => 
    { 
     var resps = new List<Task<HttpResponseMessage>>(); 
     var postProcessing = new List<Task>(); 

     for (int i = 0; i < 1000000; i++) 
     { 
      //Console.WriteLine("Firing.."); 
      var req = new HttpRequestMessage(HttpMethod.Get, "test/delay/5"); 
      var tsk = client.SendAsync(req); 
      resps.Add(tsk); 
      var n = i; 
      postProcessing.Add(tsk.ContinueWith(async ts => 
      { 
       var resp = ts.Result; 
       var content = await resp.Content.ReadAsStringAsync(); 
       if (n%1000 == 0) 
       { 
        Console.WriteLine("Requests processed: " + n); 
       } 

       //Console.WriteLine(content); 
      })); 

      if (n%20000 == 0) 
      { 
       await Task.WhenAll(resps); 
       resps.Clear(); 
      } 

     } 

     await Task.WhenAll(resps); 
     resps.Clear(); 
     Console.WriteLine("All requests done."); 
     await Task.WhenAll(postProcessing); 
     postProcessing.Clear(); 
     Console.WriteLine("All postprocessing done."); 
    }); 

    t.Wait(); 
    Console.Clear(); 
    client.Dispose(); 

    GC.Collect(); 
    Console.WriteLine("Done"); 
    Console.ReadLine(); 
} 
+0

'HttpClient',' 'HttpRequestMessage' e HttpResponseMessage' siete tutti usa e getta, ma di smaltire solo' HttpClient'. Disponi tutto ciò che ha bisogno di smaltimento (tramite 'using'), quindi controlla di nuovo. (Potrebbe benissimo aver ancora allocato oggetti 'Node <>', ma almeno gli articoli usa e getta non confonderanno il problema.) –

+0

Come ho già menzionato nella prima riga, con tutte le combinazioni di dispose. L'esempio dato non lo smaltisce, ma tutto ha come risultato la stessa perdita. In ogni caso, capisco il tuo punto. Lo modificherà per renderlo chiaro. –

+0

Provare combinazioni diverse in realtà non ha senso. Smaltire * meno * di quanto si suppone non può certamente aiutare a ridurre l'uso della memoria. –

risposta

11

Indaghiamo il problema con tutti gli strumenti che abbiamo in mano.

In primo luogo, diamo un'occhiata a quali sono questi oggetti, per fare ciò, ho inserito il codice indicato in Visual Studio e creato un'applicazione di console semplice. Fianco a fianco eseguo un semplice server HTTP su Node.js per soddisfare le richieste.

eseguire il client fino alla fine e inizi ad allegare WinDBG ad esso, ho ispezionare l'heap gestito e ottenere questi risultati:!

0:037> !dumpheap 
Address  MT  Size 
02471000 00779700  10 Free 
0247100c 72482744  84  
... 
Statistics: 
     MT Count TotalSize Class Name 
... 
72450e88  847  13552 System.Collections.Concurrent.ConcurrentStack`1+Node[[System.Object, mscorlib]] 
... 

Il comando dumpheap dump tutti gli oggetti nella heap gestito lì. Questo potrebbe includere oggetti che dovrebbero essere liberati (ma non ancora perché GC non è ancora entrato in gioco). Nel nostro caso, dovrebbe essere raro perché abbiamo appena chiamato GC.Collect() prima della stampa e nient'altro dovrebbe essere eseguito dopo la stampa.

La nota è la riga specifica in alto. Questo dovrebbe essere l'oggetto Nodo a cui ti stai riferendo nella domanda.

Successivamente, diamo un'occhiata ai singoli oggetti di quel tipo, prendiamo il valore MT di quell'oggetto e quindi richiamiamo! Dumpheap di nuovo così, questo filtrerà solo gli oggetti a cui siamo interessati.

0:037> !dumpheap -mt 72450e88 
Address  MT  Size 
025b9234 72450e88  16  
025b93dc 72450e88  16  
... 

Ora afferrare un casuale nella lista, e poi chiede il debugger perché questo oggetto è ancora sul mucchio richiamando il comando gcroot come segue:!

0:037> !gcroot 025bbc8c 
Thread 6f24: 
    0650f13c 79752354 System.Net.TimerThread.ThreadProc() 
     edi: (interior) 
      -> 034734c8 System.Object[] 
      -> 024915ec System.PinnableBufferCache 
      -> 02491750 System.Collections.Concurrent.ConcurrentStack`1[[System.Object, mscorlib]] 
      -> 09c2145c System.Collections.Concurrent.ConcurrentStack`1+Node[[System.Object, mscorlib]] 
      -> 09c2144c System.Collections.Concurrent.ConcurrentStack`1+Node[[System.Object, mscorlib]] 
      -> 025bbc8c System.Collections.Concurrent.ConcurrentStack`1+Node[[System.Object, mscorlib]] 

Found 1 unique roots (run '!GCRoot -all' to see all roots). 

Ora è abbastanza ovvio che abbiamo una cache e che la cache mantiene uno stack, con lo stack implementato come elenco collegato. Se riflettiamo ulteriormente vedremo nella fonte di riferimento, come viene usata questa lista. Per fare questo, si deve prima ispezionare l'oggetto cache stesso, utilizzando! DumpObj

0:037> !DumpObj 024915ec 
Name:  System.PinnableBufferCache 
MethodTable: 797c2b44 
EEClass:  795e5bc4 
Size:  52(0x34) bytes 
File:  C:\WINDOWS\Microsoft.Net\assembly\GAC_MSIL\System\v4.0_4.0.0.0__b77a5c561934e089\System.dll 
Fields: 
     MT Field Offset     Type VT  Attr Value Name 
724825fc 40004f6  4  System.String 0 instance 024914a0 m_CacheName 
7248c170 40004f7  8 ...bject, mscorlib]] 0 instance 0249162c m_factory 
71fe994c 40004f8  c ...bject, mscorlib]] 0 instance 02491750 m_FreeList 
71fed558 40004f9  10 ...bject, mscorlib]] 0 instance 025b93b8 m_NotGen2 
72484544 40004fa  14   System.Int32 1 instance  0 m_gen1CountAtLastRestock 
72484544 40004fb  18   System.Int32 1 instance 605289781 m_msecNoUseBeyondFreeListSinceThisTime 
7248fc58 40004fc  2c  System.Boolean 1 instance  0 m_moreThanFreeListNeeded 
72484544 40004fd  1c   System.Int32 1 instance  244 m_buffersUnderManagement 
72484544 40004fe  20   System.Int32 1 instance  128 m_restockSize 
7248fc58 40004ff  2d  System.Boolean 1 instance  1 m_trimmingExperimentInProgress 
72484544 4000500  24   System.Int32 1 instance  0 m_minBufferCount 
72484544 4000501  28   System.Int32 1 instance  0 m_numAllocCalls 

Ora vediamo qualcosa di interessante, lo stack è effettivamente utilizzato sia in elenco libero per la cache. Il codice sorgente ci dice come viene utilizzato l'elenco libero, in particolare, nel metodo Gratis() mostrato di seguito:

http://referencesource.microsoft.com/#mscorlib/parent/parent/parent/parent/InternalApis/NDP_Common/inc/PinnableBufferCache.cs

/// <summary> 
/// Return a buffer back to the buffer manager. 
/// </summary> 
[System.Security.SecuritySafeCritical] 
internal void Free(object buffer) 
{ 
    ... 
    m_FreeList.Push(buffer); 
} 

Così che è, quando il chiamante è fatto con il buffer, ritorna alla cache, la cache poi mettere che nella lista libera, la lista libera viene quindi utilizzato a scopo di assegnazione

[System.Security.SecuritySafeCritical] 
internal object Allocate() 
{ 
    // Fast path, get it from our Gen2 aged m_FreeList. 
    object returnBuffer; 
    if (!m_FreeList.TryPop(out returnBuffer)) 
    Restock(out returnBuffer); 
    ... 
} 

Ultimo ma non meno importante, cerchiamo di capire il motivo per cui la cache stessa non viene liberata quando abbiamo finito con tutte quelle richieste HTTP? Ecco perché Aggiungendo un breakpoint su mscorlib.dll! System.Collections.Concurrent.ConcurrentStack.Push(), vediamo il seguente stack di chiamate (beh, questo potrebbe essere solo uno dei casi d'uso della cache, ma questo è rappresentativo)

mscorlib.dll!System.Collections.Concurrent.ConcurrentStack<object>.Push(object item) 
System.dll!System.PinnableBufferCache.Free(object buffer) 
System.dll!System.Net.HttpWebRequest.FreeWriteBuffer() 
System.dll!System.Net.ConnectStream.WriteHeadersCallback(System.IAsyncResult ar) 
System.dll!System.Net.LazyAsyncResult.Complete(System.IntPtr userToken) 
System.dll!System.Net.ContextAwareResult.Complete(System.IntPtr userToken) 
System.dll!System.Net.LazyAsyncResult.ProtectedInvokeCallback(object result, System.IntPtr userToken) 
System.dll!System.Net.Sockets.BaseOverlappedAsyncResult.CompletionPortCallback(uint errorCode, uint numBytes, System.Threading.NativeOverlapped* nativeOverlapped) 
mscorlib.dll!System.Threading._IOCompletionCallback.PerformIOCompletionCallback(uint errorCode, uint numBytes, System.Threading.NativeOverlapped* pOVERLAP) 

A WriteHeadersCallback, abbiamo finito di scrivere le intestazioni, quindi restituiamo il buffer alla cache. A questo punto il buffer viene reinserito nella lista libera e quindi assegniamo un nuovo nodo stack. La cosa fondamentale da notare è che l'oggetto cache è un membro statico di HttpWebRequest.

http://referencesource.microsoft.com/#System/net/System/Net/HttpWebRequest.cs

... 
private static PinnableBufferCache _WriteBufferCache = new PinnableBufferCache("System.Net.HttpWebRequest", CachedWriteBufferSize); 
... 
// Return the buffer to the pinnable cache if it came from there. 
internal void FreeWriteBuffer() 
{ 
    if (_WriteBufferFromPinnableCache) 
    { 
    _WriteBufferCache.FreeBuffer(_WriteBuffer); 
    _WriteBufferFromPinnableCache = false; 
    } 
    _WriteBufferLength = 0; 
    _WriteBuffer = null; 
} 
... 

Così ci andiamo, la cache viene condivisa tra tutte le richieste e non viene rilasciato quando tutte le richieste sono fatte.

2

Abbiamo avuto gli stessi problemi, quando usiamo System.Net.WebRequest per fare alcune richieste http. La dimensione del processo w3wp era compresa tra 4 e 8 GB, perché non abbiamo un carico costante. A volte abbiamo 10 richieste al secondo e 1000 altre volte. Ovviamente il buffer non viene riutilizzato nello stesso scenario.

We Are Change tutto posto quando viene utilizzato System.Net.WebRequest su System.Net.Http.HttpClient perché non ha nessun pool di buffer.

Se si dispone di molte richieste tramite httpclient, impostarlo come variabile statica per evitare Socket leaks.

enter image description here

penso che più modo semplice analizzare questo problema - usa PerfView. Questa applicazione può mostrare l'albero di riferimento in modo da poter mostrare il caso di root del problema.

enter image description here enter image description here

Problemi correlati