2004-07-02 [長年日記]
_ Webパフォーマンス向上作戦 (13:51)
なんか最近うちのサイト(というかサーバー)がげろ遅くてアクセスするのがいやになってきた。
もともとうちのサイトは、自分で思いついたネタ(ソフト)を手軽に実装&改変することができるようなフレームワーク上に構築しているんで、ほとんどのページが動的に生成されている。
それでもそれなりの負荷に抑えるような設計にはしてあるんだけど、最近のアクセス数の増大(なんか知らんが先月は1日10万ヒットくらいになってたよ。たぶんYahoo!が検索エンジンを変えた結果、末端ページにYahoo!経由で来る人が増加したからだろうなー)と、最近思いつきで行った機能増強(Estraierとの連携関連はかなりリソースを食う)のせいで、一気にパフォーマンスが限界に来たっぽい。
もう1台サーバーを増やして分散させるってのが、今時の解決策なんだろうけど、さすがに趣味で2台もサーバーは持つのはきつい。AirH"も128k契約のままだし、夏WINでauもフラットに乗り換えるつもりだから(そうしたらAirH"は32kに戻す)、あまりにもネットワークエンゲル係数が高くなりすぎてしまうよ。しかもサーバーは気軽に解約できないからなー。
というわけで、がんばっていろいろ最適化して(Webの見た目の)パフォーマンスを向上させてみた。
まず最初にいじってみたのはMySQL周り。テーブル構造(とかインデックス周り)はいじるところがなさそうだったんで、MySQLのサーバーパラメーターを適当にいじってみる。MySQLサーバの最適化(http://dev.mysql.com/doc/mysql/ja/Optimising_the_Server.html)を眺めながらいろいろいじってみたけど、よくわからん。
基本的にはよく使いそうな項目を中心に、MySQLの使用メモリ量を増やしてやればいいんだろうけど、うちの場合MySQLが半分、その他が半分くらいでリソースを使っているんで、MySQLにメモリを与えすぎると他のプロセスの方が重くなりすぎる。というかそのせいで昨日何度かサーバーを落としてしまった。512M中200MくらいをMySQLに与えるのはやりすぎだったらしい。
結局key_buffer_sizeとjoin_buffer_sizeとsort_buffer_sizeに30Mくらい割り振り、それ以外もちょぼちょぼと増やして100Mちょっと割り振ってみたけど、なんかあんまり早くなっていないっぽいなー。でかいテーブル同士を日付で絞ってjoinしてcountするquery(=blogmapのランキング集計)を高速化するにはどのあたりをいじればいいんだろう。ひとまずこれ以上いじっても改善される気がしないんで、MySQLサーバーパラメータいじりは中断。
続いてソフトウェアの対応。うちのサイトで使っているフレームワークの負荷低減の肝は、データオブジェクトレベルとHTMLパーツレベルで柔軟にキャッシング(および動的破棄)できるところで、逆に言うとそれよりも大きなレベルでのキャッシング(ページ単位とか)はやらないことによって、適当にコードをいじってもちゃんと追随できるようにしていた(そうしておかないと、よくキャッシュ間の不整合が起きておかしくなるんで、手軽に思いつきでいじりにくくなる)。
けど、もう現在このサーバーで動かしているアプリケーションに関しては、既存のコードを拡張していくくらいならば、データだけ互換にしてスクラッチで組み直した方が幸せそうな気がしてきたんで、遊べる度を低める結果になってもかまわないや。ってことで、より大きなレベルでキャッシングすることにした。
まずページレベルのキャッシュとして、アクセス数の多いblogmapのトップページとRSS、mylogのトップページとRSSを静的生成に変更。もともとPHPで動的生成されているものだから、それをWebからキックして動かすのではなくローカルでcron動作させて、
ob_start();
//従来のページ生成コード
file_put_contents('index.html', ob_get_contents()); //なんてPHP4にはまだないよ
ob_end_clean();
なんて感じでindex.htmlとrss.rdfとして保存すればおしまい。ついでにindex.html.gzとrss.rdf.gzファイルも用意して、
<Files index.html> ForceType text/html Options MultiViews </Files> <Files rss.rdf> ForceType application/xml Options MultiViews </Files>
とかするとコンテントネゴシエーションも働いてくれるかな。まあRSSの方はAccept-Encoding: gzipなリクエストを投げてくれるものは少ないかもしれないけど。ああそういえば前にどこかでRSSのContent-Typeは何が妥当かという話を見かけた気がする。結論はapplication/xmlじゃなかったはずだよなー。あとで調べて直しておくか。
で、トップページとRSSは静的データとして返すようにしたけれども、それでもあんまり負荷が低くならない。まあトップページとかはもともとかなり最適化されていたんで、高負荷の原因としてはさほど大きくはなかったからな。どちらかというと、あまり最適化されていない割には、最近検索エンジン経由のアクセスが増えている末端ページも最適化しないとまずいか。
そこで、Apacheにperfomance_logを出力させてしばらく様子をみる。
LogFormat "%T\t%v\t\"%r\"" performance
なんて感じでページごとの実行時間をTSVで出力させて、ダウンロードしてExcelで読んでソートしたりとか。で、遅そうなページに関しては、そのページのフレームワークのベンチマーク機能をONにして、関数(というかプラグイン)レベルでの実行時間も同様に測定。
やっぱり重いのはblogmapの末端ページだなー。Amazon Web Serviceが重いのは(おそらくネットワークレイヤーの話なんで)ソフトウェア的に改善できるあてはないんで放置。それ以外で重いのは、Estraierの関連ドキュメント検索か。これはもともとEstraierのインデックス更新が日次処理なんで、ものすごく長めにキャッシングしてもいいはずだ。あと、リンク元表示もどうせこっちでクローリングしているんだから長めにキャッシングしていても大丈夫なはず。BBS/TrackBack以外はめちゃめちゃ長く(数時間)キャッシングしてしまえ。
というわけで、そのあたりを中心に、今まであまりやっていなかったHTMLパーツレベルでのキャッシングを追加。うちのフレームワークでは、
function someHtmlParts() {
$cache = new Cache('cache_namespace'); //キャッシュオブジェクト
$cachekey = 'cache_name'; //名前
$cacheoption = array( //オプション
'available' => 60, //基本的に60秒間有効
'files' => array('/tmp/related_file') //更新されたらキャッシュを破棄
);
$html = $cache->Load($cachekey, $cacheoption);
if ($html) {
echo $html;
return;
}
ob_start();
//以下HTMLパーツを出力する従来のロジック
$html = ob_get_contents();
$cache->Save($cachekey, $html);
ob_end_flush();
}
という感じ。ちなみにキャッシュの強制破棄は、
$cache->Remove($cachekey);
とか
touch('/tmp/related_file');
って感じね。
さらにHTMLパーツを複数まとめてのキャッシングを増やしたり、データオブジェクトレベルでのキャッシュ有効期間を長めに変更したりしてみたんで、一度アクセスがあった末端ページについては、読み込みがかなり早くなったことでしょう。
ちなみにApache(1.3)の設定は、
Timeout 60 KeepAlive On MaxKeepAliveRequests 100 KeepAliveTimeout 15 StartServers 10 MinSpareServers 10 MaxSpareServers 50 MaxClients 50 RLimitCPU 50
なんて感じにしているんだけど、こんなもんでどうでしょうかね? もっとSpareServersを増やした方がいいのかなー。あと、Apache 2に移行するべきかどうかも迷う(PHP5の正式版がリリースされたら、それと同時にApache 2に移行しようかと思っているんだけど)。
_ AreaEditorにしましょう (13:51)
extedit(http://mylog.ishinao.net/id/1177)をご利用のみなさん、AreaEditorに乗り換えましょう。exteditの欠点(ブラウザがロックされる)が解消された完璧なIE外部エディタ連携ソフトですよ。Sleipnirとかから秀丸を開いて、タブを移動しながらテキストを書いて保存とか、ちゃんとできるようになりますよ。
内部的な動きを想像してみると、まずトリガーとしてはexteditと同じくIEから呼び出された際に、呼び出し元IE(Window)オブジェクト(external.menuArguments)をもらって、そこから該当のtextareaオブジェクトを得ているんでしょう。
で、textareaオブジェクトのインスタンスを得たあとは、データディレクトリにtextareaの内容をテキストファイルとして出力し、それを指定されたエディタで開き、ひとまずIEから呼び出されたスレッドは終了してしまう。これでブラウザのロックは解消される。
以降は、先ほど生成したテキストファイルの更新を監視(確かそういうAPIがあったよな)。ファイルが更新されたら、テキストファイルの内容を読んで、textareaオブジェクトの内容を更新する。って感じじゃないかな。
というか、exteditの欠点を解消すべく、そういうものを自分でも作ってみようかと思ったんだけど、
- 他の言語で、IEのコンテキストメニューハンドラから呼び出されたときに、external.menuArgumentsオブジェクトを得る方法がよくわからん
- コンテキストメニューから呼び出されたスレッドでは、external.menuArgumentsとかその中身のオブジェクトとかは有効だろうけど、そのスレッドが終了したあとにも、そこで取得したインスタンスは有効なんだろうか?
とかがよくわからなくて放置していたんだよなー。

