¿Cómo puedo evitar que BufferManager / PooledBufferManager en mi aplicación de cliente WCF desperdicie memoria?

Analizando una aplicación de cliente WCF (que no escribí y de la que aún no sé demasiado) que habla con un grupo de servicios a través de SOAP y después de correr durante un par de días lanzará OutOfMemoryException, descubrí que. PooledBufferManager de .net nunca libere búferes sin usar, incluso cuando la aplicación se queda sin memoria, lo que lleva a OOMEs.

Esto, por supuesto, está de acuerdo con la especificación: http://msdn.microsoft.com/en-us/library/ms405814.aspx

El grupo y sus almacenamientos intermedios se destruyen […] cuando el grupo de búferes es reclamado por la recolección de basura.

No dude en responder solo a una de las siguientes preguntas, ya que tengo un montón de preguntas, algunas de carácter más general y otras específicas para el uso que nuestra aplicación hace de BufferManager.

Primero, un par de preguntas generales sobre el BufferManager (agrupado por defecto):

1) En un entorno en el que tenemos GC, ¿por qué necesitaríamos un BufferManager que se aferre a la memoria no utilizada, incluso cuando eso conduzca a OOME? Lo sé, existe BufferManager.Clear (), que puedes utilizar para eliminar manualmente todos los buffers, si tienes acceso al BufferManager, eso es. Ver más abajo para saber por qué no parece tener acceso.

2) A pesar de que MS afirma que “este proceso es mucho más rápido que crear y destruir un buffer cada vez que necesita usar uno”, ¿no deberían dejarlo hasta el GC (y su LOH, por ejemplo) y optimizar el GC en cambio?

3) Al hacer un BufferManager.Take (33 * 1024 * 1024), obtendré un buffer de 64M, ya que el PooledBufferManager almacenará en caché ese buffer para su posterior reutilización, lo que podría – bueno, en mi caso no es así y por lo tanto es puro desperdicio de memoria, digamos que 34M o 50M o 64M son necesarios. Entonces, ¿fue prudente crear un BufferManager potencialmente muy inútil como este, que se utiliza (de forma predeterminada, supongo) por HttpsChannelFactory? No estoy viendo cómo debería importar el rendimiento de la asignación de memoria, especialmente cuando hablamos de WCF y servicios de red con los que la aplicación hablará cada 10 segundos TOPS, normalmente muchos más segundos o incluso minutos.

Ahora algunas preguntas más específicas relacionadas con el uso de nuestra aplicación de BufferManagers. La aplicación se conecta a un par de diferentes servicios de WCF. Para cada uno de ellos mantenemos un grupo de conexiones para las conexiones http, ya que las conexiones pueden ocurrir al mismo tiempo.

Inspeccionando el objeto más grande en un volcado de almacenamiento dynamic, un conjunto de bytes de 64M que solo se utilizó una vez en nuestra aplicación en el momento de la inicialización y no es necesario después, ya que la respuesta del servicio es grande solo en el momento de la inicialización, por cierto. es típico para muchas aplicaciones que he usado, aunque eso podría estar sujeto a opimización (almacenamiento en caché en disco, etc.). Un análisis de raíz de GC en WinDbg produce lo siguiente (desinfecté los nombres de nuestras clases propietarias en ‘MyServiceX’, etc.):

