Tokyo TyrantによるHAなセッションストレージ 2 PHPから利用する篇
PHPからセッションストレージとしてTokyo Tyrant (TT)を使用する場合、PHP用のTTクライアントライブラリが必要になるが、悲しい事に公式なライブラリはない。
幸い何種類かPHP用のライブラリが公開されているので、その中のどれかを使わせてもらおう。
ざっと調べたところ上の3種類が見つかった。他にもあるらしいけど見つけられなかった。
tokyotyrant_phpは作者の方が実験レベルと言っている事から採用を見合わせ、Pure PHPのNet_TokyoTyrantとPECLのphp-tokyo_tyrantを調べてみた。どちらも現在ベータ版。
PHPのNet_TokyoTyrantの方が取り扱いは楽だしモテるらしいけど、PECLのphp-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に保存できるようになった。
ベンチマーク
ここまでできたところで、主に同時接続数が増加した場合のスループットの変化を見るためベンチマークを取ってみた。
レコード数が増加した場合のベンチマークも取ってみたものの、どちらもほぼ一定のスループットを維持していたので省略。
サーバ側の環境は以下の通り。
- ハードウェア
- OS
- ソフトウェア
- Tokyo Tyrant 1.1.33
- Tokyo Cabinet 1.4.32
- MySQL 5.0.45-7.el5
- Apache 2.2.3-22.el5.centos.2
- PHP 5.2.10
- APC 3.0.19
- Net_TokyoTyrant r1158
事前に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をデュアルマスタ化した状態でセッションストレージとして使う方法について考えたい。
つづく