Oracle でセッション管理するならどうする?

以前、セッション管理に向いているデータベースは MySQL ? Oracle ? という記事で Oracle と MySQL のパフォーマンス差について書いたことがありますが、新年度の技術開発に向けてまた改めて検証をしています。世の中いろいろな事情で 商用 DB を使っている人は大勢いるでしょう。Oracle を使ってセッション管理をする必要だってあるかもしれない。

と言うわけで今回は、Oracle でセッション管理するならどうする?編です。比較対象として MySQL でのパフォーマンスも計測しています。Oracle とか MySQL は実行環境でパフォーマンスが結構異なるので、一応検証環境はこんなかんじ。

- スポンサーリンク -

検証マシンスペック

  • VMware 6.0.6000 上に構築した raw-disk タイプの仮想環境
  • Intel(R) Core(TM)2 CPU 6700 @ 2.66GHz
  • memory 1GB
  • CentOS 4.4 - 32bit
    Linux version 2.6.9-42.ELsmp (buildcentos@build-i386) (gcc version 3.4.6 20060404 (Red Hat 3.4.6-3)) #1 SMP Sat Aug 12 09:39:11 CDT 2006
  • hdparm -ft /dev/hda の結果 → raw-disk なので VM といえども結構高速
    /dev/hda: Timing buffered disk reads: 120 MB in 3.01 seconds = 39.86 MB/sec

データベース(MySQL / Oracle)の設定値

/etc/my.cnf の主要部分設定値。あまりチューニングしてない。
[mysqld]
port = 3306
socket = /tmp/mysql.sock
skip-locking
key_buffer = 16K
max_allowed_packet = 100M
table_cache = 4
sort_buffer_size = 64K
net_buffer_length = 2K
thread_stack = 64K

init.ora (oracle)の主要部分設定値。こちらもあまりチューニングしてない。

sga_target=277872640
log_archive_dest_1='LOCATION=/home/oracle/admin/archives'
log_archive_format=%t_%s_%r.dbf
open_cursors=300
db_block_size=8192
db_file_multiblock_read_count=16
undo_management=AUTO
undo_tablespace=UNDOTBS1
job_queue_processes=10
audit_file_dest=/home/oracle/admin/orcl/adump
remote_login_passwordfile=EXCLUSIVE
pga_aggregate_target=92274688
processes=150

session テーブル定義

MySQL 側の session テーブル
CREATE TABLE sessions (
  id             varchar(72) NOT NULL,
  data           longblob,
  timestamp      timestamp,
  PRIMARY KEY (id)
);

Oracle 側の session テーブル → varchar2 で何とか頑張る版と lob 使う版

-- LOB バージョン
CREATE TABLE sessions (
  id             varchar(72) NOT NULL,
  data           nclob,
  timestamp      timestamp,
  PRIMARY KEY (id)
)
LOB(data) STORE AS sessions_lob ( ENABLE STORAGE IN ROW );
alter table sessions nologging nocache;

-- VARCHAR2 バージョン
CREATE TABLE sessions2 (
  id             varchar(72) NOT NULL,
  subid          varchar(72) NOT NULL,
  data           varchar2(4000),
  timestamp      timestamp,
  PRIMARY KEY (id,subid)
);
alter table sessions2 nologging nocache;

ベンチマーク・スクリプトおよび前提条件とか目的とか。。。

  • insert は 1000 リクエスト分のセッション write に相当する処理パフォーマンスを計測。
  • select は 10000 リクエスト分のセッションの read に相当する処理パフォーマンスを計測。
  • データサイズによる違いを明確化するため 1KB, 4KB, 10KB, 100KB, 1MB の5パターンで計測してみた。
  • Oracle に限らず write するデータサイズが小さいほどパフォーマンスは良いはずなので、Compress::Zlib で事前にセッションデータを圧縮した場合も計測した。(※ Oracle の LOB 版み)
  • Apache::DBI 等のように永続接続を想定した計測方法なので、接続確立コストはベンチマーク対象外とした。(※普通にやると Oracle は破綻するのはわかっているので。)
  • Oracle で LOB を使わず varchar2 + 枝版で1セッションを分割して格納するとひょっとして高速?か検証する
  • スクリプトはお世辞にもきれいとは言えないけどあしからず・・・

use MIME::Base64;
use Storable qw/nfreeze thaw/;
use DBI;
use DBD::Oracle qw(:ora_types);
use CGI;
use Compress::Zlib ;
use Benchmark qw/cmpthese timethese/;
use constant LOOPMAX => 1000;

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

