web-dev-qa-db-ja.com

ServletOutputStreamを使用して、メモリの問題なしにJavaサーブレットに非常に大きなファイルを書き込む

IBM Websphere Application Server v6とJava 1.4を使用しており、ユーザーがダウンロードするためにServletOutputStreamに大きなCSVファイルを書き込もうとしています。ファイルの範囲は50〜750MBです現時点では。

小さいファイルが問題を引き起こしているわけではありませんが、大きいファイルではヒープに書き込まれているように見え、それがOutOfMemoryエラーを引き起こし、サーバー全体を停止させます。

これらのファイルはHTTPSを介して認証されたユーザーにのみ提供されるため、Apacheに固定するのではなく、サーブレットを介して提供しています。

私が使用しているコードは次のとおりです(これに関連するいくつかの毛羽立ち):

_    resp.setHeader("Content-length", "" + fileLength);
    resp.setContentType("application/vnd.ms-Excel");
    resp.setHeader("Content-Disposition","attachment; filename=\"export.csv\"");

    FileInputStream inputStream = null;

    try
    {
        inputStream = new FileInputStream(path);
        byte[] buffer = new byte[1024];
        int bytesRead = 0;

        do
        {
            bytesRead = inputStream.read(buffer, offset, buffer.length);
            resp.getOutputStream().write(buffer, 0, bytesRead);
        }
        while (bytesRead == buffer.length);

        resp.getOutputStream().flush();
    }
    finally
    {
        if(inputStream != null)
            inputStream.close();
    }
_

FileInputStreamは、別のファイルに書き込んだり、書き込みを完全に削除したりしても、メモリ使用量に問題がないように見えるため、問題を引き起こしていないようです。

私が考えているのは、データがクライアントに送信されるまで、resp.getOutputStream().writeがメモリに保存されているということです。したがって、ファイル全体が読み取られてresp.getOutputStream()に保存され、メモリの問題とクラッシュが発生する可能性があります。

私はこれらのストリームをバッファリングしようとしましたが、_Java.nio_のチャンネルを使用しようとしましたが、どれも私のメモリの問題に少しの違いももたらさないようです。 OutputStreamもループの反復ごとに1回、ループの後に1回フラッシュしましたが、助けにはなりませんでした。

38
Martin

平均的なまともなservletcontainer自体は、デフォルトで〜2KBごとにストリームをフラッシュします。まったく同じソースからデータを連続してストリーミングする場合、OutputStreamHttpServletResponseで間隔を置いて明示的にflush()を呼び出す必要はありません。たとえば、Tomcat(およびWebsphere!)では、これはHTTPコネクターのbufferSize属性として構成可能です。

コンテンツの長さが事前に不明な場合( Servlet API仕様 !に従って)、クライアントがHTTP 1.1をサポートしている場合、平均的なまともなサーブレットコンテナは chunks でデータをストリーミングします。

問題の症状は、少なくともサーブレットコンテナがフラッシュ前にストリーム全体をメモリにバッファリングしていることを示しています。これは、コンテンツ長ヘッダーが設定されていない、および/またはサーブレットコンテナがチャンクエンコーディングをサポートしていない、および/またはクライアント側がチャンクエンコーディングをサポートしていない(つまり、HTTP 1.0を使用している)ことを意味します。

どちらかを修正するには、事前にコンテンツの長さを設定するだけです。

response.setHeader("Content-Length", String.valueOf(new File(path).length()));
42
BalusC
  1. Kevinのクラスは、close()演算子でnullでない場合、_m_out_フィールドを閉じる必要があります。物事を漏らしたくないのですか?

  2. ServletOutputStream.flush()演算子と同様に、HttpServletResponse.flushBuffer()オペレーションもバッファをフラッシュします。ただし、これらの操作に効果があるかどうか、またはhttpコンテンツの長さのサポートが妨げられているかどうかは、実装固有の詳細のようです。 content-lengthの指定はHTTP 1.0のオプションであるため、物事をフラッシュする場合は物事がただストリームアウトするはずです。しかし、私はそれを見ません

1
SteveL

したがって、シナリオに従って、whileループの外側ではなく、(繰り返しごとに)whileループの内側にフラッシュするべきではありませんか?ただし、少し大きいバッファーで試してみます。

1
Kostas

flushは出力ストリームで機能しますか。

バッファは必ずしも完全に読み取られるとは限らないため、3引数形式のwriteを使用する必要があることをコメントしたかったのです(特にfile(!)の最後)。また、サーバーを予期せず停止させない限り、try/finallyが適切です。

While条件が機能しないため、使用する前に-1を確認する必要があります。そして、出力ストリームに一時変数を使用してください。読みやすく、getOutputStream()を繰り返し呼び出しても安全です。

OutputStream outStream = resp.getOutputStream();
while(true) {
    int bytesRead = inputStream.read(buffer);
    if (bytesRead < 0)
      break;
    outStream.write(buffer, 0, bytesRead);
}
inputStream.close();
out.close();
1
eckes

この場合、ServletOutputStreamflush()が機能するかどうかもわかりませんが、ServletResponse.flushBuffer()はクライアントに応答を送信する必要があります(少なくとも2.3サーブレット仕様ごと)。

ServletResponse.setBufferSize()も有望です。

1
david a.

出力ストリームをラップするクラスを使用して、他のコンテキストで再利用できるようにしました。ブラウザーへのデータの取得を高速化する上で、私にとってはうまく機能しましたが、メモリーへの影響は見ていません。 (時代遅れのm_変数の命名はご容赦ください)

import Java.io.IOException;
import Java.io.OutputStream;

public class AutoFlushOutputStream extends OutputStream {

    protected long m_count = 0;
    protected long m_limit = 4096; 
    protected OutputStream m_out;

    public AutoFlushOutputStream(OutputStream out) {
        m_out = out;
    }

    public AutoFlushOutputStream(OutputStream out, long limit) {
        m_out = out;
        m_limit = limit;
    }

    public void write(int b) throws IOException {

        if (m_out != null) {
            m_out.write(b);
            m_count++;
            if (m_limit > 0 && m_count >= m_limit) {
                m_out.flush();
                m_count = 0;
            }
        }
    }
}
1
Kevin Hakanson

コードには無限ループがあります。

do
{
    bytesRead = inputStream.read(buffer, offset, buffer.length);
    resp.getOutputStream().write(buffer, 0, bytesRead);
}
while (bytesRead == buffer.length);

offsetはループ全体で同じ値を持っているため、最初にoffset =の場合、無限ループを引き起こし、OOMエラーにつながるすべての反復でそうなります。

0
rooparam

メモリの問題とは無関係に、whileループは次のようになります。

while(bytesRead > 0);
0
james