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 です。
ベンチマーク結果からの考察
結果を考察すると、このベンチマーク環境下では次のことが言えると思います。- MySQL の insert パフォーマンスは平均して Oracle の 6 - 10倍速い。
- データサイズが大きくなるに釣れて二時曲線的に速度は低下する。
- MySQL の select パフォーマンスは Oracle の 2 倍程度速い。逆に言えば Oracle も Select は高速。
- select のパフォーマンスはデータサイズにそれほど依存しない(とは言え100MBのデータなら話は別と思われ)
- Oracle の場合はセッションデータを圧縮して格納する方が圧縮コストを払ってでも高速になる。
- 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)
コメントやシェアをお願いします!