0:000:x86> !gcroot -nostacks 193e1000 DOMAIN(00B8CCD0):HANDLE(Pinned):4d1330:Root:0e5b9c50(System.Object[])-> 035064f0(MyServiceManager)-> 0382191c(MyHttpConnectionPool`1[[MyServiceX, MyLib]])-> 03821988(System.Collections.Generic.Queue`1[[MyServiceX, MyLib]])-> 038219a8(System.Object[])-> 039c05b4(System.Runtime.Remoting.Proxies.__TransparentProxy)-> 039c0578(System.ServiceModel.Channels.ServiceChannelProxy)-> 039c0494(System.ServiceModel.Channels.ServiceChannel)-> 039bee30(System.ServiceModel.Channels.ServiceChannelFactory+ServiceChannelFactoryOverRequest)-> 039beea4(System.ServiceModel.Channels.HttpsChannelFactory)-> 039bf2c0(System.ServiceModel.Channels.BufferManager+PooledBufferManager)-> 039c02f4(System.Object[])-> 039bff24(System.ServiceModel.Channels.BufferManager+PooledBufferManager+BufferPool)-> 039bff44(System.ServiceModel.SynchronizedPool`1[[System.Byte[], mscorlib]])-> 039bffa0(System.ServiceModel.SynchronizedPool`1+GlobalPool[[System.Byte[], mscorlib]])-> 039bffb0(System.Collections.Generic.Stack`1[[System.Byte[], mscorlib]])-> 12bda2bc(System.Byte[][])-> 193e1000(System.Byte[]) 

Ver las raíces de gc para otras matrices de bytes administradas por un BufferManager revela que otros servicios (no ‘MyServiceX’) tienen diferentes instancias de BufferPool, por lo que cada uno desperdicia su propia memoria, ni siquiera comparten el desperdicio.

4) ¿Estamos haciendo algo mal aquí? No soy un experto en WCF de ninguna manera, así que ¿podríamos hacer que todas las instancias de HttpsChannelFactory usen el mismo BufferManager?

5) O tal vez incluso mejor, ¿podríamos decirle a todas las instancias de HttpsChannelFactory que NO utilicen BufferManagers en absoluto y pedirle al GC que haga su condenado trabajo, que es “administrar la memoria”?

6) Si las preguntas 4) y 5) no se pueden responder, ¿podría obtener acceso al BufferManager de todas las instancias de HttpsChannelFactory y llamar manualmente a .Clear () sobre ellas? Esto dista mucho de ser la solución óptima, pero ya ayudaría, en mi caso, liberaría no solo el mencionado 64M, sino también 64M + 32M + 16M + 8M + 4M + 2M en una sola instancia de servicio. Solo eso haría que mi aplicación dure mucho más tiempo sin tener problemas de memoria (y no, no tenemos un problema de pérdida de memoria, salvo BufferManager, aunque consumimos mucha memoria y acumulamos una gran cantidad de datos a lo largo del curso de muchos días, pero ese no es el problema aquí)

Creo que tengo respuesta a su pregunta n. ° 5:

5) O tal vez incluso mejor, ¿podríamos decirle a todas las instancias de HttpsChannelFactory que NO utilicen BufferManagers en absoluto y pedirle al GC que haga su condenado trabajo, que es “administrar la memoria”?

Existe un parámetro de enlace MaxBufferPoolSize, que controla el tamaño máximo de los buffers en BufferManager. Al ponerlo en 0, se deshabilitará el almacenamiento en búfer y se creará GCBufferManager en lugar de uno agrupado, y se asignarán los búferes tan pronto como se procese el mensaje, como en su pregunta.

Este artículo analiza la gestión de búfer de memoria WCF en mayor detalle.

4) ¿Estamos haciendo algo mal aquí? No soy un experto en WCF de ninguna manera, así que ¿podríamos hacer que todas las instancias de HttpsChannelFactory usen el mismo BufferManager?

5) O tal vez incluso mejor, ¿podríamos decirle a todas las instancias de HttpsChannelFactory que NO utilicen BufferManagers en absoluto y pedirle al GC que haga su condenado trabajo, que es “administrar la memoria”?

Supongo que una forma de abordar esas dos preguntas podría ser cambiar el TransferMode de ‘buffer’ a ‘stream’. Tendré que investigar, ya que el modo “transmitido” tiene un par de limitaciones y es posible que no pueda usarlo.

Actualización: ¡Realmente funciona genial! El consumo de memoria en modo de búfer durante el inicio de la aplicación fue de 630 M en las horas punta y se redujo a 470M cuando estaba completamente cargado. Después de cambiar al modo de transmisión, el consumo de memoria no muestra un pico temporal y cuando está completamente cargado, ¡el consumo es de solo 270M !

Por cierto, este fue un cambio de una línea en el código de la aplicación del cliente para mí. Solo tuve que agregar esta línea:

 httpsTransportBindingElement.TransferMode = TransferMode.StreamedResponse; 

Como dice John, sería más fácil responder una sola pregunta en lugar de escribir un ensayo. Pero esto es lo que pienso

1) En un entorno en el que tenemos GC, ¿por qué necesitaríamos un BufferManager?

Parece que no entiende el concepto de GC y buffers. GC trabaja con objetos tipados de referencia y libera memoria si detecta que un objeto es un vértice (punto o nodo) de un gráfico y no tiene ningún borde válido (líneas o conexiones) a otros vértices. Los búferes son solo algunos de almacenamiento temporal para la matriz temporal de datos sin formato. Por ejemplo, si necesita enviar un mensaje de nivel de aplicación WCF y su tamaño actual es mayor que el tamaño del mensaje de nivel de transporte, WCF lo hará en unos pocos mensajes de transporte. En cuanto al tamaño del receptor, WCF esperará hasta que llegue el mensaje de nivel de aplicación completo y solo entonces pasará el mensaje para su procesamiento (a menos que sea un enlace de transmisión). Los mensajes de transporte temporal se almacenan en el búfer , almacenados en algún lugar de la memoria en el extremo del receptor. Como la creación de nuevos buffers para cualquier mensaje nuevo en este ejemplo puede ser muy expansiva, .NET le proporciona una clase de gestión de búfer que se encarga de agrupar y compartir búferes.

2) A pesar de que MS afirma que “este proceso es mucho más rápido que crear y destruir un buffer cada vez que necesita usar uno”, ¿no deberían dejarlo hasta el GC (y su LOH, por ejemplo) y optimizar el GC en cambio?

No, no deberían. Buffers y GC no tienen nada en común (a menos que desee destruir el búfer cada vez, en el contexto de la muestra, que es un defecto de diseño). Tienen diferentes responsabilidades y abordan diferentes problemas.

3) La aplicación se conecta a un par de diferentes servicios de WCF. Para cada uno de ellos mantenemos un grupo de conexiones para las conexiones http

El enlace HTTP no está diseñado para manejar grandes cargas como 64Mb, considere cambiar el enlace a uno más apropiado. Si utiliza el mensaje de esa sie, WCF no lo aprobará a menos que los 64Mb completos se reciban por completo. Por lo tanto, si tiene 10 conexiones simultáneas, su tamaño de búfer será de 640 Mb.

Para sus otras preguntas, publique otra pregunta en SO con algún código y su configuración WCF. Será más fácil encontrar dónde está el problema. Tal vez los búferes no se eliminen porque se usan de manera inapropiada, debe considerar la cantidad de pruebas que se han realizado en GC y WCF y la cantidad de pruebas que se han realizado en un proyecto heredado: siga la maquinilla de afeitar de Occam.