如何防止WCF客户端应用程序中的BufferManager / PooledBufferManager浪费内存?

分析一个WCF客户端应用程序(我没有写,但仍然不太了解)通过SOAP与一堆服务进行通信,运行几天后会抛出一个OutOfMemoryException,我发现.net的PooledBufferManager将会永远不会释放未使用的缓冲区,即使应用程序内存不足,也会导致OOME。

这当然符合规范: http : //msdn.microsoft.com/en-us/library/ms405814.aspx

当垃圾收集回收缓冲池时,池及其缓冲区被销毁。

请随意回答下面的一个问题,因为我有一堆问题,一些更一般的问题,以及一些特定于我们的应用程序使用BufferManager。

首先是关于(默认的Pooled)BufferManager的几个一般性问题:

1)在我们有GC的环境中,为什么我们需要一个可以保留未使用内存的BufferManager,即使它导致OOME? 我知道,有BufferManager.Clear(),您可以使用它来手动删除所有缓冲区 – 如果您有权访问BufferManager,即。 进一步了解为什么我似乎无法访问。

2)尽管MS声称“​​这个过程比每次需要使用一个缓冲区创建和销毁缓冲区要快得多。”,他们不应该把它留给GC(例如它的LOH)并优化GC而不是?

3)当做一个BufferManager.Take(33 * 1024 * 1024)时,我会得到一个64M的缓冲区,因为PooledBufferManager将缓存该缓冲区以供以后重用,这可能 – 好吧,在我的情况下它不是,因此它是纯粹浪费记忆 – 比如34M,或50M,或64M,是需要的。 因此创建一个可能非常浪费的BufferManager是明智的,HttpsChannelFactory使用(默认情况下,我假设)? 我没有看到内存分配的性能如何重要,特别是当我们讨论WCF和网络服务时,应用程序将每隔10秒TOPS,通常更多秒甚至几分钟。

现在有一些更具体的问题与我们的应用程序使用BufferManagers有关。 该应用程序连接到几个不同的WCF服务。 对于它们中的每一个,我们为http连接维护一个连接池,因为连接可能同时发生。

检查一个堆转储中的单个最大对象,一个64M字节数组,在初始化时只在我们的应用程序中使用过一次,之后不需要,因为来自服务的响应仅在初始化时才很大,即btw。 对于我使用过的许多应用程序来说都是典型的,即使这可能会受到优化(缓存到磁盘等)。 WinDbg中的GC根分析产生以下结果(我将我们的专有类的名称清理为’MyServiceX’等):

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[]) 

查看由BufferManager管理的其他字节数组的gc根表明其他服务(不是’MyServiceX’)具有不同的BufferPool实例,因此每个实例都浪费自己的内存,它们甚至不共享浪费。

4)我们在这里做错了吗? 我不是任何WCF专家,所以我们可以使各种HttpsChannelFactory实例都使用相同的BufferManager吗?

5)或者甚至更好,我们可以告诉所有HttpsChannelFactory实例根本不使用BufferManagers并要求GC完成它的神秘工作,即“管理内存”吗?

6)如果无法回答问题4)和5),我是否可以访问所有HttpsChannelFactory实例的BufferManager并手动调用它们上的.Clear() – 这远非最佳解决方案,但它已经有所帮助,就我而言,它不仅可以在一个服务实例中释放前提的64M,而且还可以释放64M + 32M + 16M + 8M + 4M + 2M! 因此,单独这样可以使我的应用程序持续更长时间而不会遇到内存问题(不,除了BufferManager之外,我们没有内存泄漏问题,尽管我们确实消耗了大量内存并在课程中累积了大量数据很多天,但这不是问题)

我相信我已回答你的问题#5:

5)或者甚至更好,我们可以告诉所有HttpsChannelFactory实例根本不使用BufferManagers并要求GC完成它的神秘工作,即“管理内存”吗?

有一个MaxBufferPoolSize绑定参数,它控制BufferManager中缓冲区的最大大小。 将其设置为0将禁用缓冲,并且将创建GCBufferManager而不是池化 – 并且一旦消息处理就会GC分配缓冲区,如您的问题所示。

本文更详细地讨论了WCF内存缓冲区管理 。

4)我们在这里做错了吗? 我不是任何WCF专家,所以我们可以使各种HttpsChannelFactory实例都使用相同的BufferManager吗?

5)或者甚至更好,我们可以告诉所有HttpsChannelFactory实例根本不使用BufferManagers并要求GC完成它的神秘工作,即“管理内存”吗?

我想解决这两个问题的一种方法是将TransferMode从’buffered’改为’streamamed’。 将不得不调查,因为’流式’模式有一些限制,我可能无法使用它。

更新:它确实很棒! 在应用程序启动期间,我在缓冲模式下的内存消耗在高峰时间为630M,在满载时减少到470M 。 切换到流模式后,内存消耗不会显示临时峰值,完全加载后,功耗仅为270M

顺便说一句,这对我来说是客户端应用程序代码的单行更改。 我只需要添加这一行:

 httpsTransportBindingElement.TransferMode = TransferMode.StreamedResponse; 

正如约翰所说,只回答一个问题而不是写一篇文章会更容易。 但这就是我的想法

1)在我们有GC的环境中,为什么我们需要一个BufferManager

您似乎误解了GC和缓冲区的概念。 GC与引用类型对象一起使用,如果它检测到对象是图形的顶点(点或节点)并且它没有任何有效的边(线或连接)到其他顶点,则释放内存。 缓冲区只是临时存储数据的一些临时存储。 例如,如果您需要发送WCF应用程序级别消息并且其当前大小大于传输级别消息大小,则WCF将在一些传输消息中执行此操作。 在接收器大小上,WCF将等待,直到完整的应用程序级别消息到达,然后它才会传递消息进行处理(除非它是流式绑定)。 临时传输消息被缓冲 – 存储在接收器端的内存中。 由于在此示例中为任何新消息创建新缓冲区可能会非常广泛,因此.NET为您提供了一个缓冲区管理类,负责汇集和共享缓冲区。

2)尽管MS声称“​​这个过程比每次需要使用一个缓冲区创建和销毁缓冲区要快得多。”,他们不应该把它留给GC(例如它的LOH)并优化GC而不是?

不,他们不应该。 缓冲区和GC没有任何共同之处(除非您希望每次都破坏缓冲区,在样本的上下文中,这是一个设计缺陷)。 他们有不同的责任,解决不同的问题。

3)应用程序连接到几个不同的WCF服务。 对于每个人,我们为http连接维护一个连接池

HTTP绑定不是设计用于处理像64Mb这样的大型有效负载,考虑将绑定更改为更合适的绑定。 如果您使用该sie的消息,除非完全接收到整个64Mb,否则WCF将不会传递它。 因此,如果您有10个并发连接,则缓冲区大小将为640Mb。

对于您的其他问题,请在SO上发布另一个问题,其中包含一些代码和您的WCF配置。 找到问题的位置会更容易。 也许缓冲区没有被清除,因为它们被不恰当地使用,你应该考虑在GC和WCF上进行的测试量以及在遗留项目上执行的测试量 – 遵循Occam的razor。