WCF HttpTransport: StreamMode vs buffer mode

Tengo un servicio WCF HttpTransport (v4 framework) que se expone a través de un HttpTransport personalizado basado en HttpTransport . El enlace utiliza un MessageEncoder personalizado que es prácticamente un BinaryMessageEncoder con la adición de la funcionalidad de compresión gzip.

Un cliente Silverlight y Windows consume el servicio web.

Problema : en algunos casos, el servicio tenía que devolver objetos muy grandes y ocasionalmente arrojaba excepciones OutOfMemory cuando respondía a varias solicitudes simultáneas (incluso si el Administrador de tareas informaba aproximadamente 600 Mb para el proceso). La excepción ocurrió en el codificador personalizado, cuando el mensaje estaba a punto de ser comprimido, pero creo que esto fue solo un síntoma y no la causa. La excepción declaró “no se pudo asignar x Mb” donde x era 16, 32 o 64, no una cantidad demasiado grande, por esta razón, creo que otra cosa ya puso el proceso cerca de cierto límite antes de eso.

El punto final del servicio se define de la siguiente manera:

 var transport = new HttpTransportBindingElement(); // quotas omitted for simplicity var binaryEncoder = new BinaryMessageEncodingBindingElement(); // Readerquotas omitted for simplicity var customBinding = new CustomBinding(new GZipMessageEncodingBindingElement(binaryEncoder), transport); 

Luego hice un experimento: cambié TransferMode de Buffered a StreamedResponse (y modifiqué el cliente en consecuencia). Esta es la nueva definición de servicio:

 var transport = new HttpTransportBindingElement() { TransferMode = TransferMode.StreamedResponse // <-- this is the only change }; var binaryEncoder = new BinaryMessageEncodingBindingElement(); // Readerquotas omitted for simplicity var customBinding = new CustomBinding(new GZipMessageEncodingBindingElement(binaryEncoder), transport); 

Mágicamente, ya no hay excepciones de OutOfMemory . El servicio es un poco más lento para mensajes pequeños, pero la diferencia se hace cada vez más pequeña a medida que crece el tamaño del mensaje. El comportamiento (tanto para la velocidad como para las excepciones OutOfMemory) es reproducible, hice varias pruebas con ambas configuraciones y estos resultados son consistentes.

Problema resuelto, PERO: no puedo explicarme qué está pasando aquí. Mi sorpresa se debe al hecho de que no cambié el contrato de ninguna manera . Es decir, no creé un contrato con un solo parámetro Stream , etc., como suele hacer para los mensajes transmitidos. Todavía estoy usando mis clases complejas con el mismo atributo DataContract y DataMember. Acabo de modificar el punto final , eso es todo.

Pensé que configurar TransferMode era solo una forma de habilitar la transmisión para contratos formados correctamente, pero obviamente hay más que eso. ¿Alguien puede explicar lo que sucede bajo el capó cuando cambias TransferMode ?

Cuando use ‘GZipMessageEncodingBindingElement’, supongo que está utilizando la muestra MS GZIP.

Eche un vistazo a DecompressBuffer() en GZipMessageEncoderFactory.cs y comprenderá qué está sucediendo en el modo de búfer.

Por ejemplo, supongamos que tiene un mensaje de tamaño sin comprimir 50M, tamaño comprimido 25M.

DecompressBuffer recibirá un parámetro ‘ArraySegment buffer’ de (1) 25M de tamaño. El método creará un MemoryStream, descomprimirá el buffer en él, usando (2) 50M . Luego hará un MemoryStream.ToArray (), copiando el búfer de la secuencia de memoria en una nueva matriz de byte grande (3) de 50M . Luego toma otra matriz de bytes del BufferManager de AT LEAST (4) 50M + , en realidad, puede ser mucho más, en mi caso siempre fue 67M para una matriz de 50M.

Al final de DecompressBuffer, (1) se devolverá al BufferManager (que parece que nunca se borrará por WCF), (2) y (3) están sujetos a GC (que es asincrónico, y si usted es más rápido que el GC , es posible que obtenga excepciones de OOM aunque habrá suficientes mem si se limpian). (4) presumiblemente será devuelto al BufferManager en su BinaryMessageEncodingBindingElement.ReadMessage ().

En resumen, para su mensaje de 50M, su escenario almacenado ocupará temporalmente 25 + 50 + 50 + por ejemplo, 65 = 190M de memoria, algunos de ellos sujetos a GC asíncrono, algunos gestionados por BufferManager, lo que, en el peor de los casos, significa conserva muchas matrices no utilizadas en la memoria que no son utilizables en una solicitud posterior (por ejemplo, demasiado pequeña) ni elegibles para GC. Ahora imagine que tiene múltiples solicitudes concurrentes, en ese caso BufferManager creará búferes separados para todas las solicitudes concurrentes, que nunca se limpiarán, a menos que llame manualmente a BufferManager.Clear (), y no conozco una manera de hacerlo con los administradores de búfer utilizados por WCF, consulte también esta pregunta: ¿Cómo puedo evitar que BufferManager / PooledBufferManager en mi aplicación de cliente WCF desperdicie memoria? ]

