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) |
. .: : : : : : : : :: :::: :: :: : :::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
. . : : : :: : : :: : ::: :: : :::: :: ::: ::: ::::::::::::::::::::::::::::::::::::::
. . .... ..: : :: :: ::: :::::: :::::::::::: : :::::::::::::::::::::::::::::::::::::::::::::
Λ_Λ . . . .: : : ::: : :: ::::::::: :::::::::::::::::::::::::::::
/:彡ミ゛ヽ;)ー、 . . .: : : :::::: :::::::::::::::::::::::::::::::::
/ :::/:: ヽ、ヽ、 ::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 |
問題の部分が ParameterList#setBytea() に置き換わっている。これをたどって行くと、入力ストリームおよび長さを単にパラメータとして保持しているだけのようである。
さらに、データベースと通信する部分まで踏み込んでみる。execute() からたどって、実際にストリームの内容が送信されるのは SimpleParameterList#streamBytea() の PGStream#SendStream() である。もうここまで来れば答えは出たも同然だが、最後の最後で御腐れコードになっていないか一応確認w
- ストリームはステートメントが実行されるまで読み出されない
- 8kB のバッファ経由で入力ストリームから PostgreSQL の接続上に流される
8.1 ドライバはトンデモ実装ではないようだ。次の記事参照。
◆ メモリ消費を比較
比較のためプロファイラを使ってメモリ消費量を見てみよう。以下のグラフは PreparedStatement#setBinaryStream() を使って BYTEA 型カラムに 1~5MB のストリームを突っ込むコードを実行したグラフである。
![]() 7.4 ドライバ |
![]() 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 も次の記事を読め!!
がんがれ