【问题标题】:SqlClient returning strange OOM exception? C# .NET 4SqlClient 返回奇怪的 OOM 异常? C# .NET 4
【发布时间】:2013-08-27 08:36:54
【问题描述】:

我正在开发一些每天处理大量数据的企业应用程序,为此它使用 C# .NET 4 编写的 WINDOWS SERVICE 应用程序。它还连接到 SQL SERVER 2008 R2,但出于某种原因(随机) 在存储 JSON 序列化数据的同步表中引发此错误:

Exception of type 'System.OutOfMemoryException' was thrown.
at System.Data.SqlClient.TdsParser.ReadPlpUnicodeChars(Char[]& buff, Int32 offst, Int32 len, TdsParserStateObject stateObj)
at System.Data.SqlClient.TdsParser.ReadSqlStringValue(SqlBuffer value, Byte type, Int32 length, Encoding encoding, Boolean isPlp, TdsParserStateObject stateObj)
at System.Data.SqlClient.TdsParser.ReadSqlValue(SqlBuffer value, SqlMetaDataPriv md, Int32 length, TdsParserStateObject stateObj)
at System.Data.SqlClient.SqlDataReader.ReadColumnData()
at System.Data.SqlClient.SqlDataReader.ReadColumn(Int32 i, Boolean setTimeout)
at System.Data.SqlClient.SqlDataReader.GetValueInternal(Int32 i)
at System.Data.SqlClient.SqlDataReader.GetValues(Object[] values)

此表是保存 LOB 数据的相当通用的表:

CREATE TABLE [dbo].[SyncJobItem](
 [id_job_item] [int] IDENTITY(1,1) NOT NULL,
 [id_job] [int] NOT NULL,
 [id_job_item_type] [int] NOT NULL,
 [id_job_status] [int] NOT NULL,
 [id_c] [int] NULL,
 [id_s] [int] NULL,
 [job_data] [nvarchar](max) NOT NULL,
 [last_update] [datetime] NOT NULL,
CONSTRAINT [PK_SyncJobItem] PRIMARY KEY CLUSTERED)

失败的 LOB 记录在 job_data 列中有 36.231.800 个字符的数据,即(如果我们说 1 个字符是 2 个字节,UTF-8)大约 70MB 的数据,这并不多。

请考虑更改作业的数据存储(例如磁盘)或类似的东西不是我的选择。我想修复这个错误,所以如果有人知道任何事情,请帮忙!

这个错误也随机发生在相同的数据上,运行的系统是vmWare-vCloud,我认为是一些大型刀片系统。我们有大约 6GB 的 RAM 专用于我们的 vm(服务最多使用大约 1-2GB),服务编译为 x64,系统是 x64 Windows 2008R2 Standard。我已经确保没有单个对象的内存超过 2GB,所以不是这样,SqlClient 内部也有错误,在我 15 年的开发经验中,我从未见过它,谷歌一无所获。此外,由于 DB 具有超过 32GB 的 RAM 并且仅使用 20GB 峰值,因此错误不在 DB 端。对于我在这个系统中使用的不常见的细节是多线程和每个作业步骤之后的 GC.Collect() (数据上有多个步骤)。

编辑:

这是解决这个问题的完整代码:

    internal static void ExecuteReader(IConnectionProvider conn, IList destination, IObjectFiller objectBuilder, string cmdText, DbParameterCollection parameters, CommandType cmdType, int cmdTimeout)
    {
        IDbCommand cmd = CreateCommand(conn.DBMS, cmdText, parameters, cmdType, cmdTimeout);
        cmd.Connection = conn.Connection;

        bool connIsOpennedLocally = EnsureOpenConnection(conn);
        try
        {
            AssignExistingPendingTransactionToCommand(conn, cmd);
            using (IDataReader reader = cmd.ExecuteReader(CommandBehavior.SingleResult))
            {
                objectBuilder.FillCollection(reader, destination);
                PopulateOutputParameterValues(parameters, cmd);
            }
        }
        finally
        {
            CloseConnectionIfLocal(conn, connIsOpennedLocally);
            cmd.Dispose();
        }
    }

...

    private void FillFromAlignedReader(ICollection<TEntity> collection, IDataReader openedDataReader, IDbTable table)
    {
        // Fastest scenario: data reader fields match entity field completely.
        // It's safe to reuse same array because GetValues() always overwrites all members. Memory is allocated only once.
        object[] values = new object[openedDataReader.FieldCount];
        while (openedDataReader.Read())
        {
            openedDataReader.GetValues(values);
            TEntity entity = CreateEntity(table, EntityState.Synchronized, values);
            collection.Add(entity);
        }
    }