timethese(
    1,
    {
        'mysql::insert    1K' => '&_insert(1, 1000)',
        'mysql::select    1K' => '&_select(1, 1000)',
        'oraLOB::insert   1K' => '&_insert(2, 1000)',
        'oraLOB::select   1K' => '&_select(2, 1000)',
        'oralce::insert   1K' => '&_insert(3, 1000)',
        'oralce::select   1K' => '&_select(3, 1000)',
        'oraLOBgz::insert 1K' => '&_insert(4, 1000)',
        'oraLOBgz::select 1K' => '&_select(4, 1000)',

        'mysql::insert    4K' => '&_insert(1, 4000)',
        'mysql::select    4K' => '&_select(1, 4000)',
        'oraLOB::insert   4K' => '&_insert(2, 4000)',
        'oraLOB::select   4K' => '&_select(2, 4000)',
        'oralce::insert   4K' => '&_insert(3, 4000)',
        'oralce::select   4K' => '&_select(3, 4000)',
        'oraLOBgz::insert 4K' => '&_insert(4, 4000)',
        'oraLOBgz::select 4K' => '&_select(4, 4000)',

        'mysql::insert    10K' => '&_insert(1, 10000)',
        'mysql::select    10K' => '&_select(1, 10000)',
        'oraLOB::insert   10K' => '&_insert(2, 10000)',
        'oraLOB::select   10K' => '&_select(2, 10000)',
        'oralce::insert   10K' => '&_insert(3, 10000)',
        'oralce::select   10K' => '&_select(3, 10000)',
        'oraLOBgz::insert 10K' => '&_insert(4, 10000)',
        'oraLOBgz::select 10K' => '&_select(4, 10000)',

        'mysql::insert    100K' => '&_insert(1, 100000)',
        'mysql::select    100K' => '&_select(1, 100000)',
        'oraLOB::insert   100K' => '&_insert(2, 100000)',
        'oraLOB::select   100K' => '&_select(2, 100000)',
        'oralce::insert   100K' => '&_insert(3, 100000)',
        'oralce::select   100K' => '&_select(3, 100000)',
        'oraLOBgz::insert 100K' => '&_insert(4, 100000)',
        'oraLOBgz::select 100K' => '&_select(4, 100000)',

        'mysql::insert    1000K' => '&_insert(1, 1000000)',
        'mysql::select    1000K' => '&_select(1, 1000000)',
        'oraLOB::insert   1000K' => '&_insert(2, 1000000)',
        'oraLOB::select   1000K' => '&_select(2, 1000000)',
        'oralce::insert   1000K' => '&_insert(3, 1000000)',
        'oralce::select   1000K' => '&_select(3, 1000000)',
        'oraLOBgz::insert 1000K' => '&_insert(4, 1000000)',
        'oraLOBgz::select 1000K' => '&_select(4, 1000000)',
    }
);

sub _select {
    my $mode = shift;
    my $size = shift;
    my $loop = LOOPMAX * 10;

    my $dbh =
        ($mode == 1)
        ? DBI->connect( 'dbi:mysql:testdb',  'test', 'test' )
        : DBI->connect( 'dbi:Oracle:testdb', 'test', 'test' );
    my $args = { ora_auto_lob => 0 } if ( $mode == 2 || $mode == 4 );

    my $sql = 'select * from sessions where id = ?';
       $sql = 'select * from sessions2 where id = ?' if ( $mode == 3 );
    foreach ( 1 .. $loop ) {
        my $sth = $dbh->prepare( $sql, $args );
        $sth->execute( int(rand(LOOPMAX)+1) );
        ## ここちょっと手抜き。
        while ( my $row = $sth->fetchrow_hashref ) { 1; }
        $sth->finish();
    }
    $dbh->disconnect;
}

sub _insert {
    my $mode = shift;
    my $size = shift;
    my $loop  = LOOPMAX;

    my $dbh =
        ($mode == 1)
        ? DBI->connect( 'dbi:mysql:testdb',  'test', 'test' )
        : DBI->connect( 'dbi:Oracle:testdb', 'test', 'test' );
    my $args = { ora_auto_lob => 0 } if ( $mode == 2 || $mode == 4 );
    $dbh->do('truncate table sessions')  if ( $mode == 1 );
    $dbh->do('truncate table sessions')  if ( $mode == 2 || $mode == 4 );
    $dbh->do('truncate table sessions2') if ( $mode == 3 );

    my $sql = 'insert into sessions values (?,?,NOW())';
       $sql = 'insert into sessions values (?,?,sysdate)'    if ( $mode == 2 || $mode == 4 );
       $sql = 'insert into sessions2 values (?,?,?,sysdate)' if ( $mode == 3 );
    my $q   = CGI->new;
    my @randstr = map { $_ } ('a'..'z',0..9);
    my $text = '';
    $text .= $randstr[int(rand(scalar @randstr))] foreach(1..$size);
    $q->param( text => $text );
    my $data = encode_base64( nfreeze( $q ) );

    ## Compress::Zlib でデータ圧縮
    $data = Compress::Zlib::memGzip($data) if ( $mode == 4 );

    my (@datas) = $data =~ /(.{4000})/smg;

    if ( $mode == 3 ) {
        foreach my $i ( 1 .. $loop ) {
            my $j = 1;
            foreach (@datas) {
                my $sth = $dbh->prepare($sql);
                $sth->execute( $i, $j, $_ );
                $sth->finish();
                $j++;
            }
        }
    } else {
        foreach ( 1 .. $loop ) {
            my $sth = $dbh->prepare($sql);
            $sth->execute( $_, $data );
            $sth->finish();
        }
    }
    $dbh->disconnect;
}

