Perl の iThread 使って負荷試験ツールを書いてみた

えーっと、最近のお仕事でデータベース関連やってまして、DBMS としての性能試験みたいなのを実施したくて(しなくてはいけなくって・・・)、Perl で作ってみました。

いや、Apache bench とか使ってもいいんですが、httpd のメモリ使用量とかも馬鹿にならないので、Perl の ithread 使って書いてみることにしました。
→Perl の ithread 使うとメモリ一杯使うので、結果的に同じだったけど・・・(苦笑

プログラムの構造は、producer & consumer モデルってやつの応用です。具体的にはこんな感じ。

- スポンサーリンク -

load01.jpg

で、負荷スクリプトを走らせている間にデータベースサーバの負荷を vmstat なり、sar なりで計測すればOKです。キューに投入するリクエスト数(producer スレッドの enqueue 数)や処理スレッド数(consumer スレッドの数)を増減すれば、DBサーバの処理限界値がわかるという寸法。もっとも実験してみたら、Web サーバ側も ithread 一生懸命作ったりする都合上、スレッド数が多くなってくると Web サーバが swap したりするので注意が必要。

あと、この負荷スクリプトがそれぞれの処理を何秒で完了したか出力するので、それをグラフ化しても良い感じ。
例えば、ある例のSQLを構築中のDBMSで計測すると以下の感じ。高負荷時でも 0.035 秒で処理可能ってのがわかります。

load02.jpg


以下負荷計測スクリプトです。汎用的に作っていないので、利用したい方は適当に改変して下さい。
ちょっと改造すれば、並列で処理するクローラーとかにも応用できると思います。

#!/usr/bin/perl -w

package LoadTest;
use strict;
use warnings;
use DBI;
use Time::HiRes;
use threads;
use threads::shared;
use Thread::Queue;
use Readonly;
Readonly my $LOOP        => 10;     ## 検証回数
Readonly my $CONCURRENCY => 100;    ## タスク実行の並列度 (= processes)
Readonly my $REQ_PERSEC  => 100;    ## リクエスト数/sec (= MaxClients)
Readonly my $SLEEP_TIME  => 1;      ## リクエストを何秒ごとに生成するか
Readonly my $DATASOURCE  => ['dbi:Oracle:testdb', 'testid', 'testpw'];
Readonly my $TESTSQL     => 'select * from userlog where date=trunc(sysdate)';
Readonly my $DEBUG       => 0;


###--------------------------------------------------------------------------###
## 初期設定
###--------------------------------------------------------------------------###
my $alerm   : shared = 0;             ## 終了の合図
my %thrDone : shared = ();
my $taskid  : shared = 0;

my $queue = new Thread::Queue;
my @consumers;
for(1..$CONCURRENCY) {
    push @consumers, threads->new(\&consumerQueue, $queue, $_);
}
threads->new(\&producerQueue, $queue, $REQ_PERSEC, $LOOP);
do { 1 } while(!$alerm);
map { $_->join; } @consumers;
exit;

###--------------------------------------------------------------------------###
# 検証回数分、リクエストをキューに投入する(=仮想的なdb-requestの生成)
###--------------------------------------------------------------------------###
sub producerQueue {
    my $queue       = shift;
    my $req_persec  = shift;
    my $loop        = shift;
    my $processed   = 0;
    my $time0       = Time::HiRes::time();

    ## 指定の検証回数分、毎秒指定のリクエストを生成する
    while($loop >= 0) {
        $queue->enqueue(reverse(1..$req_persec));
        $loop--;
        $processed += $req_persec;
        my $time1 = Time::HiRes::time();
        print "enqueue,time,".substr($time1-$time0,0,5),",process,$processed\n" if($DEBUG);
        sleep $SLEEP_TIME;
    }

    ## プロセス生成の終了
    $alerm = 1;
    print "=> enqueue end:$alerm\n" if($DEBUG);
}


###--------------------------------------------------------------------------###
# リクエストの処理(=仮想的なdb-processによる処理)
###--------------------------------------------------------------------------###
sub consumerQueue {
    my $queue       = shift;
    my $consumerid  = shift;
    my $processid   = 0;

    ## プロセス生成スレッドが生きている間は処理続行
    my %threads;
    while(!$alerm) {
        ## ひたすら処理を続ける
        while($queue->pending>0) {
            $processid = $queue->dequeue or last;
            print "-> dequeue:$alerm:$consumerid:$taskid:$processid\n" if($DEBUG);
            &doTask($taskid++, $consumerid);
            threads->yield();
        }
    }
}


###--------------------------------------------------------------------------###
# タスクを実行する
###--------------------------------------------------------------------------###
sub doTask {
    my $taskid      = shift;
    my $consumerid  = shift;
    my $time0       = Time::HiRes::time();

    ## DB 接続してSQL実行
    eval {
        my $dbh = DBI->connect(@$DATASOURCE, {PrintError => 1, RaiseError => 1, AutoCommit => 1,});
        my $sth = $dbh->prepare($TESTSQL);
        $sth->execute;
#       print DBI::dump_results($sth);
        $sth->finish;
        $dbh->disconnect;
    };
    ## DBI ERROR が発生していたら、不要な部分を削除してエラー用のメッセージを作成
    die "$@" if($@);

    my $time1= Time::HiRes::time();
    print "taskid,$taskid,time,".substr($time1-$time0,0,5).",consumerid,$consumerid\n";
}

あぁ、そうそう、Apache::DBI 的な計測にしたい場合は、DBI の生成場所を consumerQueue 内で作って、toDask に引数で渡すって書き方に変更が必要です。

- スポンサーリンク -