WCF HttpTransport:流式传输与缓冲的TransferMode

时间:2010-10-28 14:09:08

标签: c# .net silverlight wcf

我有一个自托管的WCF服务(v4框架),它通过基于HttpTransport的自定义绑定公开。绑定使用自定义MessageEncoder,它几​​乎是BinaryMessageEncoder,并添加了gzip压缩功能。

Silverlight和Windows客户端使用Web服务。

问题:在某些情况下,服务必须返回非常大的对象,并且在响应多个并发请求时偶尔会抛出OutOfMemory异常(即使任务管理器报告过程为600 Mb)。当消息即将被压缩时,自定义编码器中发生异常,但我认为这只是一种症状,而不是原因。例外情况是“未能分配x Mb”,其中x为16,32或64,而不是一个过大的数量 - 因此我认为其他事情已经使该过程接近某个限制。

服务端点定义如下:

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

然后我做了一个实验:我将TransferModeBuffered更改为StreamedResponse(并相应地修改了客户端)。这是新的服务定义:

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

可悲的是,没有OutOfMemory例外。对于小消息,服务有点慢,但随着消息大小的增加,差异变得越来越小。 行为(速度和OutOfMemory异常)都是可重现的,我用两种配置做了几次测试,这些结果是一致的。

问题解决了,但是:我无法解释自己这里发生了什么。令我惊讶的是,我没有以任何方式改变合同。即我没有像通常对流式消息那样使用单个Stream参数等创建合同。我仍在使用具有相同DataContract和DataMember属性的复杂类。 我刚刚修改了端点,这就是全部。

我认为设置TransferMode只是为正确形成的合同启用流的一种方式,但显然还有更多。 当你改变TransferMode时,有人可以解释实际发生的事情吗?

4 个答案:

答案 0 :(得分:18)

当您使用'GZipMessageEncodingBindingElement'时,我假设您正在使用MS GZIP示例。

在GZipMessageEncoderFactory.cs中查看DecompressBuffer(),您将了解缓冲模式下发生了什么。

为了举例,假设您有一条未压缩大小为50M,压缩大小为25M的消息。

DecompressBuffer将收到(1) 25M 大小的'ArraySegment buffer'参数。然后,该方法将创建一个MemoryStream,使用(2) 50M 将缓冲区解压缩到其中。然后它将执行一个MemoryStream.ToArray(),将内存流缓冲区复制到一个新的(3) 50M 大字节数组中。然后它需要来自至少(4) 50M + 的BufferManager的另一个字节数组,实际上它可以更多 - 在我的情况下,50M阵列总是67M。

在DecompressBuffer结束时,(1)将返回到BufferManager(它似乎永远不会被WCF清除),(2)和(3)受GC(这是异步的,如果你更快)比GC,你可能会得到OOM异常,即使有足够的内存,如果清理了)。 (4)可能会被返回到BinaryMessageEncodingBindingElement.ReadMessage()中的BufferManager。

总而言之,对于您的50M消息,您的缓冲方案将暂时占用 25 + 50 + 50 +,例如65 = 190M 内存,其中一些受异步GC管理,其中一些由BufferManager管理,最坏的情况 - 意味着它在内存中保留了大量未使用的数组,这些数组在后续请求中都不可用(例如小)也不符合GC的条件。现在假设您有多个并发请求,在这种情况下,BufferManager将为所有并发请求创建单独的缓冲区,除非您手动调用BufferManager.Clear(),否则将永远不会清除,而我不会知道使用WCF使用的缓冲区管理器的方法,请参阅此问题:How can I prevent BufferManager / PooledBufferManager in my WCF client app from wasting memory?]

更新:迁移到IIS7 Http压缩(wcf conditional compression内存消耗后,cpu负载和启动时间下降(没有方便的数字)然后从缓冲区迁移到流式传输TransferMode(How can I prevent BufferManager / PooledBufferManager in my WCF client app from wasting memory?我的WCF客户端应用程序的内存消耗从630M(峰值)/470M(连续)下降到270M(峰值和连续)

答案 1 :(得分:8)

我在WCF和流媒体方面有过一些经验。

基本上,如果你没有将TransferMode设置为流式传输,那么它将默认为缓冲。因此,如果您要发送大量数据,它将在内存中构建数据,然后在加载所有数据并准备发送后发送。这就是为什么你出现内存错误的原因,因为数据非常大而且超出了你机器的内存。

现在,如果您使用流式传输,那么它将立即开始向其他端点发送数据块而不是缓冲它,从而使内存使用量极少。

但这并不意味着接收器也必须设置为流式传输。它们可以设置为缓冲区,并且如果它们没有足够的内存用于数据,则会遇到与发送方相同的问题。

为获得最佳效果,应设置两个端点以处理流(对于大型数据文件)。

通常,对于流式传输,您使用MessageContracts而不是DataContracts,因为它可以让您更好地控制SOAP结构。

有关详细信息,请参阅MessageContractsDatacontracts上的这些MSDN文章。这里有关于Buffered vs Streamed的更多信息。

答案 2 :(得分:0)

我认为(我可能错了)将用户限制为使用Stream传输模式的操作合同中的Streamed参数,来自WCF将流数据放入正文的事实SOAP消息的一部分,并在用户开始读取流时开始传输它。因此,我认为他们很难在单个数据流中复用任意数量的流。例如,假设您有一个具有3个流参数的操作合同,并且客户端上的三个不同线程开始从这三个流中读取。如果不使用一些算法和额外的编程来复用这三种不同的数据流(WCF目前缺乏),你怎么能这样做呢?

至于你的另一个问题,很难在没有看到完整代码的情况下告诉实际情况,但我认为通过使用gzip,你实际上是将所有消息数据压缩成一个字节数组,将其交给WCF和在客户端,当客户端请求SOAP消息时,底层通道打开一个流来读取消息和WCF通道进行流式传输,启动流数据,因为它是消息的主体。

无论如何,您应该注意,设置MessageBodyMember属性只是告诉WCF该成员应该作为SOAP主体进行流式处理,但是当您使用自定义编码器和绑定时,主要是您选择外发消息的外观等。

答案 3 :(得分:-1)

缓冲:在上传/下载之前需要将整个文件放入内存中。          这种方法对于安全地传输小文件非常有用。

流式传输:文件可以以块的形式传输。