ベンチマーク結果

いよいよお待ちかねのベンチマーク結果です。生ログを見せるよりもグラフを見た方がわかりやすいと思うのでまずそちらから。生ログは一番下にくっつけておきます。あ、縦軸が sec で横軸が byte です。

image001.gif

image003.gif

image002.gif

ベンチマーク結果からの考察

結果を考察すると、このベンチマーク環境下では次のことが言えると思います。
  1. MySQL の insert パフォーマンスは平均して Oracle の 6 - 10倍速い。
  2. データサイズが大きくなるに釣れて二時曲線的に速度は低下する。
  3. MySQL の select パフォーマンスは Oracle の 2 倍程度速い。逆に言えば Oracle も Select は高速。
  4. select のパフォーマンスはデータサイズにそれほど依存しない(とは言え100MBのデータなら話は別と思われ)
  5. Oracle の場合はセッションデータを圧縮して格納する方が圧縮コストを払ってでも高速になる。
  6. Oracle の LOB は遅くて有名だが varchar2 の最大値 4K に分割格納の方がもっと遅い。

結論、Oracle でセッション管理するなら LOB を使うが、DB write する前にセッションデータを圧縮。
DB read した後にセッションデータを展開する方法が最適と思われる。

これは、DB のマスターは Oracle 10g RAC を使っても実際にはそれほどリニアにパフォーマンス向上しないが、App サーバは台数分だけリニアに性能が向上するので圧縮・展開コストなんて実はたいした話じゃないよって意味が含まれています。

とは言え圧倒的なアクセスの前では DB の処理能力が追いつかないことは確かなので、memcached の導入が許されるのであれば、Oracle RAC + memcached でオラクルでも快適なセッション管理を!の方式を検討するのがやはりよいと感じました。

あっ、あと納得いかないのですが、nologgin しているテーブルのはずなのにアーカイブログがじゃんじゃん生成されます。Oracle を noarchive モードで運用している人なんていないと思うので archive モードでパフォーマンスを計測したわけですが、これアーカイブログの出力とかでも結構性能劣化してそうです。

__追記__
nologging モードにしても、ダイレクト書き込み処理以外では普通に REDO ログが生成されるのでアーカイブログも普通に生成されることを思い出しました。う〜んどうしようかなぁ〜

最後におまけ。ベンチマーク結果の生ログはこんなかんじ。

