fc2ブログ
ホーム   »  データベース/JDBC  »  Java+PostgreSQLでBLOBを扱う その1

Java+PostgreSQLでBLOBを扱う その1

PostgreSQL 7.4.11 (JDBC 3 Driver 7.4.215) の BYTEA (BLOB) 型カラムにファイルの内容を放り込もうと思ったのだが、ファイルサイズが数十MB になると例外が発生してしまうようだ。

java.lang.OutOfMemoryError: Java heap space

例外が起きなくてもディスクスワップが多発しているようだし、ひょっとしてファイルの全バイナリから巨大な SQL でも組み立てているのだろうか? 今回はそこらへんをちょっと突っ込んで調べてみようと思う。

◆ ソースを紐解く

何とも不安に駆られながら PostgreSQL 7.4 のソースを取得し、同封されている JDBC ドライバのソースから setBinaryStream を検索してみる。JDBC 3 を使っているのだが、実際に使われているのは JDBC 1 の実装らしい (JDBC 3 はソースこそあれ中は未実装)。

public void setBinaryStream(int parameterIndex, InputStream x, int length)
    throws SQLException
{
    if (connection.haveMinimumCompatibleVersion("7.2")){
        if (x == null) {
            setNull(parameterIndex, Types.VARBINARY);
            return;
        }

        // Version 7.2 supports BinaryStream for for
        // the PG bytea type As the spec/javadoc for
        // this method indicate this is to be used for
        // large binary values (i.e. LONGVARBINARY)
        // PG doesn't have a separate long binary
        // datatype, but with toast the bytea datatype
        // is capable of handling very large values.
        // Thus the implementation ends up calling
        // setBytes() since there is no current way
        // to stream the value to the server
        byte[] l_bytes = new byte[length];
        int l_bytesRead = 0;
        try {
            while (true){
                int n = x.read(l_bytes, l_bytesRead, length - l_bytesRead);
                if (n == -1)     break;
          
     l_bytesRead += n;
               
if (l_bytesRead == length)      break;
            
}
        } catch (IOException l_ioe) {
            throw new PSQLException("postgresql.unusual",
                 PSQLState.UNEXPECTED_ERROR, l_ioe);
        }
        if (l_bytesRead == length){
            setBytes(parameterIndex, l_bytes);
        } else {
            // the stream contained less data
        // than they said
            byte[] l_bytes2 = new byte[l_bytesRead];
            System.arraycopy(l_bytes, 0, l_bytes2, 0, l_bytesRead);
            setBytes(parameterIndex, l_bytes2);
        }
    } else {
        // 7.1 以前の場合の処理...
    }
}

