SqlClient возвращает странное исключение OOM? С# .NET 4

Я работаю над некоторыми корпоративными приложениями, которые каждый день обрабатывают большие объемы данных, и для этого у него есть приложение WINDOWS SERVICE, написанное на С# .NET 4. Оно также имеет подключение к 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 имеет 36 231 800 символов данных в столбце job_data, что составляет (если мы говорим, что 1 символ равен 2 байтам, UTF-8) около 70 МБ данных, что немного.

Учтите, что изменение хранилища данных для задания (например, диска) или что-то подобное для меня не вариант. Я хотел бы исправить эту ошибку, поэтому, если кто-нибудь что-нибудь знает, пожалуйста, помогите!

Также эта ошибка возникает случайным образом с одними и теми же данными, работает система vmWare-vCloud, то есть, я думаю, какая-то большая блейд-система. У нас есть около 6 ГБ оперативной памяти, выделенной для нашей виртуальной машины (служба в большинстве случаев использует около 1-2 ГБ), служба скомпилирована как x64, а система — x64 Windows 2008R2 Standard. Я убедился, что ни один объект не имеет более 2 ГБ в памяти, так что это не так, также ошибка внутри SqlClient, и за мой 15-летний опыт разработки я никогда этого не видел, и Google ничего не выдает. Также ошибка не на стороне БД, поскольку БД имеет более 32 ГБ ОЗУ и использует только 20 ГБ в пике. Для особенностей, которые я использую в этой системе, которые не являются обычными, это многопоточность и 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);
        }
    }

person lord.fist    schedule 27.08.2013    source источник
comment
Можете ли вы уточнить, что вы подразумеваете под большим объемом данных? Вы читаете из таблицы, которую вы посоветовали, и что-то делаете с данными? Или вы читаете из других таблиц и пишете в эту таблицу? Что вы делаете с вашими данными после того, как они были обработаны, остаются ли они в памяти перед окончательной очисткой или они записываются и сбрасываются из памяти построчно?   -  person GarethD    schedule 27.08.2013
comment
Я загружаю данные из SQL Server в DataTable. Вот что ломается тогда. Это просто оператор выбора. До и после я делаю некоторые расчеты с данными. В основном это выглядит следующим образом: 1) Загрузите данные из базы данных NoSQL (Couchbase) 2) Агрегируйте данные с помощью map-reduce 3) Сериализируйте объект агрегированного результата в объект JSON 4) Сохраните в базе данных эту таблицу 5) Перейдите к следующему шагу, который загружает JSON из SQL (здесь он разрывается с OOM)   -  person lord.fist    schedule 27.08.2013
comment
Когда вы говорите Я убедился, что ни один объект не имеет в памяти более 2 ГБ, относится ли это к DataTable на шаге 5? Не могли бы вы добиться того же самого с помощью SqlDataReader, работающего только вперед, чтобы у вас была только одна строка за раз в памяти? Я думаю, было бы полезно увидеть блок кода, выдающий ошибку, а также трассировку стека.   -  person GarethD    schedule 27.08.2013
comment
Обновлено. Он использует SqlDataReader для чтения данных. Я не уверен, что это только вперед SqlDataReader.   -  person lord.fist    schedule 27.08.2013
comment
Извините, SqldataReader всегда только вперед, я просто использовал это, чтобы отличить от использования DataTable. Я подозреваю, что ваша коллекция TEntities занимает большую часть памяти, однако я не могу предложить разумную альтернативу хранению всех ваших сущностей в куче, не зная, для чего они нужны. С точки зрения объяснения того, почему вы получаете исключения из памяти задолго до того, как у вас действительно не хватает памяти эта статья подробно описывает распределение памяти.   -  person GarethD    schedule 27.08.2013
comment
Я это читал :). Я нашел это, похоже на мою проблему: stackoverflow.com/questions/15124034/   -  person lord.fist    schedule 27.08.2013


Ответы (2)


Для тех, кто столкнулся с этой проблемой после большого количества тестов и MSDN (ссылка) Я пришел к выводу, что максимальный размер одного поля, который может быть прочитан SqlDataReader в обычном режиме чтения, составляет около 70 МБ на машине x64, после этого ему нужно переключить 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();
    }

Это должно работать для данных размером примерно до 1 ГБ-1,5 ГБ, после чего он сломается на одном объекте, не имея возможности зарезервировать непрерывный блок памяти достаточного размера, поэтому либо сбрасывайте данные непосредственно на диск из буфера, либо разделяйте данные на несколько меньших объектов.

person lord.fist    schedule 28.08.2013

Я думаю, что для этих больших объемов данных вы должны использовать текст типа db. Используйте nvarchar только в том случае, если вам нужно выполнить поиск/лайк на нем. Обратите внимание, что это может привести к странному поведению при включенном полнотекстовом поиске.

person Jeroen van Langen    schedule 27.08.2013
comment
Майкрософт сообщает, что типы данных Text, NText и Image будут удалены из будущие версии sql-server и что вместо них следует использовать VARCHAR(MAX), NVARCHAR(MAX) и VARBINARY. Сомневаюсь, что это решение! - person GarethD; 27.08.2013