2013-02-27 15 views
13

Mi imbatto nella cosa più strana che non riesco a capire. Ho una tabella SQL con un mucchio di report memorizzati in un campo ntext. Quando ho copiato e incollato il valore di uno di loro nel blocco note e l'ho salvato (usato Visual Studio per prendere il valore da un report più piccolo in una riga differente), il file txt non elaborato era di circa 5Mb. Quando provo a ottenere questi stessi dati usando SqlDataReader e lo converto in una stringa, ottengo un'eccezione di memoria insufficiente. Ecco come sto provando a farlo:Memoria insufficiente durante la lettura di una stringa da SqlDataReader

string output = ""; 
string cmdtext = "SELECT ReportData FROM Reporting_Compiled WHERE CompiledReportTimeID = @CompiledReportTimeID"; 
SqlCommand cmd = new SqlCommand(cmdtext, conn); 
cmd.Parameters.Add(new SqlParameter("CompiledReportTimeID", CompiledReportTimeID)); 
SqlDataReader reader = cmd.ExecuteReader(); 
while (reader.Read()) 
{ 
    output = reader.GetString(0); // <--- exception happens here 
} 
reader.Close(); 

Ho cercato di creare un oggetto e uno StringBuilder per afferrare i dati, ma ho ancora la stessa di un'eccezione di memoria. Ho anche provato a utilizzare reader.GetValue (0) .ToString() e senza alcun risultato. La query restituisce solo 1 riga e quando viene eseguita in SQL Management Studio è la più felice possibile.

L'eccezione generata è:

System.OutOfMemoryException was unhandled by user code 
Message=Exception of type 'System.OutOfMemoryException' was thrown. 
Source=mscorlib 
StackTrace: 
at System.String.CreateStringFromEncoding(Byte* bytes, Int32 byteLength, Encoding  encoding) 
    at System.Text.UnicodeEncoding.GetString(Byte[] bytes, Int32 index, Int32 count) 
    at System.Data.SqlClient.TdsParserStateObject.ReadString(Int32 length) 
    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.GetString(Int32 i) 
    at Reporting.Web.Services.InventoryService.GetPrecompiledReportingData(DateTime ReportTime, String ReportType) in C:\Projects\Reporting\Reporting.Web\Services\InventoryService.svc.cs:line 3244 
    at SyncInvokeGetPrecompiledReportingData(Object , Object[] , Object[]) 
    at System.ServiceModel.Dispatcher.SyncMethodInvoker.Invoke(Object instance, Object[] inputs, Object[]& outputs) 
    at System.ServiceModel.Dispatcher.DispatchOperationRuntime.InvokeBegin(MessageRpc& rpc) 
InnerException: 
    null 

avevo provato con altri numeri di riga che sembravano funzionare, ma che era un falso positivo, come quelli di test ID avuto alcun dato. Ho estratto alcuni altri ID di test dopo aver esaminato la tabella che contiene report che sono quasi identici e ottengo la stessa eccezione. Forse è come la stringa è codificata? I dati memorizzati nella tabella sono una stringa codificata JSON che è stata generata da una classe davvero gnarly che ho creato da qualche altra parte, nel caso ciò sia d'aiuto.

Ecco il blocco di codice precedente:

// get the report time ID 
int CompiledReportTimeTypeID = CompiledReportTypeIDs[ReportType]; 
int CompiledReportTimeID = -1; 
cmdtext = "SELECT CompiledReportTimeID FROM Reporting_CompiledReportTime WHERE CompiledReportTimeTypeID = @CompiledReportTimeTypeID AND CompiledReportTime = @ReportTime"; 
cmd = new SqlCommand(cmdtext, conn); 
cmd.Parameters.Add(new SqlParameter("CompiledReportTimeTypeID", CompiledReportTimeTypeID)); 
cmd.Parameters.Add(new SqlParameter("ReportTime", ReportTime)); 
reader = cmd.ExecuteReader(); 
while (reader.Read()) 
{ 
    CompiledReportTimeID = Convert.ToInt32(reader.GetValue(0)); 
} 
reader.Close(); 

CompiledReportTypeIDs è un dizionario che ottiene il CompiledReportTimeTypeID corretta sulla base di un parametro di stringa che viene alimentato all'inizio del metodo. ReportTime è un DateTime che viene inserito in precedenza.

Modifica: Ho intenzione di rilasciare la tabella e ricrearla con il campo ReportData come nvarchar (MAX) anziché ntext, solo per escludere un problema di tipo di dati SQL. È una soluzione lunga e aggiornerò di nuovo con quello che trovo.