. .: : : : : : : : :: :::: :: :: : :::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
    . . : : : :: : : :: : ::: :: : :::: :: ::: ::: ::::::::::::::::::::::::::::::::::::::
   . . .... ..: : :: :: ::: :::::: :::::::::::: : :::::::::::::::::::::::::::::::::::::::::::::
        Λ_Λ . . . .: : : ::: : :: ::::::::: :::::::::::::::::::::::::::::
       /:彡ミ゛ヽ;)ー、 . . .: : : :::::: :::::::::::::::::::::::::::::::::
      / :::/:: ヽ、ヽ、 ::i . .:: :.: ::: . :::::::::::::::::::::::::::::::::::::::
      / :::/;;:   ヽ ヽ ::l . :. :. .:: : :: :: :::::::: : ::::::::::::::::::
 ̄ ̄ ̄(_,ノ  ̄ ̄ ̄ヽ、_ノ ̄ ̄ ̄ ̄

いきなりストリームと同じ大きさのバッファを確保する大技に出たか… 全バイナリを読み込んで setBytes() で済ますとは手抜きが酷すぎるな。こりゃサイズが数十MBになればメモリ足りなくなって当然だし、サーバ用途で使うならデータサイズを 1MB 未満に抑えないと厳しすぎる。一体何のための LOB だよ…

◆ PostgreSQL 8 用ドライバ

ひとしきり驚愕したところでこの実装が直るわけでもなく。とりあえずこのドライバでは MB 級の BLOB は扱えないと判断し、PostgreSQL 8.1 用のドライバを試してみることにした。ダウンロードページによれば:

一般的にはサーバと同じバージョンを使えば良いが、問題があるようなら上位バージョンを使うべし。そちらのほうがバグフィクスも進んでいる。

とのことなので、下位互換性は考慮されているようだ。早速 JAR ファイルを入れ替えて実行してみた。結論から言ってしまうと

8.1 の JDBC3 ドライバは問題無し!
超問題あり。次の記事参照。

なのである。OutOfMemoryError が発生しなくなっているどころか、ディスクスワップも発生しなくなっている。このドライバはファイルの内容をメモリに展開しないようだ。

8.1 のソースをダウンロードして紐解いてみた。setBinayStream で検索してみると org.postgresql.jdbc2.AbstractJdbc2Statement というクラスに実装されているようだ。

public void setBinaryStream(int parameterIndex, InputStream x, int length) throws SQLException
{
    // ...

    if (connection.haveMinimumCompatibleVersion("7.2")){
        // Version 7.2 supports BinaryStream for for
        // the PG bytea type As the spec/javadoc for
        // this method indicate this is to be used for
        // large binary values (i.e. LONGVARBINARY)
        // PG doesn't have a separate long binary
        // datatype, but with toast the bytea datatype
        // is capable of handling very large values.

        preparedParameters.setBytea(parameterIndex, x, length);
    } else {
        // 7.1 以前の場合の処理...
    }
}

問題の部分が ParameterList#setBytea() に置き換わっている。これをたどって行くと、入力ストリームおよび長さを単にパラメータとして保持しているだけのようである。

さらに、データベースと通信する部分まで踏み込んでみる。execute() からたどって、実際にストリームの内容が送信されるのは SimpleParameterList#streamBytea()PGStream#SendStream() である。もうここまで来れば答えは出たも同然だが、最後の最後で御腐れコードになっていないか一応確認w

  • ストリームはステートメントが実行されるまで読み出されない
  • 8kB のバッファ経由で入力ストリームから PostgreSQL の接続上に流される

 8.1 ドライバはトンデモ実装ではないようだ。次の記事参照。

◆ メモリ消費を比較

比較のためプロファイラを使ってメモリ消費量を見てみよう。以下のグラフは PreparedStatement#setBinaryStream() を使って BYTEA 型カラムに 1~5MB のストリームを突っ込むコードを実行したグラフである。

PostgreSQL JDBC3 7.4 ドライバ
7.4 ドライバ
PostgreSQL JDBC3 8.1 ドライバ
8.1 ドライバ

結果は一目瞭然。7.4 ドライバの最大ヒープサイズが 300MB 近くまで拡張するのに対して、8.1 ドライバは 4.4MB から変化なし。使用メモリに関しても 7.4 が大量の確保と開放を繰り返すのに対して 8.1 はほとんど変化なし。実行時間は 7.4 はディスクスワップが入って 2分40秒もかかったのに対し 8.1 は 20 秒で処理を終えた。

しごくまっとうな結果である。ちなみに 20MB 程度まで実行してみたところ 7.4 ドライバはこのままどんどん膨れ上がって行くだけだった。

◇ 結論

Java から PostgreSQL に PreparedStatement#setBinaryStream() で大きなデータを送る場合は、7.x 用の JDBC ドライバは使ってはならない。

ただし 8.1 も次の記事を読め!!

コメント
GJ!
がんがれ
管理人のみ閲覧できます
このコメントは管理人のみ閲覧できます
トラックバック
トラックバック URL
コメントの投稿
管理者にだけ表示を許可する
Profile
Takami Torao
Takami Torao
C/C++ 使いだった 1996年、運命の Java と出会い現在に至る。のらアーキテクト。
Yah, this is image so I don't wanna eat spam, sorry!
Search

Google
MOYO Laboratory
Web

カテゴリー
最近の記事
最近のコメント
最近のトラックバック
月別アーカイブ
ブロとも申請フォーム
RSSフィード
リンク