15

私は理解できない最も奇妙なことに遭遇しています。ntextフィールドに多数のレポートが格納されたSQLテーブルがあります。そのうちの1つの値をコピーしてメモ帳に貼り付けて保存すると(Visual Studioを使用して別の行の小さなレポートから値を取得)、生のtxtファイルは約5Mbでした。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();

データを取得するためにオブジェクトと文字列ビルダーを作成しようとしましたが、それでも同じメモリ不足の例外が発生します。また、reader.GetValue(0).ToString()を使用してみましたが、役に立ちませんでした。クエリは1行しか返しません。SQLManagementStudioで実行すると、可能な限り満足のいく結果が得られます。

スローされる例外は次のとおりです。

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

動作しているように見える他の行番号でテストしましたが、それらのテストIDにはデータがなかったため、誤検知でした。ほぼ同一のレポートを含むテーブルを確認した後、他のいくつかのテストIDを取得しましたが、同じ例外が発生します。たぶん、文字列がどのようにエンコードされているのでしょうか?テーブルに格納されているデータは、JSONでエンコードされた文字列であり、他の場所で作成した非常に厄介なクラスから生成されたものです。

上記のコードブロックは次のとおりです。

// 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は、メソッドの先頭で入力された文字列パラメーターに基づいて正しいCompiledReportTimeTypeIDを取得するディクショナリです。ReportTimeは、以前に入力されたDateTimeです。

編集:SQLデータ型の問題を除外するために、テーブルを削除し、ReportDataフィールドをntextではなくnvarchar(MAX)として再作成します。それはロングショットであり、私が見つけたもので再び更新します。

Edit2:テーブルのフィールドをnvarchar(max)に変更しても効果はありませんでした。また、output = cmd.ExecuteScalar()。ToString()も使用してみましたが、影響はありませんでした。SqlDataReaderの最大サイズがあるかどうかを確認しようとしています。SQL Mgmt Studioからテキストの値をコピーしたとき、メモ帳に保存したときの値はわずか43Kbでした。これを確認するために、既知の作業ID(小さいレポート)を使用してレポートを取得しました。値をVisual Studioから直接コピーしてメモ帳にダンプすると、約5MBになりました。つまり、これらの大きなレポートは、おそらくnvarchar(max)フィールドにある約20MBの範囲にあります。

Edit3:開発IISサーバー、SQLサーバー、開発ラップトップを含めるために、すべてを再起動しました。今は動いているようです。しかし、これはなぜこれが起こったのかについての答えではありません。何が起こったのかを説明するためにこの質問を開いたままにしておき、そのうちの1つを回答としてマークします。

Edit4:そうは言っても、何も変更せずに別のテストを実行したところ、同じ例外が返されました。これはSQLの問題だと本当に思い始めています。この質問のタグを更新しています。まったく同じクエリを実行する別のアプリを作成しましたが、正常に実行されます。

Edit5:以下の回答の1つに従って、シーケンシャルアクセスを実装しました。すべてが適切にストリームに読み込まれますが、文字列に書き出そうとすると、メモリ不足の例外が発生します。これは、メモリの連続ブロックを取得する問題を示していますか?バッファリングを実装した方法は次のとおりです。

                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:これに加えて、OOM例外が発生すると、IISワーカープロセス(実行中のメソッドを保持している)が約700MBに達することがわかります。これはIISExpressで実行されており、運用サーバーの完全なIISでは実行されていません。これはそれと何か関係がありますか?また、Byte [] data = stream.ToArray()を呼び出すと、断続的にOOMも取得します。私が本当に必要としているのは、このプロセスにより多くのメモリを与える方法だと思いますが、これをどこで構成するかはわかりません。