Edit2: La modifica del campo nella tabella in nvarchar (max) non ha avuto alcun effetto. Ho anche provato a usare output = cmd.ExecuteScalar(). ToString(), senza alcun impatto. Sto cercando di vedere se c'è una dimensione massima per SqlDataReader. Quando ho copiato il valore del testo da SQL Mgmt Studio, era solo 43Kb quando salvato nel blocco note. Per verificarlo, ho tirato un report con un ID funzionante noto (un report più piccolo) e quando ho copiato il valore direttamente da Visual Studio e l'ho scaricato nel blocco note era di circa 5 MB! Ciò significa che questi grandi report sono probabilmente nell'intervallo ~ 20 MB seduto in un campo nvarchar (massimo).

Edit3: Ho riavviato tutto, per includere il mio server IIS di dev, il server SQL e il mio portatile di sviluppo. Ora sembra che funzioni. Questa non è la risposta al motivo per cui questo è successo però. Lascio questa domanda aperta per le spiegazioni su quello che è successo, e segnerò una di quelle come risposta.

Edit4: Detto questo, ho eseguito un altro test senza modificare nulla e la stessa eccezione è stata restituita. Sto davvero iniziando a pensare che questo è un problema SQL. Sto aggiornando i tag su questa domanda. Ho creato un'app separata che esegue esattamente la stessa query e funziona correttamente.

Edit5: Ho implementato l'accesso sequenziale come da una delle risposte di seguito. Tutto viene letto correttamente in un flusso, ma quando provo a scriverlo su una stringa sto ancora ricevendo l'eccezione di memoria insufficiente. Indicherebbe il problema di ottenere un blocco contiguo di memoria?Ecco come ho implementato il buffer:

   reader = cmd.ExecuteReader(CommandBehavior.SequentialAccess); 
      long startIndex = 0; 
      long retval = 0; 
      int bufferSize = 100; 
      byte[] buffer = new byte[bufferSize]; 
      MemoryStream stream = new MemoryStream(); 
      BinaryWriter writer = new BinaryWriter(stream); 
      while (reader.Read()) 
      { 
       // Reset the starting byte for the new CLOB. 
       startIndex = 0; 

       // Read bytes into buffer[] and retain the number of bytes returned. 
       retval = reader.GetBytes(0, startIndex, buffer, 0, bufferSize); 

       // Continue while there are bytes beyond the size of the buffer. 
       while (retval == bufferSize) 
       { 
        writer.Write(buffer); 
        writer.Flush(); 

        // Reposition start index to end of last buffer and fill buffer. 
        startIndex += bufferSize; 
        retval = reader.GetBytes(0, startIndex, buffer, 0, bufferSize); 
       } 

       //output = reader.GetString(0); 
      } 
      reader.Close(); 
      stream.Position = 0L; 
      StreamReader sr = new StreamReader(stream); 
      output = sr.ReadToEnd(); <---- Exception happens here 
      //output = new string(buffer); 

Edit6: Per aggiungere a questo, quando eccezione OOM accade vedo il processo di lavoro IIS (che detiene il metodo che esegue) ha colpito quasi 700 MB. Questo è in esecuzione su IIS Express e non su IIS completo sul server di produzione. Questo dovrebbe avere qualcosa a che fare con questo? Anche quando chiamo Byte [] data = stream.ToArray() ottengo a intermittenza anche l'OOM. Penso che quello di cui ho veramente bisogno sia un modo per dare più memoria a questo processo, ma non so dove configurarlo.

Edit7: Ho appena modificato il mio server di sviluppo utilizzando IIS Express sul mio computer locale sul server Web di Visual Studio incorporato. L'eccezione di OOM ora è andata. Penso davvero che sia stato l'allocazione di un blocco contiguo di problemi di memoria, e per qualsiasi motivo IIS Express non lo rinviasse. Ora che sta funzionando bene, pubblicherò sul mio server completo su 2008R2 che esegue il normale IIS7 per vedere come va.

+1

È necessario includere anche il messaggio di errore completo. –

+1

Quanto è grande la stringa che viene restituita? In altre parole, quanto è grande ReportData? –

+0

Mostra la traccia stack completa dell'eccezione. –

risposta

9

Si dovrebbe provare a leggere i dati in sequenza specificando command behavior quando si esegue il lettore. Per la documentazione, Utilizzare SequentialAccess per recuperare valori di grandi dimensioni e dati binari. In caso contrario, potrebbe verificarsi un'eccezione OutOfMemoryException e la connessione verrà chiusa.

Mentre l'accesso sequenziale viene in genere utilizzato su grandi dati binari, in base alla documentazione MSDN è possibile utilizzarlo per leggere grandi quantità di dati di carattere.

Quando si accede ai dati nel campo BLOB, utilizzare i GetBytes o GetChars di accesso del DataReader digitati, che riempiono un array con dati . Puoi anche utilizzare GetString per i dati dei caratteri; però. a conservare le risorse di sistema che potresti non voler caricare un intero valore BLOB in una variabile a stringa singola. È invece possibile specificare una dimensione del buffer specifica di da restituire e una posizione iniziale per il primo byte o carattere da leggere dai dati restituiti. GetBytes e GetChars restituiscono un valore lungo, che rappresenta il numero di byte o caratteri restituiti. Se si passa un array nullo a GetBytes o GetChars, il valore lungo restituito sarà il numero totale di byte o caratteri nel BLOB. Opzionalmente è possibile specificare un indice nella matrice come posizione di partenza per i dati letti.

