毎秒1000リクエスト を捌く超高速CMS「adiary」
2010/04/28(水)[adiary2] かんたんログイン認証の脆弱性対策
今はかんたんログイン脆弱性がブームなようです。今更何言ってるんだという感じですが、一応adiary内部でどのようにかんたんアクセスを処理しているか書いておきます。
かんたんログインとは
携帯の機種固有IDを使用した自動ログイン機能のことです。Cookieを実装する気のない某殿様商売通信キャリアの存在が一気に利用を広げました。
機種固有IDは、USER-AGENTなどのHTTPヘッダによって送信されます。HTTPについて知っている人ならすぐわかることですが、このヘッダは誰でも簡単に任意の値で送信することができるため何のセキュリティにもなりません。例えるなら、掲示板に書き込む際の名前欄で承認しているようなものです。しかも比較的単純な通し番号なので、秘匿性もあったものではありません。*1
adiaryの実装
もともとこんな危険なものを実装する気はなかったのですが(苦笑)、要望があったのでしぶしぶ実装しました。
右の画面がかんたんログインの実際の画面です*2。このページをブックマークしてもらい「かんたんログイン」をクリックしてもらうことでログインできます。
ID:useridの人がかんたんログインを設定したこの画面のURLは次のようになります。
http://~/adiary.cgi/=userid:LE0GdeSwSdQ/?login_docomo
LE0GdeSwSdQという文字列は「機種固有IDから生成されるハッシュ」です。adiary.conf.cgiの中の
<$Secret_phrase = '秘密の言葉'>
の設定が漏洩しない限りハッシュ文字列を第3者が知ることはできません。
仮にこのログインURLだけ漏洩しても、対応する機種固有IDが分からないとログインできません。ですので、いわゆるかんたんログイン脆弱性はadiaryには存在しないことが分かります。
しかし別の問題が……
次は携帯ログイン後のURLです。
http://~/adiary.cgi/=userid=kpQYloloFFF0M4Xnujjs/?login_docomo
「kpQYloloFFF0M4Xnujjs」がセッションIDになります。Cookieが使えないので仕方なくURLに埋め込んでいるのですが、リファラーからこのURLが漏れるとセッションが乗っ取られてしまいます。
ドコモはリファラを送らないことになっているみたいですが、他機種だと色々で頭の痛い問題です。
(参考)携帯電話向けWebアプリケーションのセッション管理手法
とりあえず
Cookieを食わせて、食べられたらそっちに誘導するように変更するか……。Cookie非対応でリファラ送信なんて機種もあるそうですが、それは端末脆弱性なんで無視かな。
2008/02/03(日)SpeedyCGI/FastCGI でモジュール動的更新
Satsuki-system(adiaryの基礎)で使用している、モジュールの自動更新機構と SpeedyCGI/FastCGI での実装について詳細に解説します。
モジュール自動更新の必要性
SpeedyCGIやmod_perl等々のスクリプトキャッシュ環境で実行速度が改善するのは、プログラム本体やライブラリをコンパイルした状態で内部に保持するためスクリプト実行時間の大半を占めるコンパイル時間が短縮されるというメリットがあるためです。特に最近のスクリプトは便利なライブラリを使用する傾向が強く、実行時間の95%以上がスクリプトのコンパイル時間なんてこともめずらしくありません。*1
しかしそのような環境では、スクリプトライブラリを書き換えてもプログラムは依然としてキャッシュした古いライブラリを使用し続けるので、少し書き換える度に SpeedyCGI の常駐プロセスを手動で殺したり、Apacheを再起動させたりといった手間が必要になります。
これを避けるためにはスクリプト自身でライブラリの更新をチェックし、ライブラリが更新されていた際に自動的にスクリプトを再起動(リロード)させる仕組みが必要になります。
ライブラリ更新チェック方法
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 させるわけにはいかないので、別の方法になっています。
なぜか更新が検出できない
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 します。
2007/04/28(土)ページ送りのためのデータベース検索
adiaryにGoogleのようなページ送りを実装できないかというお話がメーリングリストでありました。
実は速度面で不利なので実装してなかったんですよね。どれくらい不利なのか調べてなかったので検討してみました。
その前にクイズ
adiaryではご覧の通り「次のページ」と「前のページ」は(存在すれば)常に表示されるようになっていますが、実は全件のデータを取得することなく表示しています。どうやってるか分かりますか?
分かってしまえばかなり単純な仕組みですが、ご覧の通り十分実用的な動作をしています。
実行速度の検証
データ数が少ないときは気にするほどの差はないので、とりあえず810件、1MBほどのデータをインポートしてテストしてみました。
条件 | pseudo(cgi) | pseudo(cache) | Pg(8.1.8) | MySQL(5.0.27) |
---|---|---|---|---|
今の実装(トップ) | 131.0 ms | 110.9 ms | 92.9 ms | 50.2 ms |
件数取得(トップ) | 130.7 ms | 111.7 ms | 98.2 ms | 51.7 ms |
今の実装(検索) | 135.3 ms | 121.6 ms | 171.5 ms | 86.1 ms |
件数取得(検索) | 1642.6 ms | 397.5 ms | 269.4 ms | 88.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 ~
考察
微妙なところですねぇ~。気にしなくていいと言えば気にしなくていいような、気にすると言えば気にするような。検索のことを考えなければ、どのデータベースでも件数取得しても問題ないのですか、全文検索、しかも多くのユーザーが使っている cgi 動作を考えると、ちょっとなぁ……。
その他、擬似データベースは結構チューニングしてあるのですが、その甲斐あってか思ったより高パフォーマンスでびっくりました。1000件ぐらいなら余裕で使えますね。これ見てるadiary利用者の方々は「MySQL速いんだ、ぜひとも使おう」とか思いませんように。
adiary を cgi として動かす限り、MySQLの速さの恩恵にあずかるどころかMySQLのモジュールロード時間で恩恵どころか遅くなってしまいます(PostgreSQLも同様)。速く動作させたかったら、まず cgi 以外の方法で動作させてからにしましょう。
件数取得せずにページ送りを出す方法
クイズの答え。例えば1ぺージ5件表示だとしたら、検索するときに6件のデータを取得します。検索結果として6件のデータが得られたら次のページが存在すると分かります。前のページがあるかどうかは、検索開始位置が1件目であれば前のページがない、2より大きければ前のページがあると判別します。
単純でしょ?
2006/07/08(土)Satsuki-system技術情報
はてな技術者のnaoyaさんのPerl技術記事見てたんですが
adiary関連の技術情報=マニュアルも本格的にまとめ始めたほうかいいなぁーと思ったりしたりしました。数日中にもβ版公開しましすが、ソースみただけではわからないところが多々ありますし。
比較的短期間かつ少人数(一人か)でこれだけのシステムになったのかってのは訳があって、生産性を担保するシステム構成によるところがとても大きいのです。それを担保してくれる Satsuki-system(スケルトンシステム)がなかったらadiaryの開発は不可能でしたし、そもそも「自作のスケルトンシステムがあまりに便利かつ有用なので、これを有効活用する手だて」としてblog(adiary)を開発したという、なんとも本末転倒な話だったりします(笑)*1
Satsuki-systemとTemplate Toolkitの比較
スケルトンシステムというのは何度も申し上げているとおりで、Perlの変数埋め込みを一歩進めて簡単な命令を書けるようにしたものです。この考え方の有名な例として、Movable Typeにも使われている*2 Template Toolkitがあります。このTemplate Toolkitは変数置換の他、簡単な条件構文やループなどをHTMLテンプレート中に記述できます。
Template Toolkitでは読み込まれたHTMLファイルを、Perlで実行可能なプログラムとしてテンポラリに吐き出します。次以降は、そのコンパイルされたファイルを実行するわけです(mod_perlのときはキャッシュされるのかな?)。Satsuki-systemも紆余曲折の上、現在はその形に落ち着いています。
でも、Satsuki-systemとTemplate Toolkitは決定的に違うところがあります。Template Toolkitのキャッシュはperlプログラムとして実行することでそのまま出力HTMLが得られますが、Satsuki-systemのそれは、そのまま実行することは不可能なプログラムになっています。Satsuki-system本体とコンパイル済のプログラムの2つが合わさって初めて意味を持つわけです。
なんだ「使いにくいだけじゃないか……」と言われるかもしれませんが、実はそこが最大のミソなのです。Satsuki-systemはスケルトンシステムでありフレームワークでもあります。スケルトンシステムから参照する必要なルーチン(ライブラリ)は、システム本体に納められており、そのライブラリ部分を自由に拡張できることこそが最大の特徴です。
以下はスケルトンのコメント表示部分を抜き出したものです。
<p><span class="canchor"><a href="<@t.this_diary_url>#c<@t2.pkey>#">#</a></span> <span class="commentator"><span<@if(t2.id, " title=<@t2.id>")>><@t2.name></span></span> 『<@t2.text_short>』 <span class="comment-date">(<@tm_printf('%Y/%m/%d %J:%M', t2.tm)>)</span></p>
強調表示のところが埋め込みです。大半はただの変数置換だと思って問題ありません。一番最後の、tm_printf が関数呼び出しとなっていて、この実体はSatsuki-system本体(Base.pm)に含まれます。この関数に相当するものを増やしていくことで、システム全体を構築します。*3
つまり、Template Toolkitキットでは「Perlプログラムの付属品としてのHTMLテンプレート」であったわけですが、Satsuki-systemでは「スケルトン(HTML)の付属品がPerlプログラム」なのです。
Satsuki-systemとPHPの比較
そこまで、スケルトン万歳ならはじめからPHPみたいな埋め込み言語を使えばいいじゃないかという話になります。PHPは使ったことはないのですが、一つそして決定的にPHPを使いたくない理由があります。プログラムとHTMLのソースが一緒になっててメンテしずらい。きちんと書かれた(?)PHPのソースならばそうはならないのかもしれませんが、1画面=1HTML=1プログラム主義のように感じます。
というわけで、もちろんSatsuki-systemだって同じだろうというツッコミもあります。adiaryの日記一覧のような部分は、実際にHTML内にPHPのようにプログラム的動作を記述してページ送りなどを実装していますから、同じといえば同じです。ですが、それは推奨される形ではなく、ほとんどの場合1ページに対して1つのPerlのサブルーチンが付属しています。
RoRのモデル・ビュー・コントローラー的な考えからいえば、Viewがスケルトンで、ControllerがPerlのコードということになります。PHPとの大きな違いはここですが、PHPのプログラム部を外部ファイルとして追い出したとしたら(本体には呼び出しのみ残る)、似たようなものだと思います。
Satsuki-systemとRuby on Railsの比較
比較も何も……似たもの同士です*4。これは別にRoR的思想を取り込んだわけではなくて、Satsuki-systemの「変数埋め込みHTMLの拡張」を突き進めた結果、似たようなものにたどり着いたというわけです*5。もっとも、最初の思想からMVCなRoRとはスタート地点で負けているわけですが、そこを敢えて比較すると
- Satsuki-systemはPerl用である。開発時点でRubyよりPerlの方が処理が速かったのがPerlになった理由ですが、Ruby 2.0 YARVが登場すると立場逆転しそうです(汗
- RoRの方が思想的にOO(オブジェクト指向)として綺麗である。そもそもPerlのOOはやっつけなので、これもスタート地点で負けてます。
となります。勝ち目ないですけど(汗) 後ろ向きなアドバンテージは、
まとめると
んー……Perl版フレームワークのないRoRの出来損ない?(ぉぃぉぃ*8
もっとも、
- 可能な限り高速に動作するよう設計されている*9
- mod_perl (prefork/worker)でもcgiでもSatuki-systemが差を吸収してくれる(ただし若干気を遣って書く必要はあり)
ですけども。早くマニュアル整備しろってことですね(汗)*10
2006/06/23(金)6/23版スナップショット
スケルトンコンパイラの改良
スケルトンコンパイラをさらに改良しました。今回の変更は多大な労力を割いた割に、テスト環境においてたった1msしか高速化しませんでした*1。コンパイル時間は絶対遅くなっていると思いますが……、1度コンパイルすればキャッシュされるので気にしない方向で(汗)
しかし、スケルトンシステムの仕様は、後々コンパイラ次第である程度性能を改善出来るよう設計していました。今回たまたま気になって、泥沼に入ってしまったのですが、改良に改良を重ねた遍歴で用のない(スマートでない)コンパイル処理を何度も行っていたためスリム化。
とか言いながらパスは7パスになりましたけど……(^^;
newコンパイラ@VIA C3
VIA C3@500 という、とてつもなく環境にやさしい(遅い)CPUで今回の変更を試してみました。非キャッシュ環境(cgi動作)ではそこそこでしたが*2、キャッシュされた環境(mod_perl2 or SpeedyCGI)で実行してみたら、びっくり!!
Total time : 39.3 ms
とても C3@500MHz とは思えないレスポンスをたたき出しております(笑)*3