【问题讨论】:

  • 您能否澄清“处理大量数据”的含义">您是否正在阅读您建议的表格并对数据进行处理?或者您是从其他表读取并写入此表?数据整理后,您将如何处理数据,是在最终刷新之前保留在内存中,还是逐行写入并从内存中刷新?
  • 我正在将数据从 SQL Server 加载到 DataTable。这就是打破的。这是简单的选择语句。在我对数据进行一些计算之前和之后。基本上它是这样的:1)从 NoSQL db(Couchbase)加载数据 2)使用 map-reduce 聚合数据 3)将聚合结果对象序列化为 JSON 对象 4)保存到 db 到这个表 5)转到下一步从 SQL 加载 JSON(这里它与 OOM 中断)
  • 当您说我已确保没有单个对象的内存超过 2GB 时,是否包括第 5 步中的 DataTable?您能否使用仅向前的 SqlDataReader 实现相同的目的,以便您在内存中一次只有一行?我认为查看抛出错误的代码块以及堆栈跟踪会很有用。
  • 已更新。它使用 SqlDataReader 来读取数据。我不确定它是否只转发 SqlDataReader。
  • 我读到了 :)。我发现了这个,似乎与我的问题相似:stackoverflow.com/questions/15124034/…

标签: c# sql sql-server-2008 service sqlclient


【解决方案1】:

对于那些在经过大量测试和 MSDN (link) 后遇到此问题的人,我得出的结论是,SqlDataReader 在正常读取模式下能够读取的最大单个字段大小在 x64 机器上约为 70MB,之后这需要将其SqlCommand 切换为CommandBehavior.SequentialAccess 并流式传输字段内容。

可以这样工作的示例代码:

    ...
    behaviour = CommandBehavior.SequentialAccess;
    using (IDataReader reader = cmd.ExecuteReader(behaviour))
    {
       filler.FillData(reader, destination);
    }

当你在循环中读取数据时,你需要按顺序获取列,当你到达 BLOB 列时,你应该调用这样的东西(取决于数据类型):

    ...
    private string GetBlobDataString(IDataReader openedDataReader, int columnIndex)
    {
        StringBuilder data = new StringBuilder(20000);
        char[] buffer = new char[1000];
        long startIndex = 0;

        long dataReceivedCount = openedDataReader.GetChars(columnIndex, startIndex, buffer, 0, 1000);
        data.Append(buffer, 0, (int)dataReceivedCount);
        while (dataReceivedCount == 1000)
        {
            startIndex += 1000;
            dataReceivedCount = openedDataReader.GetChars(columnIndex, startIndex, buffer, 0, 1000);
            data.Append(buffer, 0, (int)dataReceivedCount);
        }

        return data.ToString();
    }

    private byte[] GetBlobDataBinary(IDataReader openedDataReader, int columnIndex)
    {
        MemoryStream data = new MemoryStream(20000);
        BinaryWriter dataWriter = new BinaryWriter(data);

        byte[] buffer = new byte[1000];
        long startIndex = 0;

        long dataReceivedCount = openedDataReader.GetBytes(columnIndex, startIndex, buffer, 0, 1000);
        dataWriter.Write(buffer, 0, (int)dataReceivedCount);
        while (dataReceivedCount == 1000)
        {
            startIndex += 1000;
            dataReceivedCount = openedDataReader.GetBytes(columnIndex, startIndex, buffer, 0, 1000);
            dataWriter.Write(buffer, 0, (int)dataReceivedCount);
        }

        data.Position = 0;
        return data.ToArray();
    }

这应该适用于高达 1GB-1.5GB 左右的数据,之后它将在单个对象无法保留足够大小的连续内存块时中断,因此要么直接从缓冲区刷新到磁盘,要么将数据拆分为多个较小的对象.

【讨论】:

    【解决方案2】:

    我认为对于这些大量数据,您应该使用 db-type Text。仅当您需要对其进行搜索/喜欢时才使用 nvarchar。请注意,启用全文搜索时,这可能会产生奇怪的行为。

    【讨论】:

    • Microsoft advise that Text、NText 和 Image 数据类型将从 sql-server 的未来版本中删除,并且应该使用 VARCHAR(MAX)NVARCHAR(MAX)VARBINARY 代替它们。我怀疑这是解决方案!
    猜你喜欢
    • 1970-01-01
    • 2010-09-16
    • 2012-01-02
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-12-07
    • 1970-01-01
    相关资源
    最近更新 更多