Actualización: después de migrar a IIS7 Compresión Http ( compresión condicional wcf ) consumo de memoria, carga de la CPU y tiempo de inicio eliminado (no tienen los números a la mano) y luego migración del modo de transferencia en búfer al transmitido ( ¿Cómo puedo evitar BufferManager / PooledBufferManager en mi WCF? aplicación de cliente de la pérdida de memoria? ) el consumo de memoria de mi aplicación de cliente WCF ha disminuido de 630M (pico) / 470M (continuo) a 270M (tanto pico como continuo) .

He tenido cierta experiencia con WCF y transmisión.

Básicamente, si no configura TransferMode para que se transmita por secuencias, se usará de forma predeterminada en el búfer. Entonces, si está enviando grandes cantidades de datos, va a acumular los datos de su extremo en la memoria y luego los enviará una vez que todos los datos estén cargados y listos para enviarse. Esta es la razón por la que estaba perdiendo los errores de memoria porque los datos eran muy grandes y más que la memoria de su máquina.

Ahora, si utiliza el flujo continuo, inmediatamente comenzará a enviar fragmentos de datos al otro extremo en lugar de almacenarlos en un búfer, haciendo que el uso de la memoria sea mínimo.

Pero esto no significa que el receptor también deba configurarse para la transmisión. Podrían configurarse para almacenar en búfer y experimentarán el mismo problema que el remitente si no tienen suficiente memoria para sus datos.

Para obtener los mejores resultados, ambos puntos finales deben configurarse para manejar la transmisión (para archivos de datos grandes).

Normalmente, para la transmisión, utiliza MessageContracts lugar de DataContracts porque le brinda más control sobre la estructura SOAP.

Consulte estos artículos de MSDN sobre MessageContracts y Datacontracts para obtener más información. Y aquí hay más información sobre Buffered vs Streamed .

Creo (y podría estar equivocado) que, restringir a los usuarios a solo un parámetro Stream en contratos de operación que usan el modo de transferencia por Streamed , proviene del hecho de que WCF coloca los datos de flujo en la sección del cuerpo del mensaje SOAP y comienza a transferirlo como el el usuario comienza a leer la secuencia. Por lo tanto, creo que habría sido difícil para ellos multiplexar un número arbitrario de flujos en un solo flujo de datos. por ejemplo, supongamos que tiene un contrato de operación con 3 parámetros de flujo y tres hilos diferentes en el cliente comienzan a leer de estos tres flujos. ¿Cómo podría hacer eso sin utilizar algún algoritmo y progtwigción adicional para multiplexar estos tres flujos de datos diferentes (que WCF carece en este momento)

En cuanto a su otra pregunta, es difícil saber qué está sucediendo realmente sin ver su código completo, pero creo que al usar gzip, en realidad está comprimiendo todos los datos del mensaje en una matriz de bytes, entregándoselos a WCF y al cliente lado, cuando el cliente solicita el mensaje SOAP, el canal subyacente abre una secuencia para leer el mensaje y el canal WCF para la transferencia transmitida, inicia la transmisión de datos tal como era el cuerpo del mensaje.

De todos modos, debe tener en cuenta que configurar el atributo MessageBodyMember solo le dice a WCF que este miembro debe ser transmitido como el cuerpo de SOAP, pero cuando usa codificador y enlace personalizado, es principalmente su elección cómo se verá el mensaje saliente.

Buffered: necesita poner el archivo completo en la memoria antes de cargar / descargar. este es un enfoque que es muy útil para transferir pequeños archivos de forma segura.

Transmitido por streaming: el archivo se puede transferir en forma de fragmentos.