まだ重たいCMSをお使いですか?
毎秒1000リクエスト を捌く超高速CMS「adiary

2008/03/13(木)adiaryの記法入力支援ツール

最低人間さんが作られたようです。ひとぅさんもだけど*1、みなさん早っ。

色の指定などに関しては adiary 側の色記法対応待ちでしょうか。

colorタグでも作りましょうか。

color = 色指定, ASCII, 2, <span style="color: $1">#2</span>
red   = 赤, ASCII, 1, <span style="color: red">#1</span>
blue  = 青, ASCII, 1, <span style="color: blue">#1</span>
green = 緑, ASCII, 1, <span style="color: green">#1</span>
yellow= 黄, ASCII, 1, <span style="color: yellow">#1</span>

コメント欄(ひとぅさん)からですが。

adiary2に取り込まれ、ユーザーごとにON/OFFが選べるようになるとベストですね。

とりあえずライセンスを教えてくださ(略)

*1 : ここに書くのもなんですが、オフィシャルからリンクしました。

2008/02/03(日)SpeedyCGI/FastCGI でモジュール動的更新

Satsuki-system(adiaryの基礎)で使用している、モジュールの自動更新機構と SpeedyCGI/FastCGI での実装について詳細に解説します。

モジュール自動更新の必要性

SpeedyCGIやmod_perl等々のスクリプトキャッシュ環境で実行速度が改善するのは、プログラム本体やライブラリをコンパイルした状態で内部に保持するためスクリプト実行時間の大半を占めるコンパイル時間が短縮されるというメリットがあるためです。特に最近のスクリプトは便利なライブラリを使用する傾向が強く、実行時間の95%以上がスクリプトのコンパイル時間なんてこともめずらしくありません。*1

しかしそのような環境では、スクリプトライブラリを書き換えてもプログラムは依然としてキャッシュした古いライブラリを使用し続けるので、少し書き換える度に SpeedyCGI の常駐プロセスを手動で殺したり、Apacheを再起動させたりといった手間が必要になります。

これを避けるためにはスクリプト自身でライブラリの更新をチェックし、ライブラリが更新されていた際に自動的にスクリプトを再起動(リロード)させる仕組みが必要になります。

*1 : adiaryはPerlの付属ライブラリを極力使用せず、車輪を再発明しまくることによって実行速度を向上させています。

ライブラリ更新チェック方法

Satsuki-system(adiary) では lib/Satsuki/Base_auto2.pm にその仕組みが実装されています(むしろそのファイルは、それ関連しか書かれてません)。

# AGPLv3
#######################################################
# ■SpeedyCGI / FastCGI用のリロードチェックサブルーチン
#######################################################
my %lib_modtime;
#------------------------------------------------------
# ●新たにロードされたライブラリの更新時間を保存
#------------------------------------------------------
sub save_lib_modtime {
	while (my ($pkg, $file) = each(%INC)) {
		if (exists $lib_modtime{$file}) { next; }
		$lib_modtime{$file} = (stat($file)) [9];
	}
}
#------------------------------------------------------
# ●ライブラリが更新されているか確認
#------------------------------------------------------
sub check_lib_update {
	while(my ($file, $current_tm) = each(%lib_modtime)) {
		my $tm = (stat($file)) [9];
		if ($tm > $current_tm) {
			return 1;	# 更新されている
		}
	}
	return 0;
}

%lib_modtime がライブラリの更新時刻を保持するハッシュです。save_lib_modtime をプログラムの終了時に呼び出し、新たにロードされたライブラリを %lib_modtime に保存しています。そしてプログラム開始時に check_lib_update を呼び出して、更新されたライブラリがあるか確認しています。

もし更新されたライブラリが発見された場合は、この実行終了後に(キャッシュされた)プロセスを終了する予約をして、リクエストされたURIにリダイレクトします。*2

#SpeedyCGI
if ($self->check_lib_update()) {
	CGI::SpeedyCGI->shutdown_next_time();	# リクエスト終了後シャットダウン
	if ($ENV{REQUEST_METHOD} ne 'POST') {	# POSTはリダイレクトできないのでそのまま処理
		$self->redirect($ENV{REQUEST_URI});	# 同一URLにリダイレクト
	}
}

FastCGIでの処理も同様です。mod_perlの場合はApache自体を reload させるわけにはいかないので、別の方法になっています。

*2 : 但しPOSTメソッド時を除く

なぜか更新が検出できない

SpeedyCGI のバグなのか仕様なのか、Base.pm の更新が検出できない(リロードされない)というバグに長いこと悩まされていました。SpeedyCGI は何分かアクセスがなければ自動でプロセスが落ちるためさほど深刻ではありませんが、奇妙なので少し調査してみました。

SpeedyCGIの起動ルーチンは次のようになっています。

#!/usr/bin/speedy
use Satsuki::Base();
{
	# ルートオブジェクト生成
	my $ROBJ = Satsuki::Base->new();
	# SpeedyCGI向けリロードチェック
	$ROBJ->init_for_speedycgi('Satsuki::');
	# メイン
	$ROBJ->start_up();
}

色々調べてみると、CGI::SpeedyCGI->shutdown_next_time(); 自体の処理は問題なく、SpeedyCGI自体はリロードがかかっているにも関わらず、Satsuki::Base がリロードされないということが判明しました。どうやらshutdown_next_time() は本当にリロードしているわけではなく、use段階は再実行しない(perlのBEGIN構文)ようです*3

試行錯誤の末、use Satsuki::Base(); の部分を次のように書き換えることで解決されました。

if (! $Satsuki::Base::VERSION) {
	delete $INC{"Satsuki/Base.pm"};
	require Satsuki::Base;
}

