我有一个自托管的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);
然后我做了一个实验:我将TransferMode
从Buffered
更改为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
时,有人可以解释实际发生的事情吗?
答案 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结构。
有关详细信息,请参阅MessageContracts和Datacontracts上的这些MSDN文章。这里有关于Buffered vs Streamed的更多信息。
答案 2 :(得分:0)
我认为(我可能错了)将用户限制为使用Stream
传输模式的操作合同中的Streamed
参数,来自WCF将流数据放入正文的事实SOAP消息的一部分,并在用户开始读取流时开始传输它。因此,我认为他们很难在单个数据流中复用任意数量的流。例如,假设您有一个具有3个流参数的操作合同,并且客户端上的三个不同线程开始从这三个流中读取。如果不使用一些算法和额外的编程来复用这三种不同的数据流(WCF目前缺乏),你怎么能这样做呢?
至于你的另一个问题,很难在没有看到完整代码的情况下告诉实际情况,但我认为通过使用gzip,你实际上是将所有消息数据压缩成一个字节数组,将其交给WCF和在客户端,当客户端请求SOAP消息时,底层通道打开一个流来读取消息和WCF通道进行流式传输,启动流数据,因为它是消息的主体。
无论如何,您应该注意,设置MessageBodyMember
属性只是告诉WCF该成员应该作为SOAP主体进行流式处理,但是当您使用自定义编码器和绑定时,主要是您选择外发消息的外观等。
答案 3 :(得分:-1)
缓冲:在上传/下载之前需要将整个文件放入内存中。 这种方法对于安全地传输小文件非常有用。
流式传输:文件可以以块的形式传输。