PersistentPerlのベンチマーク

ようやくPersistentPerl(SpeedyCGI)が素直に動くようになったので、軽く計測してみました。

  • Core Duo T2300 1.66GHz
  • CentOS 5.2 on VMware Server 1.0.7 with Windows XP Pro SP3
  • Apacheはprefork設定、ほぼデフォルト
  • Perl-DBIMySQLに接続し、SELECT文を1個投げて結果を返すCGI
  • SQLは約6,000件のテーブルから主キー検索で1件だけ取り出すもの
  • abコマンドを用い、多重度1で数千回試行して平均のレスポンスタイムを出す

VMwareな時点でちょっとアレですが、非常に分かりやすい結果が出ています。

(1) Perl/CGI
    Requests per second:    8.81 [#/sec] (mean)
    Time per request:       113.477 [ms] (mean)

(2) PersistentPerl/CGI
    Requests per second:    54.98 [#/sec] (mean)
    Time per request:       18.189 [ms] (mean)

(3) mod_persistentperl
    Requests per second:    229.24 [#/sec] (mean)
    Time per request:       4.362 [ms] (mean)

(4) (3)+thread_cache_size=10+query_cache_size=16M
    Requests per second:    252.09 [#/sec] (mean)
    Time per request:       3.967 [ms] (mean)

(5) (3)+持続的接続+query_cache_size=16M
    Requests per second:    367.53 [#/sec] (mean)
    Time per request:       2.721 [ms] (mean)

PersistentPerlをCGIとして使うだけでも、Perl/CGIに比べて性能が6倍になります。この差はLAN内であれば体感でもはっきりわかります。ちなみにPerl-DBIを使わない単純なプログラムで試したところ3倍の差となっていたので、DBIまわりの初期化処理が差を広げる要因になっていると考えられます。
個人利用であればPersistentPerl/CGIで十分な気がしますが、(3)でmod_persistentperlを用いると性能がさらに4倍に跳ね上がります。PersistentPerl/CGIはバックエンドで解析済みのPerlプログラムを繰り返し実行することで性能を稼いでいます。しかしそれをキックするためのフロントエンドプロセスが、Perl本体より軽いとはいえリクエストの度に起動されることになります。mod_persistentperlはフロントエンドプロセスの処理を肩代わりすることで新規プロセスの起動を不要とし、高速化を実現しています。14msの差がプロセス生成コストと考えられます。
(4)はMySQL側のチューニングです。(1)(2)(3)の項目はいずれもリクエストの度にMySQLへの新規接続が発生しますが、(4)ではスレッドキャッシュの指定によってMySQL側でのサーバスレッド生成を最初の一回に抑えています。設定値はPersistentPerlのMaxBackendsに合わせています。また、クエリキャッシュを設定することで同じクエリに対し素早く結果を返すようにしています。いずれもきちんとキャッシュにヒットしていることを確認済みです。
(4)についてですが、性能向上に寄与したのは主にクエリキャッシュの方で、スレッドキャッシュの有無はほとんど性能に関係ありませんでした。元々Kernel2.6ではスレッド生成コストが無視できるほど小さいのか、認証まわりがスレッド生成より遥かに重い処理なのか、VMwareがおかしいのかは分かりませんが、興味深い結果です。
今回の到達点が(5)です。最初のPerl/CGIに比べると実に40倍以上の性能になっています。これはPersistentPerlにおいてリクエスト間でグローバル変数が保持されることを利用し、DBIのデータベースハンドラをグローバル変数に格納することで、リクエストごとのデータベース接続を不要にしたものです。おそらく認証処理がなくなったことが大きいと思いますが、スレッドキャッシュより明らかに良い数値が出ていることが分かります。
良いサンプルが見つからなかったのであまり自信がないのですが、持続的接続は以下のように実装しています。

# PersistentPerlオブジェクトとデータベースハンドラをグローバル変数として宣言
our ($pp, $dbh);

(略)

# データベースへの接続処理
# データベースハンドラが未定義、あるいは接続が正常でない場合のみ実行
if ((!defined($dbh)) or (!$dbh->ping())) {
    # 接続する
    $dbh = DBI->connect($dsn, $dbuser, $dbpass,
           { RaiseError => 1, PrintError => 0, AutoCommit => 0 });

    # 初回のみ、PersistentPerlオブジェクトの初期化を行う
    if (!defined($pp)) {
        $pp = PersistentPerl->new();

        # バックエンドプロセスが終了するときの処理を定義
        # ※ バックエンドプロセスは指定回数のリクエストをこなすか
        #    一定時間リクエストがないと、自動的に終了します
        $pp->add_shutdown_handler(sub {
            if (defined($dbh)) {
                eval {
                    $dbh->rollback();
                };
                eval {
                    $dbh->disconnect();
                };
            }
        });
    }
}

実際のWebアプリケーションでは、少し複雑な処理を書いたりSQLに伴ってディスクI/Oが発生したりすると簡単に10ms単位の時間がかかってしまいます。ですからこんなことをして1ms削って意味があるのかと言われると厳しいところで、MySQLではあまり恩恵がないでしょう。ただし、Oracleなど接続処理のコストが高いRDBMSでは必須になると思います。OraclePerlでWebアプリを作るかどうかは別として(^^;
mod_persistentperlのビルドに際してはid:dayflowerさんのパッチを利用させていただきました。ありがとうございました。(http://d.hatena.ne.jp/dayflower/20061205/1165309213http://d.hatena.ne.jp/dayflower/20070216/1171620558)