ここに delete が付いている理由ですが、Base.pm のロードの途中に構文エラーなどで失敗した場合、requireしたライブラリにロード済の情報が残ったまま($INC{"Satsuki/Base.pm"}が保存されたまま)の状態でキャッシュしてしまいます。この状態ではライブラリのリロードに問題があるため、ロード情報を消してから require します。

*3 : 注:cgiファイルを書き換えた場合は、useも含めて再実行されます。

2007/05/14(月)table記法 の summary, caption

table記法による summary, caption の指定

テーブルに summary や caption を指定できるようになりました。

|caption=表1 [wiki:RAID] 0,1の違い
|summary=RAIDについて
|*RAID |*名称|*容量|*write速度|*read速度|*故障率|
|*RAID0|ストライピング|そのまま|倍      |倍|およそ倍|
|*RAID1|ミラーリング  |半分    |そのまま|倍|半分|

と入力すると次のようになります。

表1 RAID 0,1の違い
RAID名称容量write速度read速度故障率
RAID0ストライピングそのままおよそ倍
RAID1ミラーリング半分そのまま半分

Google トランジット記法(Ver1.32以降)

Googleトランジットにリンクする記法です。

[g:tr:東京:大阪]
[g:tr:横浜:池袋:横浜から池袋まで]

と入力すると次のようになります。

東京:大阪

横浜:池袋:横浜から池袋まで

2007/04/28(土)ページ送りのためのデータベース検索

adiaryにGoogleのようなページ送りを実装できないかというお話がメーリングリストでありました。

google_pages.jpg

実は速度面で不利なので実装してなかったんですよね。どれくらい不利なのか調べてなかったので検討してみました。

その前にクイズ

adiaryではご覧の通り「次のページ」と「前のページ」は(存在すれば)常に表示されるようになっていますが、実は全件のデータを取得することなく表示しています。どうやってるか分かりますか?

分かってしまえばかなり単純な仕組みですが、ご覧の通り十分実用的な動作をしています。

実行速度の検証

データ数が少ないときは気にするほどの差はないので、とりあえず810件、1MBほどのデータをインポートしてテストしてみました。

条件pseudo(cgi)pseudo(cache)Pg(8.1.8)MySQL(5.0.27)
今の実装(トップ)131.0 ms110.9 ms92.9 ms50.2 ms
件数取得(トップ)130.7 ms111.7 ms98.2 ms51.7 ms
今の実装(検索)135.3 ms121.6 ms171.5 ms86.1 ms
件数取得(検索)1642.6 ms397.5 ms269.4 ms88.3 ms

検索は「あ」一文字の全文検索という、多量にデータが引っかかるような検索を行いました。マシンは相変わらず C3 500MHz(笑)

擬似データベース(pseudo)

件数取得の全文検索以外は大したことないですね。擬似データベースはインデックスデータを常にメモリに展開していますので、全文検索しない限りは個別のデータファイルを参照にいきません。たかだか数件しか取得しないのならば、数件発見した時点で個別データのロードを終了するので、通常のトップ表示と比べせいぜい10ms程度しか違わないわけです。

  • 件数取得の検索は、810個の個別のデータファイルをすべてロードすることになり、多量の時間がかかっている。
  • cache と付いているのは、cgiがキャッシュされている場合です*1。この場合、一度でも読み出したファイルはメモリにキャッシュされますので810件のファイルを開く必要がないので速く済んでいます。
  • それ以外は、結局メモリに展開されている(1ファイルになっている)インデックスデータをループで回すだけなので、時間的にほとんど変わらない。

PostgreSQL (Pg)

総じて擬似データベースより少し良いパフォーマンスという感じですね。件数取得に時間がかかっていますが、PostgreSQLでは、件数取得を指示された場合、そのためだけに1回クエリを投げる実装になっています(Satsuki-systemの実装の話です)。

SELECT count(pkey) FROM $table WHERE ~

件数取得をすると時間が増えてしまうのは、単純に検索範囲が増えた結果なのか……難しいところです。

MySQL 5.0.27

MySQL (MyISAM) がこんな速いとは思いませんでした。単純 Query で約倍も速い。MySQLはありがたいことに、検索時にヒット件数を取得する書式があり、ほとんどオーバーヘッドなく該当件数を取得出来ます。

SELECT SQL_CALC_FOUND_ROWS $cols FROM $table WHERE ~

*1 : SpeedyCGI, FastCGI, mod_perl等

考察

微妙なところですねぇ~。気にしなくていいと言えば気にしなくていいような、気にすると言えば気にするような。検索のことを考えなければ、どのデータベースでも件数取得しても問題ないのですか、全文検索、しかも多くのユーザーが使っている cgi 動作を考えると、ちょっとなぁ……。

その他、擬似データベースは結構チューニングしてあるのですが、その甲斐あってか思ったより高パフォーマンスでびっくりました。1000件ぐらいなら余裕で使えますね。これ見てるadiary利用者の方々は「MySQL速いんだ、ぜひとも使おう」とか思いませんように。

adiary を cgi として動かす限り、MySQLの速さの恩恵にあずかるどころかMySQLのモジュールロード時間で恩恵どころか遅くなってしまいます(PostgreSQLも同様)。速く動作させたかったら、まず cgi 以外の方法で動作させてからにしましょう。

件数取得せずにページ送りを出す方法

クイズの答え。例えば1ぺージ5件表示だとしたら、検索するときに6件のデータを取得します。検索結果として6件のデータが得られたら次のページが存在すると分かります。前のページがあるかどうかは、検索開始位置が1件目であれば前のページがない、2より大きければ前のページがあると判別します。

単純でしょ?