none
.net WebService経由で100MB超えのファイルがダウンロードできません RRS feed

  • 全般的な情報交換

  • ご覧いただきありがとうございます。

    VS2005上で.net C#2.0でIISサーバー上のファイルから1MBづつ読み取り、Win C#クライアントで1MBずつ戻すことで大ファイルのダウンロードをすることを目論みましたが、実際にWebサービスのメソッドから1回に受け取るのは1MB程度なのに目的のファイルが100MBを超える場合、ダウンロードに失敗します。

    サービス側のメソッドは

    [WebMethod]

    public byte[] DownloadFilePacketByte(string サーバー上ファイルパス, long 読み取り開始位置)

    {

    FileStream fs = null;
    FileInfo finfo = new FileInfo(サーバー上ファイルパス);
    fs = finfo.OpenRead();
    byte[] bs = new byte[1048576];
    fs.Seek(読み取り開始位置, SeekOrigin.Begin);

    int readSize = fs.Read(bs, 0, bs.Length);

    if (readSize != bs.Length)
    {
         bs = null;
         bs = new byte[readSize];
         fs.Seek(読み取り開始位置, SeekOrigin.Begin);
         readSize = fs.Read(bs, 0, bs.Length);
    }
    fs.Close();
    ByteArray = bs;
    fs.Dispose();
    return ByteArray;

    }

    クライアントからは以下の容量で1MBずつ受信、読み取り開始位置をカウントアップしてクライアントローカルファイルに保存しています。

    for (long i = 0; i < ファイルサイズまで; i+=1048576 )
         {
                    
         // サービスから分割パケット受信
         byte[] ByteArray = webサービス.DownloadFilePacketByte(サーバー上ファイルパス, 読み取り開始位置);

         FileStream fs;
         if (i == 0)
         {
            fs = File.Create(クライアント保存ファイルパス);
         }
         else
         {
            fs = File.OpenWrite(クライアント保存ファイルパス);
         }
       
         using (BinaryWriter bw = new BinaryWriter(fs))
         {
            if (i > 0)
               {
                  bw.Seek(0, SeekOrigin.End);
               }
            bw.Write(ByteArray);
         }
                       
         読み取り開始位置 += 1048576;
         ByteArray = null;
         fs.Close();
         }
    }

    この仕組みでVisualStudio上でクライアント側のforループ内でどこかしらブレイクするようにしてF5キーで徐々に実行すると、特に例外メッセージもなく大きめのファイル(400MBくらいです)がダウンロードできましたが、ブレイクなしのVisualStudio上での実行、または実行形式のクライアントのexeとIIS上にASP.NET webサービス展開して実行すると、OutOfMemoryExceptionであったり、XMLExceptionであったりと何かしらエラーとなりクライアント側での保存に失敗します。ちなみに100MB以下くらいのファイルは上記の仕組みで実行環境でも正常なダウンロードが終了しました。

    逆の事例で、クライアントからASP.NET Webサービス経由で大きいファイルをアップロードする場合、web.configにmaxrequestLengthで許容値を上げたりすることができますが、ダウンロードする場合にも同様のweb.configまたはクライアントアプリのapp.configに何かしら許容値設定などが必要なのでしょうか?もしプログラミング上での解決手法もありましたらご教授よろしくお願いいたします。

    • 種類を変更済み 山本春海 2012年4月24日 8:13 自己解決されているようなので、ステータスを変更させていただきました。
    2012年4月11日 8:19