Edit7:開発サーバーをローカルマシンでのIISExpressの使用から組み込みのVisualStudioWebサーバーに変更しました。OOM例外はなくなりました。これは、メモリの問題の連続したブロックを割り当てることであり、何らかの理由でIISExpressがそれをフォークしなかったと本当に思います。正常に動作しているので、通常のIIS7を実行している2008R2の本格的なサーバーに公開して、どのように動作するかを確認します。

4

3 に答える 3

13

リーダーの実行時にコマンドの動作を指定して、データを順番に読み取るようにしてください。ドキュメントに従って、SequentialAccess を使用して大きな値とバイナリ データを取得します。そうしないと、OutOfMemoryException が発生し、接続が閉じられる可能性があります

シーケンシャル アクセスは通常、大量のバイナリ データに対して使用されますが、MSDN のドキュメントに基づいて、大量の文字データの読み取りにも使用できます。

BLOB フィールドのデータにアクセスするときは、DataReader の GetBytes または GetChars 型指定されたアクセサーを使用して、配列にデータを入力します。文字データに GetString を使用することもできます。でも。システム リソースを節約するために、BLOB 値全体を単一の文字列変数にロードしたくない場合があります。代わりに、返されるデータの特定のバッファー サイズと、返されたデータから読み取られる最初のバイトまたは文字の開始位置を指定できます。GetBytes と GetChars は、返されたバイト数または文字数を表す long 値を返します。GetBytes または GetChars に null 配列を渡すと、返される long 値は、BLOB 内の合計バイト数または文字数になります。オプションで、読み取られるデータの開始位置として配列内のインデックスを指定できます。

このMSDN の例は、順次アクセスを実行する方法を示しています。GetCharsメソッドを使用してテキスト データを読み取ることができると思います。

于 2013-02-28T21:59:43.217 に答える
7

基本的に、System.OutOfMemoryExceptionメモリが不足している場合だけでなく、オブジェクトに単一の連続したメモリ ブロックを割り当てることができない場合にも発生します。非常に大きな配列を作成しようとしたり、大きなビットマップ オブジェクトを読み込もうとしたり、時には大きな XmlDocuments を作成したりするときに、このエラーがよく見られます...

Array通常、String連続して割り当てる必要があります。つまり、断片に分割してメモリ内の空きスペースに割り当てることはできません。

これはおそらく SQL の問題ではなく、行にデータを格納するのに十分な大きさの文字列を割り当てようとする SqlReader に関する問題です。

再起動後に正常に動作したとのことでしたので、コードが基本的に正しく (レコードセットをバッファリングするのではなく、データをストリームとして公開するように最適化できる可能性があります)、現在の症状が環境にあると仮定しましょう。再起動したばかりのマシンには断片化されたメモリがあまりない可能性がありますが、それをさらに使用すると、メモリが断片化し、エラーが返されます...

できるだけ多くの他のプログラムを閉じ、エラーのあるコードの前に(参照) を強制するコードを追加することで、連続メモリ理論を証明できる場合があります。プロセスに割り当てられたメモリがまだ断片化されている可能性があるため、これは保証ではありません。GC.Collect(GC.MaxGeneration)

値をストリーミングすることは、エラーの発生を止める方法であり、すべてを文字列にバッファリングしようとするのを避ける方がよいと思います。これの欠点は、結果がプログラムの残りの部分によってストリーミング/消費されている間、データベース接続を開いたままにし、それが独自のオーバーヘッドをもたらすことです。コードが結果に対して何をする必要があるのか​​ わかりませんが、Stringインスタンスで動作する必要がある場合は、プロセスで使用できるメモリを拡張する必要があるかもしれません (それを支援するいくつかの方法ですが、トピックから外れている可能性があります -コメントを残してください。必要に応じてこの回答に追加できます)

于 2013-03-01T08:34:45.933 に答える
0

ここで野生の推測。

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

あなたは@記号を逃しました。CompiledReportTimeID の両方のインスタンスを id に置き換え、同等であるため代わりにすべての結果を取得しますか?

于 2013-02-28T21:00:05.870 に答える