Чтение файла размером 200 МБ из базы данных вызывает исключение из памяти

Я пытаюсь запросить базу данных и извлечь файлы Excel, размер которых может достигать 1 миллиона строк (~200 МБ), хранящихся как varbinary, и передать их через средство проверки.

Наш сервер сборки имеет 6 ГБ памяти и процессор с балансировкой нагрузки, и во время выполнения даже близко не достигает максимальной загрузки ЦП или памяти.

Тем не менее, примерно через 40 секунд процесс выдает ошибку OutOfMemoryException.

Вот трассировка стека:

System.OutOfMemoryException: Exception of type 'System.OutOfMemoryException' was thrown.
   at System.Data.SqlTypes.SqlBinary.get_Value()
   at System.Data.SqlClient.SqlBuffer.get_ByteArray()
   at System.Data.SqlClient.SqlBuffer.get_Value()
   at System.Data.SqlClient.SqlDataReader.GetValueFromSqlBufferInternal(SqlBuffer data, _SqlMetaData metaData)
   at System.Data.SqlClient.SqlDataReader.GetValueInternal(Int32 i)
   at System.Data.SqlClient.SqlDataReader.GetValue(Int32 i)
   at System.Data.SqlClient.SqlCommand.CompleteExecuteScalar(SqlDataReader ds, Boolean returnSqlValue)
   at System.Data.SqlClient.SqlCommand.ExecuteScalar()
   at eConfirmations.DataService.FileServices.FileDataService.GetFileContent(Guid fileId) in d:\w1\3\s\Source\eConfirmations.DataService\FileServices\FileDataService.cs:line 157
...
   at System.Data.SqlTypes.SqlBinary.get_Value()
   at System.Data.SqlClient.SqlBuffer.get_ByteArray()
   at System.Data.SqlClient.SqlBuffer.get_Value()
   at System.Data.SqlClient.SqlDataReader.GetValueFromSqlBufferInternal(SqlBuffer data, _SqlMetaData metaData)
   at System.Data.SqlClient.SqlDataReader.GetValueInternal(Int32 i)
   at System.Data.SqlClient.SqlDataReader.GetValue(Int32 i)
   at System.Data.SqlClient.SqlCommand.CompleteExecuteScalar(SqlDataReader ds, Boolean returnSqlValue)
   at System.Data.SqlClient.SqlCommand.ExecuteScalar()
   at eConfirmations.DataService.FileServices.FileDataService.GetFileContent(Guid fileId) in d:\w1\3\s\Source\eConfirmations.DataService\FileServices\FileDataService.cs:line 157

А вот мой код, который генерирует исключение:

    private byte[] GetFileContent(Guid fileId)
    {
        byte[] content;
        string connectionString = ConfigurationManager.ConnectionStrings["eConfirmationsDatabase"].ConnectionString;

        using (SqlConnection sqlConnection = new SqlConnection(connectionString))
        {
            using (SqlCommand sqlCommand = sqlConnection.CreateCommand())
            {
                sqlCommand.CommandTimeout = 300;
                sqlCommand.CommandText = $"SELECT Content FROM dbo.[File] WHERE FileId = '{fileId}'";
                sqlConnection.Open();
                content = sqlCommand.ExecuteScalar() as byte[];
                sqlConnection.Close();
                sqlCommand.Dispose();
            }
            sqlConnection.Dispose();
        }
        return content;
    }

Есть ли более эффективный способ получить эти данные или мы можем обновить настройку на нашем сервере сборки, чтобы избежать этой ошибки?


person Nathan Foss    schedule 19.04.2017    source источник
comment
Сколько места в куче доступно в среде C#?   -  person Carcigenicate    schedule 19.04.2017
comment
@Carcigenicate C# Увеличение размера кучи - возможно ли это.   -  person Theraot    schedule 19.04.2017
comment
Или SqlDataReader с флагом SequentialAccess позволит вам передавать данные в потоковом режиме.   -  person Alex K.    schedule 19.04.2017
comment
@Theraot Triple dispose должен был гарантировать отсутствие утечек памяти.   -  person Nathan Foss    schedule 19.04.2017
comment
Я знаю, что это может быть глупо, но проверили ли вы уровень в своем коде, чтобы увидеть, не зацикливается ли то, что вызывает это? Я не могу сказать вам, сколько раз я находил ошибки в коде, когда одна и та же функция вызывалась более одного раза без необходимости, но трассировка стека этого не отражала.   -  person CDove    schedule 19.04.2017
comment
@CDove спасибо за вклад, я уверен, что это запускается только один раз и не зацикливается. Я просто не могу говорить о классе SqlDataReader и о том, что он делает за кулисами.   -  person Nathan Foss    schedule 19.04.2017
comment
Я бы рекомендовал вместо того, чтобы возвращать большой массив байтов, отправить целевой поток в эту функцию и записать в нее.   -  person Magnus    schedule 19.04.2017
comment
Разве первый параметр в GetBytes не должен быть равен 0, а не 1?   -  person Magnus    schedule 19.04.2017


