Tokyo TyrantによるHAなセッションストレージ 2 PHPから利用する篇

PHPからセッションストレージとしてTokyo Tyrant (TT)を使用する場合、PHP用のTTクライアントライブラリが必要になるが、悲しい事に公式なライブラリはない。
幸い何種類かPHP用のライブラリが公開されているので、その中のどれかを使わせてもらおう。

ざっと調べたところ上の3種類が見つかった。他にもあるらしいけど見つけられなかった。

tokyotyrant_phpは作者の方が実験レベルと言っている事から採用を見合わせ、Pure PHPのNet_TokyoTyrantPECLphp-tokyo_tyrantを調べてみた。どちらも現在ベータ版。
PHPのNet_TokyoTyrantの方が取り扱いは楽だしモテるらしいけど、PECLphp-tokyo_tyrantの方がパフォーマンスが高いとの噂。しかも

session.save_handler = tokyo_tyrant

とすれば自前でセッションハンドラを書かなくても良いというおまけ付き。
これは助かると採用方向に傾いていたところで問題発見。
どうやらphp-tokyo_tyrant (0.1.0)はバイナリセーフではなく、データ内に\0が含まれているとそこでデータが終了してしまう。
PHPはオブジェクトをserializeすると結果に\0が含まれる事があるのでこれは困る。

Net_TokyoTyrantではこの問題は発生しないため、今回はNet_TokyoTyrantを採用する事にした。現在のリリース0.2.1-betaにはテーブルデータベース用のNet_TokyoTyrant_Tableが含まれていないので、trunkのr1158から取得している。

PHPセッションハンドラ

PHPのセッションハンドラはこんな感じ。

<?php
require_once 'Net/TokyoTyrant/Table.php';

class Net_TokyoTyrant_Table_Session extends Net_TokyoTyrant_Table
{
    public function __destruct()
    {
        session_write_close();
    }
}

class TokyoTyrant_Session
{
    const HOST = 'localhost';
    const PORT = 1978;
    const TIMEOUT = 5;
    const EXPIRE = 1440;

    const DATA_COLUMN_NAME = 'd';
    const EXPIRE_COLUMN_NAME = 'x';

    private static $tt = null;

    public static function connect()
    {
        $ttt = new Net_TokyoTyrant_Table_Session();
        $ttt->connect(self::HOST, self::PORT, self::TIMEOUT);
        $ttt->setTimeout(self::TIMEOUT);
        self::$tt = $ttt;
        return true;
    }
    public static function close()
    {
        if (self::$tt !== null) {
            self::$tt->close();
        }
        return true;
    }
    public static function put($key, $value)
    {
        if (self::$tt !== null) {
            return self::$tt->put($key, array(
                self::DATA_COLUMN_NAME => $value,
                self::EXPIRE_COLUMN_NAME => time() + self::EXPIRE
            ));
        }
        return false;
    }
    public static function get($key)
    {
        if (self::$tt !== null) {
            $result = self::$tt->get($key);
            if (is_array($result) && isset($result[self::DATA_COLUMN_NAME])) {
                return $result[self::DATA_COLUMN_NAME];
            }
        }
        return '';
    }
    public static function out($key)
    {
        if (self::$tt !== null) {
            return self::$tt->out($key);
        }
        return false;
    }
}

// {{{ Handler
function open($save_path, $session_name)
{
    return TokyoTyrant_Session::connect();
}
function close()
{
    return TokyoTyrant_Session::close();
}
function read($sess_id)
{
    return TokyoTyrant_Session::get($sess_id);
}
function write($sess_id, $sess_data)
{
    return TokyoTyrant_Session::put($sess_id, $sess_data);
}
function destroy($sess_id)
{
    return TokyoTyrant_Session::out($sess_id);
}
function gc($maxlifetime)
{
    return true;
}
session_set_save_handler('open', 'close', 'read', 'write', 'destroy', 'gc');
// }}}