Questo MSDN example mostra come eseguire l'accesso sequenziale. Credo che tu possa usare il metodo GetChars per leggere i dati testuali.

+0

Sembra promettente. Ho bisogno di jet per il giorno, ma proverò questa prima cosa al mattino. –

+0

Il buffering funziona alla grande, ma sto ricevendo l'eccezione di OOM quando provo a scrivere lo stream che ho creato su una stringa. Quando ho usato .GetChars() invece di .GetBytes(), otterrei immediatamente OOM mentre stavo cercando di ottenere la lunghezza del campo per creare un'istanza dell'array char che avrebbe contenuto i risultati. –

+0

È possibile utilizzare DATALENGTH per restituire la lunghezza totale come parte del set di risultati, quindi utilizzare tale valore per costruire l'array prima di leggere il risultato in blocchi. – Oppositional

0

indovinare qui.

cmd.Parameters.Add(new SqlParameter("CompiledReportTimeID", CompiledReportTimeID)); 

hai perso il segno @. quindi sostituisce entrambe le istanze di CompiledReportTimeID con l'id e ottieni invece tutti i risultati a causa dell'uguaglianza?

+0

Se aggiungo @ al primo argomento per il parametro SQL, ottengo lo stesso risultato. Stack Overflow ha formattato il CompiledReportTimeID in modo divertente, ma è solo un int. Ho chiamato il parametro la stessa cosa dell'int, che è anche lo stesso nome del campo nella tabella. Probabilmente non è una buona pratica nominarla in questo modo (la sistemerò più tardi una volta che avrò risolto questa cosa stupida) –

4

Fondamentalmente, un System.OutOfMemoryException non si verifica solo quando si è esaurito la memoria, ma quando non è possibile allocare un singolo blocco di memoria contiguo per un oggetto. Potrai vedere spesso tale errore quando si cerca di creare un array di grandi dimensioni, o caricare un oggetto bitmap di grandi dimensioni, o, talvolta, durante la creazione di grandi XMLDocument ...

Array e String tipicamente devono essere assegnati in modo contiguo, vale a dire non si può essere suddiviso in pezzi e assegnato in spazi vuoti nella memoria.

Questo probabilmente non è un problema SQL ed è più un problema con SqlReader che tenta di allocare una stringa abbastanza grande da contenere i dati in una riga.

Hai detto che ha funzionato correttamente dopo un riavvio, quindi supponiamo che il tuo codice sia fondamentalmente corretto (probabilmente può ancora essere ottimizzato per esporre piuttosto i dati come flusso invece di buffering del recordset) e che il sintomo corrente è ambientale. Una macchina appena riavviato forse non ha la memoria più frammentato, ma come è stato utilizzato di più, la memoria frammentata e l'errore restituito ...

È può essere in grado di dimostrare la teoria memoria contigua chiudendo come molti altri programmi possibili e aggiungendo codice per forzare un GC.Collect(GC.MaxGeneration) (reference) prima del codice con l'errore. Questa non è una garanzia, in quanto la memoria allocata al processo potrebbe essere ancora frammentata.

Penso che lo streaming del valore potrebbe essere il modo per fermare l'errore che si verifica, e meglio evitare di provare a bufferare tutto in una stringa. Lo svantaggio di questo è che manterrai aperta la connessione al database mentre il risultato è in streaming/consumato dal resto del programma e questo porterà i propri overhead. Non sono sicuro di cosa debba fare il codice con il risultato, ma se ha bisogno di lavorare con un'istanza String, potrebbe essere necessario espandere la memoria disponibile per il processo (diversi modi per farlo, ma potrebbe essere fuori tema - lasciare un commento e posso aggiungere a questa risposta se necessario)

+0

Ho provato a forzare il GC senza alcun risultato (buona idea però!). Ho implementato il buffering come da risposta di Oppositional e sto ricevendo l'OOM quando provo a scaricare il flusso su una stringa. Questo mi porta ad essere d'accordo con il problema di allocazione della memoria. Hai un link a una guida che posso seguire per espandere la memoria di processo disponibile? Dovrei trovare un modo per fare ciò di cui ho bisogno senza questo, naturalmente, ma qualcosa del genere funzionerà per ora. –

+0

Consiglio vivamente di provare a modificare le impostazioni dell'ambiente per far funzionare la memoria. Hai la possibilità di trasmettere i dati alla destinazione? per esempio. immagina di essere la stazione di pompaggio tra due dighe, non puoi succhiare tutta l'acqua in una diga prima di pomparla nell'altra. Devi lavare un buffer alla volta. Penso che il problema stia cercando di scaricare tutti i dati in una stringa, meglio evitare di farlo. –

Problemi correlati