Ответы (1)


Итак, вот что происходит:

Поскольку это работает на 32-битной сборке, максимальное выделение памяти составляет 2 ГБ, но я все еще не приближаюсь к этому порогу.

Согласно этой записи о переполнении стека, которая очень похожа на В моей ситуации платформа .NET ограничивает объекты до предела 256MB в памяти.

Поэтому, несмотря на то, что мой файл всего 200 МБ, byte[]s и MemoryStreams расширяются в степени 2, пока не достигнут необходимых 256 МБ. Когда они расширяются, они создают новый экземпляр соответствующего размера и копируют старые данные в новый, эффективно умножая использование памяти на 3, что вызывает исключение.

В MSDN есть пример извлечения большого файла используя FileStream, но вместо FileStream я использую статический byte[], предварительно инициализированный размером моих данных, используя этот пост.

Вот мое окончательное решение:

    public File GetFileViaFileIdGuid(Guid fileId)
    {
        File file = new File();
        string connectionString = ConfigurationManager.ConnectionStrings["Database"].ConnectionString;
        using (var sourceSqlConnection = new SqlConnection(connectionString))
        {
            using (SqlCommand sqlCommand = sourceSqlConnection.CreateCommand())
            {
                sqlCommand.CommandText = $"SELECT FileName, FileExtension, UploadedDateTime, DATALENGTH(Content) as [ContentLength] FROM dbo.[File] WHERE FileId = '{fileId}'";
                sqlCommand.CommandType = CommandType.Text;
                sqlCommand.CommandTimeout = 300;
                sourceSqlConnection.Open();

                var reader = sqlCommand.ExecuteReader();
                while (reader.Read())
                {
                    file.FileId = fileId;
                    file.FileExtension = reader["FileExtension"].ToString();
                    file.FileName = reader["FileName"].ToString();
                    file.UploadedDateTime = (DateTime)reader["UploadedDateTime"];
                    file.Content = new byte[Convert.ToInt32(reader["ContentLength"])];
                }

                reader.Close();
                sourceSqlConnection.Close();
            }
        }
        file.Content = GetFileContent(file.FileId, file.Content.Length);
        return file;
    }

Чтобы получить содержимое:

    private byte[] GetFileContent(Guid fileId, int contentLength)
    {
        int outputSize = 1048576;
        int bufferSize = contentLength + outputSize;
        byte[] content = new byte[bufferSize];
        string connectionString = ConfigurationManager.ConnectionStrings["Database"].ConnectionString;

        using (SqlConnection sqlConnection = new SqlConnection(connectionString))
        {
            using (SqlCommand sqlCommand = sqlConnection.CreateCommand())
            {
                sqlCommand.CommandTimeout = 300;
                sqlCommand.CommandText = $"SELECT Content FROM dbo.[File] WHERE FileId = '{fileId}'";
                sqlConnection.Open();
                using (SqlDataReader reader = sqlCommand.ExecuteReader(CommandBehavior.SequentialAccess))
                {

                    while (reader.Read())
                    {
                        int startIndex = 0;
                        long returnValue = reader.GetBytes(0, startIndex, content, startIndex, outputSize);
                        while (returnValue == outputSize)
                        {
                            startIndex += outputSize;
                            returnValue = reader.GetBytes(0, startIndex, content, startIndex, outputSize);
                        }
                    }
                }

                sqlConnection.Close();
            }
        }
        return content;
    }
person Nathan Foss    schedule 19.04.2017
comment
Если ваш валидатор работает в отдельном процессе, вы можете передать файл напрямую на диск или передать поток другому процессу. Это позволяет вам захватывать файл небольшими порциями. - person Berin Loritsch; 19.04.2017
comment
@BerinLoritsch К сожалению, нет. Я использую Aspose.Cells для создания файла excel в памяти, и если у него нет всего содержимого, произойдет сбой. - person Nathan Foss; 19.04.2017
comment
Причина, по которой вы создаете 32-битное приложение, связана с библиотекой Aspose.Cells? В качестве несколько связанной точки данных я обнаружил, что самое большое 32-разрядное приложение C # в конечном итоге составляет около 1,4 ГБ, прежде чем столкнуться с проблемами памяти. Помните, что у вас также должно быть место для сборки мусора, и есть несколько корзин GC — если какая-либо из них заполнится, вы получите OutOfMemoryException. Наконец, если вы используете WinForms и пытаетесь нарисовать что-то меньше 1 пикселя, вы также получаете OutOfMemoryException. Это было самое удивительное, с чем я столкнулся. при решении проблем с памятью. - person Berin Loritsch; 20.04.2017