PHP 5.0.5 以降、write ハンドラおよび close ハンドラはオブジェクトが破棄されたあとにコールされます。 そのため、セッション内でデストラクタを使用可能ですが、 ハンドラ内ではオブジェクトを使用できません。

PHP: session_set_save_handler - Manual

というわけなので、Net_TokyoTyrant_Tableを継承したクラスのデストラクタでsession_write_close()を呼び出すようにしている。

期限切れレコードの削除

TTで有効期限が切れたレコードを削除する方法はドキュメントに解説があるのでそのまま使わせていただく。

function expire()
   local args = {}
   local cdate = string.format("%d", _time())
   table.insert(args, "addcond\0x\0NUMLE\0" .. cdate)
   table.insert(args, "out")
   local res = _misc("search", args)
   if not res then
      _log("expiration was failed")
   end
end

この内容をttexpire.luaとして保存。

ttserver -dmn -port 1978 -pid pid -log log -ext ttexpire.lua -extpc expire 1 casket.tct#bnum=10000#idx=x:dec#dfunit=8

ttexpire.luaと定期的に呼び出す関数expireを指定してTTを起動すると、この例の場合1秒毎に期限切れレコードの削除処理が行われる。
なお、-dmn付きで起動する場合はファイルのパスを絶対パスで書いた方が良いみたいだ。

これでPHPのセッションをTTに保存できるようになった。

ベンチマーク

ここまでできたところで、主に同時接続数が増加した場合のスループットの変化を見るためベンチマークを取ってみた。
レコード数が増加した場合のベンチマークも取ってみたものの、どちらもほぼ一定のスループットを維持していたので省略。

サーバ側の環境は以下の通り。


事前に100000セッション分のレコードを作成し、作成された中のひとつのセッションIDを設定したリクエストを10000件、同時接続数を変えてabから発行した際の平均秒間リクエスト数を計測した。
セッションデータのサイズは概ね80バイト。

ApacheとTT又はMySQLが同居したサーバ(古いラップトップ)に対し、クライアント(MacBook)からリクエストを発行している。

MySQLを使用するハンドラの内容はPHPのセッション管理に使う箱選び 2とほぼ同じで、SET NAMESの部分をmysql_set_charset()に直している。
GCの起動確率は1/1000。

Apacheはpreforkで550プロセス起動した。

StartServers     550
MinSpareServers  550
MaxSpareServers  550
ServerLimit      600 
MaxClients       600

MySQLはこのような設定で起動した。

skip-locking
key_buffer = 64M
max_allowed_packet = 1M
table_cache = 64
sort_buffer_size = 512K
net_buffer_length = 8K
read_buffer_size = 1M
read_rnd_buffer_size = 512K
myisam_sort_buffer_size = 1M

max_connections = 500

innodb_data_file_path = ibdata1:50M:autoextend
innodb_buffer_pool_size = 256M
innodb_additional_mem_pool_size = 8M
innodb_log_file_size = 32M
innodb_log_buffer_size = 8M
innodb_flush_log_at_trx_commit = 1
innodb_lock_wait_timeout = 50

結果

同時接続数が10程度の場合はTTとMySQLの差はほとんどないものの、同時接続数の増加に伴いMySQLスループットだけが大幅に落ち込んでいる。
ただし、今回の方法では特定の1レコードのみを頻繁に更新する処理になっているので、実際の環境ではここまで極端な落ち込み方はしないと思う。とはいえTTの安定性が光る結果に。まさに多い日も安心。

追記
「思う」で終わらせてしまうのは良くないので、10000件の新規セッションを作る場合についても計測してみた。

落ち込みの幅は小さくなったものの、傾向は同じだった。
追記おわり


数値については環境が貧弱ゥな点を考慮すればなかなか良いのではないかと思う。

次はTTをデュアルマスタ化した状態でセッションストレージとして使う方法について考えたい。

つづく

  1. 検討篇
  2. PHPから利用する篇
  3. デュアルマスタ篇