セッション管理に向いているデータベースは MySQL ? Oracle ?

Catalyst-Plugin-Session-Store-DBIC とか検証してます。で前から気になってはいたのですが、Perl 界ではセッション管理するモジュールといえばほぼ全て MySQL が前提っぽい作りになってると思います。でも業務で使っているデータベースは Oracle でして、う〜ん・・・どうすっべかなぁ〜と思ってました。

で、試しに Oracle でセッション管理するためのテーブルをつくってみました。Oracle は VACHAR2 型とかは 4000 文字までしか扱えないので、セッションデータとして使うにはちょっと物足りないデータ型。CLOB 型を使えば解決できるんですが、DBI ベースにいろいろと指定してあげないとダメ。しかも多分遅い。遅いと推測していたから、今まで LOB や LONG 型はあえて使ってきませんでした。

session 格納に使うテーブルの構造はこんな感じで定義。あくまで試験用のテーブル構造。

-- oracle
CREATE TABLE sessions (
  id             varchar(72) NOT NULL,
  session_data   nclob,
  expires        integer,
  PRIMARY KEY (id)
);

-- mysql
CREATE TABLE sessions (
  id             varchar(72) NOT NULL,
  a_session      longblob,
  timestamp      timestamp,
  PRIMARY KEY (id)
);

いきなり結論。Oracle 10g でも遅くて使い物になりませんでした

insert で約40倍の遅さ。select で約6倍の遅さという結果になりました。やっぱ DBD::Oracle で LOB 型は使うとダメっぽいですね。

__追記(50K 程度の大きさの session データで再検証)__
insert で約30倍の遅さ。select で約9倍の遅さという結果になりました。ほぼ同じですね。

- スポンサーリンク -

 
ベンチマークスクリプトはこんな感じ。1万件のデータを insert した後に100回 select をするってやつ。セッションデータとして CGI->new をシリアライズしたものをぶち込む。実際には GET/POST のデータが存在しないので非常に小さなセッションデータとなります。

use MIME::Base64;
use Storable qw/nfreeze thaw/;
use DBI;
use DBD::Oracle qw(:ora_types);
use CGI;
my $oracle = DBI->connect('dbi:Oracle:testdb', 'test', 'test123');
my $mysql  = DBI->connect('dbi:mysql:testdb', 'test', 'test123');

$oracle->{LongReadLen} = 102400;
$oracle->{LongTruncOk} = 1;

&bench_insert($mysql, 'mysql');
&bench_insert($oracle,'');

&bench_select($mysql, {});
&bench_select($oracle, {ora_auto_lob => 0});

sub bench_select {
    my $dbh = shift;
    my $args= shift;
    my $loop = 100;
    $begin = time;

    foreach(1..$loop) {
        my $sth = $dbh->prepare("select * from sessions", $args);
        $sth->execute;
        while(my $row = $sth->fetchrow_hashref) { 1; }
        $sth->finish();
    }

    $end = time;
    print "TIME=".($end-$begin)."\n";
}

sub bench_insert {
    my $dbh = shift;
    my $args= shift;
    my $loop = 10000;
    $begin = time;

    my  $sql = ($args eq 'mysql') ?
                    'insert into sessions (id,a_session,timestamp) values (?,?,?)' : 
                    'insert into sessions (id,session_data,expires) values (?,?,?)';
    my $data = encode_base64(nfreeze(CGI->new));

    foreach(1..$loop) {
        my $sth = $dbh->prepare($sql);
        $sth->execute($_,$data,$_);
        $sth->finish();
    }

    $end = time;
    print "TIME=".($end-$begin)."\n";
}

で結果はこんな感じ。

TIME=2
TIME=80 (※メモリが潤沢にあるサーバだと =24 になった。うんうん。)
TIME=28
TIME=165 (※メモリが潤沢にあるサーバだと =311 になった。あれ?)

__追記(50K 程度の大きさの session データで再検証)__
TIME=3
TIME=87
TIME=42
TIME=365

あぁ・・・オラクル遅ぇ〜・・・こんなに差が開くとは思ってませんでした。。。insert がこんなに遅いんじゃぁ〜セッション管理に使うとサーバのレスポンスがすげぇ〜遅くなりそうな予感満点ですなぁ・・・

さて、最後に DBD::Oracle で LOB 型を使うための方法について説明を。ってか、「DBD::Oracle - DBIモジュールのためのOracleデータベース・ドライバ」をみればわかります。ついでなので必要部分を引用。

LOBをフェッチするとき、LONGと同じように扱い、そして$sth->{LongReadLen}と $sth->{LongTruncOk}に支配されます。OCI 7では、DBD::Oracleは返された列を組み立てる前に全ての量(LongReadLen)を先に占有していたことに注意して下さい。 OCI 8では、フェッチされた一番大きなLOBに必要な分までバッファを大きくするようになっています。

LOBを挿入または更新するとき、透過的にしている裏で、いくつかの大きな 魔法が行われなければなりません。それらが書き込みできるようになる前に、基本的にドライバは新しく挿入された'LOB Locator'を再びフェッチしなければいけません。しかしながら、これは機能し、私はそれをできる限り速くしています(挿入または更新の後の最初にサーバとの間をたった1回、余分に往復します)。さしあたって、1行だけのLOBの更新もサポートしています。またPL/SQLブロックへ複数LOBを渡すことは機能していません。

大きなLOBの挿入または更新をするには、前もってDBD::OracleがそれがLOB型であることを知っていなければなりません。そこで以下のように言わなければなりません:

$sth->bind_param($field_num, $lob_value, { ora_type => ORA_CLOB });
定数ORA_CLOBとORA_BLOBは以下のようにしてインポートされます:

use DBD::Oracle qw(:ora_types);
または単純に対応する整数値を使います(112 と 113)。

Oracle7とOracle8の両方でスクリプトを動かせるようにするためには、 Oracle7 DBD::Oracleはエラーなしに、LOB ora_typeをLOGとして扱います。そこで全てのコード、今もっているコードは以下のようになるかもしれません

$sth->bind_param($idx, $value, { ora_type => 8 });
この8(LONG型)をORA_CLOBやORA_BLOB(112や113)に変更することができます。

さらにもう一点:LOBの挿入、更新について、DBD::Oracleはパラメータが、どのテーブルのフィールドに関係しているかを伝えることが可能でなければなりません。それだけで解決できる場合であれば、それだけで機能します。しかしテーブルに同じ型のLOBフィールドが複数あるならば、各LOBパラメータがどのフィールドに関連するのかを知らせなければなりません:

$sth->bind_param($idx, $value, { ora_type=>ORA_CLOB, ora_field=>'foo' });
今のところ、DBD::Oracleを使って小さな塊でLOBを直接書き込む方法はありません。しかし、PL/SQLでDBMS_LOB.WRITEAPPENDを使って可能です(しかし効率的ではありません)。

LOBをINSERTするためには、UPDATE権限が必要です。

prepare()で/ora_auto_lobが0であれば、LOBロケータを取り出し、 Oracle::OCIを使ってあなた自身で全てのことを行うことが出来ます。


- スポンサーリンク -