すべての返信

  • 方法 : WebMethod 属性を使用するによると

    WebMethod 属性の BufferResponse プロパティは、XML Web サービス メソッドに対する応答のバッファリングを有効にします。既定の設定である true に設定すると、ASP.NET は、応答をクライアントに送信する前に、応答全体をバッファリングします。バッファリングは効率が高く、ワーカー プロセスと IIS プロセスの間の通信を最小限に抑えることにより、パフォーマンスの向上に役立ちます。false に設定すると、ASP.NET は応答を 16 KB ずつバッファリングします。一般に、このプロパティを false に設定するのは、応答全体の内容を一度にメモリに格納すると問題がある場合だけです。たとえば、データベースから項目をストリーミングするコレクションをデータベースに書き戻す場合などです。指定しない場合、既定値は true です。詳細については、「BufferResponse プロパティ」を参照してください。

    とあります。今回問題があるわけですから、falseにすると改善されるかもしれません。

    ところで、悪意あるクライアントから意図しないファイルパスを渡された場合、情報漏えいにつながりませんか? 質問のために省略されているのなら問題ありませんが。

    2012年4月11日 9:29
  • 提示していただいているソースをもとに、自分の環境で試したのですが、再現しませんでした。動作中、メモリ確保をどんどんしていいて、動作が終わると徐々にメモリが解放されて行きました。とにかくメモリ不足でOutOfMemoryExceptionが発生していることは確かなので、調べたら以下の情報を見つけました。

    http://support.microsoft.com/kb/954830/ja

    環境に依存するようです(私の環境はWindows 7 64bit メモリ8GBです)。

    解決策としては、ループ内でメモリ確保をしない、だと思います。

    2012年4月11日 14:05
  • 解決策としては、ループ内でメモリ確保をしない、だと思います。
    質問に挙げられているコードのうち、前半のASP.NET部分にはループは存在しませんし、後半のクライアントアプリケーション部分にはループは存在しますがメモリ確保が存在しません。どういった指摘なのでしょうか…?
    2012年4月12日 0:11
  • >     byte[] ByteArray = webサービス.DownloadFilePacketByte(サーバー上ファイルパス, 読み取り開始位置);

    がメモリ確保せずに動作すれば確保しないと言っていいけど、バイト配列を return するのに確保しないはずはないよね。

    ファイルへの書き込みは API にバッファを渡す為にメモリのピンニングが発生する為 GC を阻害してしまう事があります。

    • LOH をGCしたいんだけどピンニングで阻害されてしまう
    • CLR2 にはLOH のフラグメント化肥大の問題がある

    この合わせ技問題かなと個人的には推測します。

    可能であればデータの単位を 1MB から20KB程度に減らす(CLR2世代では24KBからがLOH行きだったと記憶しています)

    1MB単位で処理したいのであれば20KB程度のバッファの配列で扱いLOHを苛めない様にするか、1MBのバッファを使い回し続けて処理するかの二択と思います。


    Kazuhiko Kikuchi

    2012年4月12日 1:19
  • そもそも、「エラー」はサーバとクライアントのどちらで起きているエラーなんでしょうか?

    >解決策としては、ループ内でメモリ確保をしない、だと思います。

    ということは、クライアント側で OutOfMemory 例外が発生しているということですか?

    2012年4月12日 1:36
    モデレータ
  • ええ、もちろん呼び出し先はWebサービスクライアントのコードですから、XMLパーサーやHTTPクライアントを含むかなりの処理が実行されその中でメモリ確保をしているのは確実です。しかしそんなことを言っていてはループ内からあらゆるメソッド呼び出しができなくなりますよね…。なのでここは簡略化し、当該メソッドについてはループ内でメモリ確保していないと指摘しました。
    # すっとばしてすみません。

    実際このようなループではバイト配列はGCによって再利用されないものなのでしょうか…? 幸い本件は送信されるデータが毎回 new byte[1048576] とのことで、ブロックサイズがきっちりと一致するのにもかかわらず、です。pinnedであろうと解放済みなんだから再利用しそうなものですが。

    1MBごとに区切るよりいい方法がないかなと思って返信前に調べましたが、ASP.NET Webサービスからはストリームを返せないのですね。非同期メソッドはありましたがWebサーバーの負担が低減されるだけですし、今回はクライアントからシーケンシャルに呼び出されてるので大差はなさそうで。いっそのこと、この部分はWebサービスを止めて、ファイルを直接返すとかの方が楽そう。分割したいならHTTP Rangeヘッダーもありますし。

    2012年4月12日 2:11
  • >実際このようなループではバイト配列はGCによって再利用されないものなのでしょうか…?

    「GC の違い(WorkstationGC or ServerGC, ...)」によってこの辺の挙動が変わるのは確かですが、総計 100MB で音を上げるという状況はちょっと極端な気がします。

    2012年4月12日 3:15
    モデレータ
  • > しかしそんなことを言っていてはループ内からあらゆるメソッド呼び出しができなくなりますよね…。

    自分は書いてる通りで return される byte 配列について言ってます。WebサービスプロキシやXMLパーサーがいかにメモリに対して効率的に書かれていても return されるbyte 配列は確保される以外に出所はありませんよねという事です。

    > pinnedであろうと解放済みなんだから再利用しそうなものですが。

    CLR2のLOHのフラグメント化問題に言及させてもらってる通りで、LOHに同一サイズの繰り返し確保解放がうまく再利用できないのはCLR2の固有の問題です。

    http://www.simple-talk.com/dotnet/.net-framework/the-dangers-of-the-large-object-heap/

    上記参照、20MBの確保解放を繰り返したら 2GB もメモリを使って OutOfMemory で死ぬという事が起こります。

    また、pinned である解放済みブロックというのは存在しません。pinnedブロックがある事でGCの動作が阻害されて解放されない可能性について言及させてもらいましたので、解放済みであるか以前にGCで解放できているのかが問題なのです。

    自分の憶測を元に修正を試みるなら元コードを以下のように変えただけでも、pinned ブロックが存在するGC世代も、pinnedブロックのサイズも変わるわけで挙動が変わるかも知れません。

         using (BinaryWriter bw = new BinaryWriter(fs))
         {
            if (i > 0)
               {
                  bw.Seek(0, SeekOrigin.End);
               }
            // bw.Write(ByteArray); ByteArray が pin されない様に出力バッファを噛ませる
            byte[] outBuffer = new byte[16*1024];
            for( index=0;index<ByteArray.Length;index+=outBuffer.Length )
            {
                Buffer.BlockCopy( ByteArray, index, outBuffer, 0, outBuffer.Length );
                bw.Write(outBuffer);
            }
         }


    Kazuhiko Kikuchi

    2012年4月12日 3:52
  • 佐祐理さま、

    初回レスありがとうございました。このwebMethod属性は今まで気にしたことがありませんでした。今回の小生の上記ソースのまま、BufferResponseを追加しただけでは効果がありませんでした。

    特に1MBというこだわりはなく、セキュリティもあまり意図しない実験段階でした。

    続くスレの展開もありがとうございます。

    2012年4月12日 6:19
  • 渋木宏明さま、

    レスありがとうございます。OutOfMemory例外はクライアント側です。クライアント側の環境はXP 空きメモリ256MB程度でシステムドライブの空きディスク容量は3GB程度です。分割しないで丸ごとbyte[]で返す場合も100MBくらいで音を上げてしまうようです。

    2012年4月12日 6:23
  • kazukさま、

    レスありがとうございます。上記のバッファを噛ます手法や20KB程度問題については知りませんでした。まずWebメソッドからのreturn byte[]サイズを16KBに変更しても、クライアント側は現状のままだとやはりOutOfMemoryが発生しました。その場合でもクライアント側のループ処理中のメモリ消費が激しく、クライアント側で処理中に空きメモリが枯渇してディスクスワップも起こり始めてしまいました。上記ご提示のバッファ利用案や、今は思いつきませんが根本的に別の手法を模索しようと思います。

    2012年4月12日 6:30
  • ちなみにですが、Webメソッド1回の呼び出しで 1MB のデータが送出されてくるからといって、クライアント側で「1MB のバッファを確保して待たなければならない」理由はありません。

    現実的には数 KB のバッファを用意して、Stream から何回かに分けて受信すれば十分なはずです。

    >クライアント側の環境はXP 空きメモリ256MB程度

    「余裕がある」状態には見えませんが、仮想記憶が有効なら踏ん張れそーな印象です。

    2012年4月13日 1:07
    モデレータ
  • 提示いただいたURL、これはpinnedには言及されていませんがあまり関係ないということでしょうか? CLRがLOHをうまく扱えないことは知りませんでした。挙げられたページに記載されているコードで言うと Fill(bool allocateBigBlocks = true, bool grow = false, bool alwaysGC = false)でしょうか。スクリーンショットでは706MBとなっていますが、改めて詳しく検証してみました。

    Windows Virutal PC上にWindows XP SP3をインストールし、そこで.NET Framework 2.0 SP2をインストールしました。この状態では上記ページと同様に706MBという結果が得られました。続いてWindows Updateに含まれているKB982524を適用したところ、1787MBまで確保できるようになりました。KBには修正内容が記載されていませんが、このパッチで改善するようです。

    質問者さんへ:
    KB982524が未適用なようでしたら適用してみてください。

    2012年4月13日 5:23
  • その後一応の解決ができました。

    OSやスペックでの組み合わせまでは確認していませんが、Win .netクライアントでASP.NETサービスからの一度の受信データの限界として100MB程度であることがわかりました。

    そこで上記のようなサーバーから分割取得、クライアント側でループでアペンドしていく案そのものは大丈夫でしたが、上記の小生のサンプルコードは一部の抜き出しで、クライアント側のOutOfmemery例外は、別の問題に起因していました。以下説明します。

    上記サンプルコードでは、クライアント側でbyte[]情報から「新しい」ファイルとして作成されますが、これだと保存されたファイルの日付などがダウンロード時点のファイル属性になってしまいます。でもサーバーに保存されているファイル日付のままでダウンロードしたいので、ダウンロード直前にサーバーから別WebメソッドAでSystem.Io.FileInfoでファイル日付情報を独自構造体FileAttrInfoとして取得し、

    bw.Write(ByteArray);

    の直後に

    File.SetCreationTime(作成されたパス、FileAttrInfo.CreationTime)

    の処理があったのですが、その別WebメソッドA内で(流れ的には不要なのに)BinaryReaderで対象ファイルの丸ごとbyte[]読みをしていました。FileAttrInfo.byte[]というメンバです。そのbyte[]が100MBを超えていると、クライアントで受け取った際にbyte[]はnullになっており、さらにCreationTimeがなぜかありえない日付に変わっていました。この手順を無くしたところ、1MBずつのままでもクライアント側のメモリ消費も良いものになりました。8割方実装の問題だったようです。

    こののち、より巨大ファイルのダウンロードができるか試してみる予定です。

    思わぬ反響が多くありがとうございました。

    2012年4月16日 7:41
  • わざわざ仮想環境まで準備してのご確認ありがとうございました。ご提示のKBは適用しなくてもとりあえず解決できました。分割しないで1度にまるごとダウンロードするパターンにおいて、上記KB適用効果を試してみたいと思います。ありがとうございました。
    2012年4月16日 7:43
  • 16KB単位とかでも発生する時点で何かおかしい(出ているコード以外に何かやってるんではないか?)と思いましたが、そういうわけでしたか。

    ちなみに、100MBという数値はかなり偶然の数値です(というか、Webサービスから取得するという状況において、もっとも運の良い状態でといってもいいかもしれません)。

    単一のサイズの大きい配列を確保しようとした場合にメモリ不足になるのは、既に出ている話ですがLOHの断片化によって、連続領域の確保ができなかったという場合が多いです(合計の容量が不足なのではなく、連続領域が確保できない)。

    よって、プログラムを色々動かして、特にサイズの大きな領域を色々使った後の状態だと、実際のメモリ量はあいていても、場合によっては数MB程度の配列の確保すら失敗することもあります。

    ですので、100MBなら一度でダウンロードできる、などとは思わないほうが良いです(非常に運に左右されます)。

    2012年4月17日 1:36