c# memory memory-leaks httpclient mscorlib

c# - HttpClient dando como resultado la fuga del nodo<Objeto> en mscorlib



memory memory-leaks (2)

Investiguemos el problema con todas las herramientas que tenemos a mano.

En primer lugar, echemos un vistazo a cuáles son esos objetos, para hacerlo, puse el código dado en Visual Studio y creé una aplicación de consola simple. De lado a lado, ejecuto un servidor HTTP simple en Node.js para atender las solicitudes.

Ejecutar el cliente hasta el final y comenzar a adjuntar WinDBG, inspecciono el montón administrado y obtengo estos resultados:

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]] ...

El comando! Dumpheap arroja todos los objetos en el montón gestionado allí. Eso podría incluir objetos que deberían liberarse (pero aún no porque GC aún no se ha activado). En nuestro caso, eso debería ser raro porque acabamos de llamar a GC.Collect () antes de la impresión y no debe ejecutarse nada más después de la impresión.

Digno de notar es la línea específica anterior. Ese debería ser el objeto Node al que se refiere en la pregunta.

A continuación, veamos los objetos individuales de ese tipo, tomamos el valor MT de ese objeto y luego invocamos! Dumpheap nuevamente de esta manera, esto filtrará solo los objetos que nos interesan.

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

Ahora agarrando uno al azar en la lista, y luego le pregunta al depurador por qué este objeto todavía está en el montón invocando el comando! Gcroot de la siguiente manera:

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).

Ahora es bastante obvio que tenemos un caché, y ese caché mantiene una pila, con la pila implementada como una lista vinculada. Si reflexionamos más, veremos en la fuente de referencia cómo se usa esa lista. Para hacer eso, primero inspeccionemos el objeto de caché, usando! 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

Ahora que vemos algo interesante, la pila se usa realmente como una lista gratuita para el caché. El código fuente nos dice cómo se usa la lista gratuita, en particular, en el método Free () que se muestra a continuación:

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); }

Así que eso es todo, cuando la persona que llama termina el búfer, regresa a la memoria caché, la memoria caché la coloca en la lista libre, la lista gratuita se usa para fines de asignación

[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); ... }

Por último, pero no por eso menos importante, vamos a entender por qué la memoria caché en sí no se libera cuando terminamos todas esas solicitudes HTTP. Aquí está el por qué. Al agregar un punto de interrupción en mscorlib.dll! System.Collections.Concurrent.ConcurrentStack.Push (), vemos la siguiente pila de llamadas (bueno, este podría ser solo uno de los casos de uso de caché, pero esto es representativo)

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)

En WriteHeadersCallback, hemos terminado de escribir los encabezados, por lo que devolvemos el búfer al caché. En este punto, el búfer vuelve a la lista libre y, por lo tanto, asignamos un nuevo nodo de pila. La clave a tener en cuenta es que el objeto de caché es un miembro estático de 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; } ...

Así que ahí vamos, el caché se comparte en todas las solicitudes y no se libera cuando se realizan todas las solicitudes.

Considere el siguiente programa, with all of HttpRequestMessage, and HttpResponseMessage, and HttpClient disposed properly. Siempre termina con aproximadamente 50 MB de memoria al final, después de la recopilación. Agregue un cero a la cantidad de solicitudes y la memoria no recuperada se duplica.

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(); } }

En una investigación rápida con un generador de perfiles de memoria, parece que los objetos que ocupan la memoria son todos del tipo Node<Object> dentro de mscorlib.

Sin embargo, mi inicial fue que era un diccionario interno o una pila, ya que son los tipos que usan Nodo como estructura interna, pero no pude encontrar ningún resultado para un Node<T> genérico Node<T> en la fuente de referencia ya que este en realidad es el tipo de Node<object> .

¿Es esto un error, o una especie de optimización esperada (no consideraría un consumo proporcional de memoria siempre retenido como una optimización de ninguna manera)? Y puramente académico, ¿cuál es el Node<Object> .

Cualquier ayuda para entender esto sería muy apreciada. Gracias :)

Actualización: para extrapolar los resultados de un conjunto de pruebas mucho más grande, lo optimicé ligeramente al estrangularlo.

Aquí está el programa cambiado. Y ahora, it seems to stay consistent at 60-70MB , para un conjunto de solicitud de 1 millón. Todavía estoy desconcertado por lo que esos Node<object> realmente son, y se le permite mantener una cantidad tan alta de objetos irrecuperables.

Y la conclusión lógica de las diferencias en estos dos resultados me lleva a adivinar, esto puede no ser realmente un problema con HttpClient o WebRequest, más bien algo rooteado directamente con async - Dado que la variante real en estas dos pruebas es la cantidad de asincronías incompletas tareas que existen en un punto dado en el tiempo. Esto es meramente una especulación de la inspección rápida.

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(); }


Tuvimos los mismos problemas cuando usamos System.Net.WebRequest para hacer algunas solicitudes http. El tamaño del proceso w3wp tenía un rango de 4-8 Gb, porque no tenemos una carga constante. A veces tenemos 10 solicitudes por segundo y 1000 en otro momento. Por supuesto, el búfer no se reutiliza en el mismo escenario.

Cambiamos todos los sitios cuando utilizamos System.Net.WebRequest en System.Net.Http.HttpClient porque no tiene ningún grupo de búferes.

Si tiene muchas solicitudes a través de su httpclient, hágalo como una variable estática para evitar filtraciones de Socket .

Creo que de una forma más simple analizar este problema: use PerfView . Esta aplicación puede mostrar el árbol de referencia para que pueda mostrar el caso raíz de su problema.