15

分析一个通过 SOAP 与一堆服务对话的 WCF 客户端应用程序(我没有编写,仍然不太了解),运行几天后会抛出 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 字节的数组,它在初始化时只在我们的应用程序中使用过一次,之后就不需要了,因为服务的响应只有在初始化时才那么大,顺便说一句。对于我使用过的许多应用程序来说是典型的,即使这可能会受到优化(缓存到磁盘等)。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 之外,我们没有内存泄漏问题,尽管我们确实消耗了大量内存并在整个过程中积累了大量数据很多天,但这不是这里的问题)

4

3 回答 3

11

我相信我已经回答了您的问题 #5:

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

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

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

于 2013-06-07T12:50:43.907 回答
10

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

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

我想解决这两个问题的一种方法可能是将 TransferMode 从“缓冲”更改为“流式”。将不得不进行调查,因为“流式传输”模式有一些限制,我可能无法使用它。

更新:它实际上工作得很好!在应用程序启动期间,我在缓冲模式下的内存消耗高峰时为630M ,满载时减少到470M 。切换到流模式后,内存消耗没有出现暂时的峰值,满载时消耗只有270M

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

httpsTransportBindingElement.TransferMode = TransferMode.StreamedResponse;
于 2011-08-31T05:55:50.120 回答
4

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

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

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

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

不,他们不应该。缓冲区和 GC 没有任何共同点(除非您每次都想销毁缓冲区,在示例的上下文中,这是一个设计缺陷)。他们有不同的职责,解决不同的问题。

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

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

对于您的其他问题,请在 SO 上发布另一个问题,其中包含一些代码和您的 WCF 配置。将更容易找到问题所在。也许缓冲区没有被清除,因为它们使用不当,您应该考虑在 GC 和 WCF 上完成的测试量以及在遗留项目上执行的测试量 - 遵循奥卡姆剃刀。

于 2011-08-31T14:35:19.523 回答