mysql::insert    1000K: 136 wallclock secs ( 3.65 usr + 39.83 sys = 43.48 CPU) @  0.02/s (n=1)
mysql::insert    100K: 12 wallclock secs ( 1.56 usr +  0.57 sys =  2.13 CPU) @  0.47/s (n=1)
mysql::insert    10K:  1 wallclock secs ( 0.07 usr +  0.31 sys =  0.38 CPU) @  2.63/s (n=1)
mysql::insert    4K:  1 wallclock secs ( 0.02 usr +  0.18 sys =  0.20 CPU) @  5.00/s (n=1)
mysql::insert    1K:  1 wallclock secs ( 0.00 usr +  0.18 sys =  0.18 CPU) @  5.56/s (n=1)
mysql::select    1000K:  7 wallclock secs ( 0.33 usr +  1.92 sys =  2.25 CPU) @  0.44/s (n=1)
mysql::select    100K:  7 wallclock secs ( 0.20 usr +  1.74 sys =  1.94 CPU) @  0.52/s (n=1)
mysql::select    10K:  7 wallclock secs ( 0.24 usr +  1.81 sys =  2.05 CPU) @  0.49/s (n=1)
mysql::select    4K:  6 wallclock secs ( 0.24 usr +  1.80 sys =  2.04 CPU) @  0.49/s (n=1)
mysql::select    1K:  6 wallclock secs ( 0.30 usr +  1.93 sys =  2.23 CPU) @  0.45/s (n=1)
oraLOB::insert   1000K: 796 wallclock secs ( 9.29 usr + 24.37 sys = 33.66 CPU) @  0.03/s (n=1)
oraLOB::insert   100K: 126 wallclock secs ( 0.78 usr +  2.48 sys =  3.26 CPU) @  0.31/s (n=1)
oraLOB::insert   10K: 21 wallclock secs ( 0.17 usr +  0.52 sys =  0.69 CPU) @  1.45/s (n=1)
oraLOB::insert   4K: 17 wallclock secs ( 0.10 usr +  0.35 sys =  0.45 CPU) @  2.22/s (n=1)
oraLOB::insert   1K:  3 wallclock secs ( 0.06 usr +  0.28 sys =  0.34 CPU) @  2.94/s (n=1)
oraLOB::select   1000K: 13 wallclock secs ( 0.49 usr +  4.44 sys =  4.93 CPU) @  0.20/s (n=1)
oraLOB::select   100K: 13 wallclock secs ( 0.49 usr +  4.68 sys =  5.17 CPU) @  0.19/s (n=1)
oraLOB::select   10K: 13 wallclock secs ( 0.96 usr +  5.06 sys =  6.02 CPU) @  0.17/s (n=1)
oraLOB::select   4K: 13 wallclock secs ( 0.43 usr +  4.89 sys =  5.32 CPU) @  0.19/s (n=1)
oraLOB::select   1K: 13 wallclock secs ( 0.61 usr +  4.55 sys =  5.16 CPU) @  0.19/s (n=1)
oraLOBgz::insert 1000K: 434 wallclock secs ( 6.07 usr + 11.96 sys = 18.03 CPU) @  0.06/s (n=1)
oraLOBgz::insert 100K: 77 wallclock secs ( 0.39 usr +  2.28 sys =  2.67 CPU) @  0.37/s (n=1)
oraLOBgz::insert 10K:  8 wallclock secs ( 0.11 usr +  0.40 sys =  0.51 CPU) @  1.96/s (n=1)
oraLOBgz::insert 4K:  7 wallclock secs ( 0.06 usr +  0.35 sys =  0.41 CPU) @  2.44/s (n=1)
oraLOBgz::insert 1K:  3 wallclock secs ( 0.04 usr +  0.23 sys =  0.27 CPU) @  3.70/s (n=1)
oraLOBgz::select 1000K:  14 wallclock secs ( 0.26 usr +  2.37 sys =  2.63 CPU) @  0.38/s (n=1)
oraLOBgz::select 100K:  12 wallclock secs ( 0.37 usr +  2.21 sys =  2.58 CPU) @  0.39/s (n=1)
oraLOBgz::select 10K:  12 wallclock secs ( 0.25 usr +  2.22 sys =  2.47 CPU) @  0.40/s (n=1)
oraLOBgz::select 4K:  12 wallclock secs ( 0.31 usr +  2.29 sys =  2.60 CPU) @  0.38/s (n=1)
oraLOBgz::select 1K:  7 wallclock secs ( 0.32 usr +  2.38 sys =  2.70 CPU) @  0.37/s (n=1)
oralce::insert   1000K: 1280 wallclock secs (30.51 usr + 109.62 sys = 140.13 CPU) @  0.01/s (n=1)
oralce::insert   100K: 135 wallclock secs ( 4.53 usr + 12.43 sys = 16.96 CPU) @  0.06/s (n=1)
oralce::insert   10K: 16 wallclock secs ( 0.27 usr +  1.07 sys =  1.34 CPU) @  0.75/s (n=1)
oralce::insert   4K:  3 wallclock secs ( 0.11 usr +  0.29 sys =  0.40 CPU) @  2.50/s (n=1)
oralce::insert   1K:  1 wallclock secs ( 0.00 usr +  0.00 sys =  0.00 CPU)
oralce::select   1000K: 20 wallclock secs ( 2.13 usr +  5.86 sys =  7.99 CPU) @  0.13/s (n=1)
oralce::select   100K: 15 wallclock secs ( 1.22 usr +  4.75 sys =  5.97 CPU) @  0.17/s (n=1)
oralce::select   10K: 15 wallclock secs ( 1.22 usr +  4.72 sys =  5.94 CPU) @  0.17/s (n=1)
oralce::select   4K: 14 wallclock secs ( 1.13 usr +  4.62 sys =  5.75 CPU) @  0.17/s (n=1)
oralce::select   1K: 15 wallclock secs ( 1.41 usr +  4.96 sys =  6.37 CPU) @  0.16/s (n=1)
- スポンサーリンク -