WCF 流式处理 System.OutOfMemoryException

WCF Streaming System.OutOfMemoryException

在我的流式 WCF 应用程序中,我在服务中得到 System.OutOfMemoryExceptions 大消息(使用数据读取器查询 >7GB 的 MSSQL 表,消息甚至可能超过 7GB),而小消息工作正常。我可以观察到 DataReaderToExcelXml 执行期间内存使用量不断增长(见下文)。奇怪的是,它通常快速增长到 2GB,保持在 2GB +-1GB 一段时间(1-5 分钟),然后再次快速增长到 ~6.5GB,这导致异常(机器有 8GB 内存)。在这一点上,在我看来,好像 Stream 不再通过而是被缓冲了。

我已经启用了跟踪记录,但它似乎因异常而停止。 DataReaderToExcelXml 函数调用是跟踪日志中的最后一个可见事件。

在 WCF 消息协定中,我确保消息只包含 Stream。在客户端,返回的 Stream 被简单地读取,写入文件流并被释放。但是,当我得到异常时,我永远无法观察到正在执行的客户端代码或正在写入的文件。

我已经尝试将客户端和服务器端的 maxBufferPoolSize 设置为零,如此处所述 。没有成功。

WCF 服务端的流编写器函数:

 Public Shared Function DataReaderToExcelXml(ByRef dr As SqlDataReader) As Stream
    Dim ms As New MemoryStream
    Dim tw As New IO.StreamWriter(ms)

    For Each row As DbDataRecord In dr
            'Embed row in ExcelXml, detailed function omitted
            tw.write(row.toString()) 'row.toString is just a simplification
    End While

    tw.Flush()
    dr.Close()
    ms.Seek(0, SeekOrigin.Begin)

    Return ms
 End Function

Web.config 绑定

    <bindings>
      <basicHttpBinding>
        <binding receiveTimeout="24.00:00:00" sendTimeout="24.00:00:00"
          maxBufferPoolSize="9223372036854775807" maxReceivedMessageSize="9223372036854775807"
          messageEncoding="Mtom" transferMode="Streamed" bypassProxyOnLocal="True">
          <readerQuotas maxStringContentLength="2147483647" maxArrayLength="2147483647"
            maxBytesPerRead="2147483647" maxNameTableCharCount="2147483647" />
          <security mode="Transport">
            <transport clientCredentialType="None" />
          </security>
        </binding>
      </basicHttpBinding>
    </bindings>

App.config 绑定

    <binding name="BasicHttpBinding_IFileService" closeTimeout="00:01:00"
        openTimeout="00:01:00" receiveTimeout="24.00:00:00" sendTimeout="24.00:00:00"
        allowCookies="false" bypassProxyOnLocal="true" hostNameComparisonMode="StrongWildcard"
        maxBufferSize="2147483647" maxBufferPoolSize="2147483647"
        maxReceivedMessageSize="8589934592" messageEncoding="Mtom"
        textEncoding="utf-8" transferMode="Streamed">
        <readerQuotas maxDepth="32" maxStringContentLength="2147483647"
            maxArrayLength="2147483647" maxBytesPerRead="4096" maxNameTableCharCount="2147483647" />
        <security mode="Transport">
            <transport clientCredentialType="None" proxyCredentialType="None"
                realm="" />
            <message clientCredentialType="UserName" algorithmSuite="Default" />
        </security>
    </binding>

编辑:我发现在我的服务上使用 .NET 内存分析器,内存量的增加可以追溯到 byte[] 类型。就个人而言,这对我来说没有任何意义,但也许是有用的信息。

edit2:在检查 System.OutOfMemoryException 时,我注意到它发生在 ms.Capacity = ms.length = 1073741824 = 1GB 时。所以就在内存流将其容量加倍之前。我仍然不确定,为什么w3wp首先会消耗如此大量的内存,但是现在很明显是内存流触发了Exception。

嗯,看了你的问题,我觉得你错误的根源,才是关键。具体错误在哪里?在传输中,还是在您的服务器中? 如果在 DataReaderToExcel 中引发异常,也许您可​​以考虑其他想法(我认为这不是 WCF 问题): 到目前为止,我认为您在 Windows 表单应用程序中会遇到同样的问题。

你需要内存,所以你只能用你的 SqlDataReader 填充它,你必须根据范围按块查询。

我们将开始计算 "TotalRecords":

SELECT COUNT(*)
FROM YOUR_TABLE  
WHERE WATHEVER_YOU_WANT 

例如,查询 10.000 条记录的块,您有 TotalRecords/10.000 页(查询)+1。 (示例:30.001 行,每个块 10.000 行 = 4 个查询)

迭代构建它们:

select top **10.000** * from 
(SELECT TOP (100) PERCENT  ROW_NUMBER() OVER (ORDER BY YOUR_TABLE_FIELD_1 DESC) ROW_PAGINATED, YOUR_TABLE_FIELD_1, YOUR_TABLE_FIELD_2 , ... , YOUR_TABLE_FIELD_N 
FROM YOUR_TABLE  
WHERE WATHEVER_YOU_WANT 
ORDER BY YOUR_TABLE_FIELD_1 DESC
) YOUR_ALIAS
WHERE YOUR_ALIAS.ROW_PAGINATED BETWEEN min_records_per_page AND max_records_per_page 
)

其中 min_records_per_page 和 max_records_per_page 具有以下值:

Query 1:
  min_records_per_page= 1  
  max_records_per_page  = 10000
Query 2:
  min_records_per_page= 10001  
  max_records_per_page  = 20000
  ...
 Query N:
  min_records_per_page= (N-1)* +1
  max_records_per_page  = TotalRecords

在每次迭代中,您会将每个数据行映射到 ExcelXml class。

这样做,您将避免消耗所有内存,假设您将处置您使用的对象。例如,您可以每 2 次迭代使用函数 SetProcessWorkingSetSize。 在处理大量行后释放内存:

Private Declare Auto Function SetProcessWorkingSetSize Lib "kernel32.dll" (ByVal procHandle As IntPtr, ByVal min As Int32, ByVal max As Int32) As Boolean

 Dim Mem As Process
 Mem = Process.GetCurrentProcess()
 SetProcessWorkingSetSize(Mem.Handle, -1, -1)

然后,现在您有了一个包含映射对象的数组。第 1 部分完成。

第 2 部分。您的客户端 WCF 可能会收到此数据。一旦要发送的数据准备就绪,请告诉我们您是否有问题。然后我们可以探索通信参数,然后我们会说 "increase that parameter, or add this one"。

希望对您有所帮助