Feb 28, 2007

VMware Virtualization Fair 2007に参加してきたよ

赤坂プリンスで開催されていたVMware Virtualization Fair 2007に参加してきた。記事も出ているね。

ヴイエムウェア、仮想化技術のロードマップを国内で披露 : ソフトウェア&サービス - Computerworld.jp

午前中の基調講演やパネルを観て、何だよVMwareの太鼓持ちイベントかよと思い、ついつい四川飯店で坦々麺だけ食べてそのまま帰りそうになったのだが、思いとどまって午後のVMwareのテクニカルなセッションに参加してよかった。なかなか面白かった。

特にOle AgesenのTalk。ASPLOS XIIの発表内容の平易な説明+αという感じ。ASPLOSのProceedingsはACMから買わないと読めないが、VMTNでも公開されているので誰でも読める。

A Comparison of Software and Hardware Techniques for x86 Virtualization - VMTN Technical Papers Directory

かいつまんで説明するとこんな話である。

PopekとGoldbergの74年の論文に出てくる古典的なVMMの作り方というのがある。古典的VMMでは特権命令が非特権コンテキスト中で実行されたときにトラップしてエミュレート実行することで、ゲストOSの実行中に現れる特権命令を正しくハンドリングするというもの。

ただし、この方法はトラップコストやMMUのエミュレーションコストが問題となる。前者のトラップコストはx86で1000サイクル以上(CPUのパイプライン長に相関する)にもなり、トラップ頻度はワークロードにも依存するが一秒に5万回以上発生する場合もある。また、後者は、ゲストのメモリーアクセスには基本的にゲスト仮想アドレス→ゲスト物理アドレス→ホスト物理アドレスという二段の間接参照をすることになるが、ハードウェアMMUはこれをサポートしていないために何らかのソフトウェアによるエミュレーションが欠かせないということである。歴史的にはIBM System 370などはinterpretive executionと呼ばれるゲストOS実行用の動作モードを備えて古典的VMMの性能問題をハードウェアで解決してきたという経緯がある。

また、x86にはpopfというユーザモードと特権モードの両方で動作するが振る舞いの異なる命令がある。カーネルコード中のpopfが何の割り込みも発生させることなくユーザコードとして実行できてしまう場合にはトラップできないことになる。トラップできなければエミュレート実行もできないので、x86では古典的方法は利用できない。

そういうわけでVMwareではBT(Binary Translation)と呼ばれる、実行中にゲストのカーネルモードをユーザモードで実行可能なエミュレーションコードに動的に変換する手法でソフトウェア仮想化を実現している。ユーザモードではネイティブ実行(Direct Execution)し、カーネルモードへのコンテキストスイッチをVMMが検出してBTに制御を渡す。BTでは、cliなど簡単な特権命令は直接コードに変換し、コンテキストスイッチなど複雑な命令はランタイムライブラリ呼び出しに書き換える。その結果としてジャンプ命令などではジャンプ先アドレスが変わり得るので、ベーシックブロック単位でコード変換を行った後、相対アドレス、レジスタ+オフセットアドレスなどはバックパッチする。変換結果はキャッシュされるので実行のたびに変換が必要なわけではない。このあたりのテクノロジーは変換速度を優先してバイナリーコードの局所最適化などを行わないだけで、JITコンパイラとそれほど違わない。

一方でIntel VT-xやAMD SVMのようなVT、ハードウェア仮想化が注目されている。VMMが動作するroot modeとゲストOS・アプリケーションが動作するnon-root modeの間を新たに追加されたVMEnter/VMExit命令を使って遷移することができる。ただし、VTを使って実現できるのは古典的なTrap & Emulate型のVMMなので、トラップオーバーヘッドはトラップ回数に比例するという制約が依然としてある。

ベンチマークを行ってみるとBTの方が軒並み速い。これは今のところVMEnter/VMExitのコストが大きいためにBTのメリットが勝る場合が多いためだ。マイクロベンチマークで特定の命令ごとに見てみるとsyscall/sysretのようにVTでネイティブと同等の性能が出るものや、call/retのようにBTではキャッシュ上にコードをリロケートするためにジャンプ先アドレスの決定に余計なコストがかかるものではVTの方が勝る。また、特権レジスタ%cr8への書き込みのようにソフトウェアでのエミュレーションの方が圧倒的に速いものもある。

VMwareのプロダクションコードではどうしているかというと、VMware Infrastructure 3 (ESX Server 3)では、VT-x付きEM64T上でx86_64 OSをゲストとして動作させる場合のみVT-xを用い、それ以外の場合(i386ゲストの場合やAMD64)はBTを用いている。EM64T+x86_64 OSでVT-xを使っているのは、現状BTでの実行に問題があるからであってVT-xが速度的に優れているためではない。

しかし悲観することはなくて、VT付きPentium 4でのVMEnterが2409サイクルであるのに対してIntel Coreでは937サイクルまで縮小している(VMExitはそれほど改善していない)。つまり、プロセッサの世代ごとに改善されてはいる。

(続きは温泉に浸かってから書く)

(温泉から帰ってきたので続き)

ところで、BTでもVTでもMMUのエミュレーションコストの問題は解決していない。一般にゲスト仮想アドレスに対応するホスト物理アドレスを決定するには、ゲストのページテーブルをルックアップしてゲスト仮想アドレスからゲスト物理アドレス(ホスト仮想アドレス)を得、さらにホストのページテーブルをルックアップしてホスト物理アドレスを得る、という二段階のルックアップが必要になる。

これに対して、VMware Workstationの実装では、ゲストの仮想アドレスからホストの物理アドレスに対応させるシャドーページテーブルをVMMで管理する。このシャドーページテーブルは(対応アドレスがホスト物理アドレスになっている点を除いて)ゲストのページテーブルのキャッシュとなっており、ホストのハードウェアページ保護機能を用いてアクセス権限を管理する。ユーザモードのメモリーアクセスは(ページテーブルポインタを制御することで)シャドーページテーブルを経由して物理メモリアドレスにマップされるが、保護違反やキャッシュミスがあればトラップされてVMMに制御が渡される。VMMはキャッシュミスなら上述の二段階のページテーブルルックアップを行ってシャドーページテーブルにPTEを加えてから制御をゲストに戻すし(メモリアクセスをリトライして必ずシャドーページテーブルルックアップに成功する)、そうでなければトラップ情報をゲストにフォワードする。

何が問題かと言えば、キャッシュミス時のテーブルルックアップコストが大きいし、ゲストでページテーブルが更新されるたびにそれを検出してシャドーページテーブルに反映する手間がかかる。また、コンテキストスイッチが起きるとTLB同様シャドーページテーブルも無効化される必要がある(その結果キャッシュミスが増加するかプリフェッチのコストを払うかどちらかを選択することになる)。このため、メモリトランザクションやコンテキストスイッチの多いワークロードでは現状のVMware(を含む仮想化ソフトウェア)の、ネイティブ実行に対する性能はかなり劣る。

しかし、この点に関しても楽観視してよい。なぜなら、次世代のプロセッサの仮想MMUサポート(NPT/EPT)は、こうしたコストを0にするか、ハードウェアで支援するのでソフトウェアオーバーヘッドは激減する。結果的に計算集中型でないワークロードでも仮想化ソフトウェアの性能が遜色ないものになるだろう。

Talkの後にOleにEPT/NPTがある環境下でのベンチマーク結果はあるのか聞いてみた。ベンチマーク結果はない。AMD Barcelonaのpreliminaryなバージョンでの動作試験をしている。エミュレータでの実験は山ほどやっていて良い結果が得られているとのこと。

ちなみにOleがSun MicrosystemsのBurlingtonでJITコンパイラの仕事をしていた(結局西海岸で作っていたHotSpotに負けてそのグループのVM+JITコンパイラ実装はご破算になってしまったが)ときにオフィスを訪問したことがあるし、98年に東工大・富士通に滞在していた時も東工大で机を並べて仕事していた。私がすごいハッカーだと思う人の一人である。

メモ: NTFSでフォーマットした USBハードディスクを FC6で使う方法

Fedora Core 6にNTFSでフォーマットしたUSBハードディスクを認識させるのは少々面倒だったので、その覚え書き。

  1. root権限でyum install ntfs-3gを実行してntfs-3gをインストールする。fuseがインストールされていない場合には、fuse, fuse-libsも一緒にインストールされる。
  2. /etc/hal/fdi/policy/storage-ntfs.fdiというファイルを作って中身を下のようにしておく。
    <?xml version="1.0" encoding="UTF-8"?>
    <deviceinfo version="0.2">
      <device>
        <match key="volume.fsusage" string="filesystem">
          <match key="volume.fstype" string="ntfs">
    	<append key="volume.mount.valid_options" type="strlist">locale=</append>
          </match>
        </match>
      </device>
    </deviceinfo>
    
  3. root権限で/etc/init.d/hald restartを実行する。
  4. ユーザ権限で下記を実行する。gconftool-2の代わりにgconf-editorを使ってもよい。
    $ gconftool-2 -s /system/storage/default_options/ntfs/mount_options \
                  -t list --list-type=string [umask=222,locale=ja_JP.UTF-8]
    
  5. おもむろにUSBハードディスクを接続する。

割と面倒。

Feb 26, 2007

近況

昨年末あたりからずーっと続いていた三度目の就職試験のようなものが先週無事に終わった。ただただ疲れた。今は開放感でいっぱいだ。自分の中でまったく迷いがないというわけではないけれど。

査読を二本片付け、今年度の業績リストを作り、iSCSI周りのプログラムを大慌てで書いた。

VMware Workstation 6.0 Betaをちょっと実験。6.0 Betaはx86_64サポートがよくなったので、VMware Server 1.0.1のモジュールを一部64bit版で置き換えられるんじゃないのと思ったのが運のつき。結構嵌った。結論から言うと、ホストの/usr/binの下のvmnet関係のユーティリティは64bit版で置き換えられる。VMのKernelが2.6.19以降でもvmxnetなどのモジュールが特にパッチなしでコンパイルできるようになる。ただし、x86_64用のvmxnetは未サポート。

インターネットカフェ(マンガ喫茶)というものに行ったことがなかったので、土日に3時間ずつ滞在してみた。システムとか過ごし方とかよく分からなかったので、浦沢直樹の20世紀少年を1巻から15巻まで読んでみた。作者があらかじめ何が記憶違いで何が嘘なのか決めずに近視眼的に物語を始めたせいで、張り巡らせたプロットの一部しか決着しない終わり方になりそうな予感がする。きっと評判はよくないに違いない。それが子供の思考というものだし、子供の作る物語が破綻しているのは自然なことだ。子供の作り出す虚構が現実のものになったときそれに翻弄される人々を描くのが物語のテーマで(あるいはそういう方法で反物語性を獲得するのが「作者」のテーマで)、それ以外に意味はないというのが15巻まで読んだ時点での感想。2000年、2015年の事件が巻き込まれた大人側からは描かれているが、当事者側からは描かれないために詳細がちっとも分からないのは、当事者(=1970年の子供たち)が詳細を想像し得なかったせいであって作者の描写には必然性がある。既存のドラマツルギーに固執したり、物語に没入したりするタイプの読み手の受けが悪いのは仕方ないが、漂流教室みたいなカルト作品になるだろう。

そして日曜日、また一つ歳をとった。

今週はVMFair 2007第5回SPAスプリングワークショップに行く予定。

Feb 17, 2007

TracのOpenID認証プラグインを試す

OpenID界隈に少し賑わいが戻ってきましたか?

Bill Gates, Craig Mundie: RSA Conference 2007
自分のブログURLをIDで使える!-OpenID.ne.jp(オープンアイディー)

OpenID.ne.jpみたいなサービスって結構たくさんあります。

意味なく片っ端から登録したりして、却って訳が分からなくなってきたのでここにメモ代わりに書いておきます。

  • http://ogawa.videntity.org/
  • http://ogawa.pip.verisignlabs.com/
  • http://getopenid.com/ogawa
  • http://mylid.net/ogawa
  • http://hogawa.myopenid.com/
  • http://ogawa.openid.ne.jp/

すでにパスワードが分からなくなっているやつもありますが。何のためのOpenIDか、と(笑)

本来は上記のOpenID URLのいずれか一つに認証をdelegateして使うものなのでたくさん登録しても意味がありません。単に付随サービスを比較してみたかったわけです。


いやいや本題はそういうことではなくて、若葉マークのTrac野郎が(Ogawa::Buzz: Trac野郎はじめました。)、スパム避けにOpenID認証プラグインを試しに使ってみたという話です。

OpenidPlugin - Trac Hacks - Plugins Macros etc. - Trac

このプラグインはTrac標準のBasic認証、Digest認証の代わりにOpenID認証を利用できるようにするものです。ユーザ名の代わりにOpenID URLが使われ、認証処理自体は外部サービスによって実施されます。つまり、Tracサイトの運営者はAuthenticationを行う必要がなくなり、Authorizationのみに留意すればよくなるという大きなご利益があります。

まず必要なものを(FreeBSDなので)portsでインストールします。

# portinstall py24-openid py24-pycrypto py24-elementtree py24-setuptools
Fedora Core 6だと「yum install python-openid python-crypto python-elementtree python-setuptools python-sqlite2」とかやって必要なものを全部入れることになります。嫌なのは、yumもtracもpython-sqliteに依存しているのに、OpenID PluginやTurboGearsはpython-sqlite2に依存している点。しかも、両方インストールしてあるとmod_pythonが死んだりするので黙ってmod_fcgidを使うしかありません。ムッキーッ!!という感じ。できる子はみんな自分でPythonを/usr/localにインストールして使っているのでしょう。そういうわけで以下はFreeBSDを前提に説明します。

次に、流れるようにeggファイルをビルドしてTracの共通pluginsディレクトリにインストールします(easy_installを使わないのはシステムのsite-packagesを汚されるのが嫌なので)。

# svn co http://trac-hacks.org/svn/openidplugin/trunk openidplugin
# cd openidplugin
# python setup.py bdist_egg
# cp dist/OpenIDAuth-0.1dev-py2.4.egg /usr/local/share/trac/plugins

さらにconf/trac.iniの末尾に以下の行を追加して、

[components]
trac.web.auth.* = disabled
openidauth.* = enabled

データベースをアップグレードして、OpenID Plugin用のスキーマをデータベースに書き込んでもらいます。

# trac-admin /var/www/projects/project1 upgrade

httpd.confはこんな感じで修正し、apachectl restart。

# Trac
<IfModule mod_python.c>
    <Location /project1>
        SetHandler mod_python
        PythonHandler trac.web.modpython_frontend
        PythonOption TracEnvParentDir /var/www/projects
        PythonOption TracUriRoot /
        SetEnv PYTHON_EGG_CACHE /tmp/.egg-cache
    </Location>
</IfModule>
<IfModule !mod_python.c>
    <Directory /usr/local/share/trac/cgi-bin>
        AllowOverride None
        Options None
        Order allow,deny
        Allow from all
    </Directory>
    <IfModule mod_fcgid.c>
        ScriptAlias /project1 /usr/local/share/trac/cgi-bin/trac-project1.fcgi
    </IfModule>
    <IfModule !mod_fcgid.c>
        ScriptAlias /project1 /usr/local/share/trac/cgi-bin/trac-project1.cgi
    </IfModule>
</IfModule>
 
# Trac authentication
<Location "/*/login">
    Require valid-user
    AuthType Digest
    AuthName "TracAuthRealm"
    AuthUserFile /var/www/auth/trac.digest
</Location>

あとはTracページのLoginからOpenID URLを入力してログインすればよいわけです。でも本当の戦いはこれから...。

Tracのデフォルトインストールでは非ログインユーザに不必要な(スパミングに十分な)権限が与えられています。

$ trac-admin [Trac Directory] permission list

を実行すると、anonymous (非ログインユーザ)にTICKET_CREATE、TICKET_MODIFY、WIKI_CREATE、WIKI_MODIFY権限が与えられているのが分かります。まずそれを剥奪しましょう。

$ trac-admin [Trac Directory] permission remove anonymous TICKET_CREATE TICKET_MODIFY WIKI_CREATE WIKI_MODIFY

次にOpenIDによるログインユーザに適当な権限を与える必要があります。それにはauthenticatedに権限を付与すればいいのです。authenticatedはanonymousの権限を継承しているので、上で削除した分の権限を付与すればだいたい問題ありません。

$ trac-admin [Trac Directory] permission add authenticated TICKET_CREATE TICKET_MODIFY WIKI_CREATE WIKI_MODIFY

後はTRAC_ADMIN権限を自分のOpenID URLに付与しておきましょう。

$ trac-admin [Trac Directory] permission add [OpenID URL] TRAC_ADMIN

ラフな設定としてはこれでおしまい。細かいパーミッションは運用しながら調整していけばよいでしょう。

Feb 16, 2007

もう少しだけmod_cacheを深追いしてみる

前のエントリーで書いた方法だとブラウザによってはページをリロードするたびに待たされることになります。

Ogawa::Buzz: mt-search.cgiを mod_cacheで超高速化する!!

なんでかな?と思ったので、動的コンテンツ(この場合はURLにクエリ文字列を含む動的コンテンツ)へのリクエストに対する、(mod_cacheで実現された)サーバーサイドキャッシュの振る舞いをもう少し深追いしてみました。

サーバーサイドキャッシュがない場合

サーバーサイドキャッシュがない場合には、すべての動的コンテンツへのリクエストごとにレンダリングを行い、そのデータがレスポンスとして返されます。

動的コンテンツで、Last-Modified, ETagヘッダを含むレスポンスを行い、If-Modified-Since, If-None-Matchヘッダを含むリクエストに対して304 Not Modifiedを返すことでレンダリング処理やトラフィックを削減することもできます。できますが、先のエントリーで見たようにCGIのプロセス起動のオーバーヘッドが数100ミリ秒もかかってしまう場合にはほとんど意味がありません。FastCGIやモジュール版PHPなどの場合には効果が期待できます。

サーバーサイドキャッシュがある場合

先のエントリーで示したように、Expiresヘッダをレスポンスとして返す動的コンテンツは、サーバーサイドキャッシュによってキャッシュされます。

下図に示すように、ブラウザがアクセスした際にそのURLがキャッシュされていれば、その内容をそのままレスポンスとして返します。また、キャッシュされていない場合やそのキャッシュが有効期限を過ぎている場合にはバックエンドの動的コンテンツにアクセスし、レンダリング結果を得、それをキャッシュした上でブラウザに返します。

一方、ブラウザのリロードボタンを押した場合など、ブラウザからのリクエストがキャッシュ制御情報を示すヘッダ(例えば、Cache-Control: no-cache、Cache-Control: max-age=0、Pragma: no-cacheなど)を含む場合には、下図のように、mod_cacheはキャッシュのチェックを行わず、バックエンドシステムにアクセスし、レンダリング結果を得、それをキャッシュした上でブラウザに返します。

ただし、mod_cacheにはCacheIgnoreCacheControlというディレクティブが用意されており、値をOnにした場合にはリクエストに含まれるCache-Controlヘッダを無視して、キャッシュにヒットすればキャッシュ内の情報を返します。これを設定すると期限切れになるまでキャッシュが更新されなくなるため効率は良いですが、有効期限を長く設定しているとキャッシュデータと真のデータがinconsistentな状態が長く続いてしまいます。この点に関しては下で再度触れます。

サーバーサイドキャッシュとConditional GETの組み合わせ

Expiresヘッダに加え、Last-Modified、ETagヘッダの一方または両方をレスポンスとして返す動的コンテンツもまた、上記と同様にサーバーサイドキャッシュによってキャッシュされます。

ただし、ブラウザはLast-Modified、ETagヘッダを含むURLへ再度アクセスする場合には、If-Modified-Since, If-None-Matchヘッダ付きのリクエスト(Conditional GET)を行う場合があります。この場合には、キャッシュ上にあるレスポンスヘッダと対応が取れていれば304 Not Modifiedを返し、対応が取れていなければIf-Modified-Since, If-None-Matchヘッダなしのリクエストと同等に扱います。つまり、キャッシュが無効でなければキャッシュデータを返し、無効ならバックエンドに問い合わせます。

ページロード時のブラウザの振る舞いとCacheIgnoreCacheControlの設定

Last-Modified, ETagヘッダの付いた(より正確にはブラウザはローカルにキャッシュできる)ページのロード時にどういうリクエストヘッダを送るかはブラウザによって異なります。Firefox 1.5/2.0、IE7で調べてみたらこんな感じ(下表中、Firefoxの強制リロードはCtrl+Reload、IE7の強制リロードはShift+Reloadを行ったものです)。

ヘッダ If-Modified-Since If-None-Match Cache-Control Pragma
Firefox 通常ロード
Firefox リロード max-age=0
Firefox 強制リロード no-cache no-cache
IE7 通常ロード
IE7 リロード
IE7 強制リロード no-cache no-cache

つまり、CacheIgnoreCacheControlをOff(デフォルト)にしている場合には、通常のリロード操作を行ったときIE7では304 Not Modifiedが返りますが、Firefox 1.5/2.0ではバックエンドへの問い合わせが発生します。リロード操作というのはWebブラウザでは案外普通にやってしまう操作なのでこの違いは大きいです。一方でCacheIgnoreCacheControlをOnにしている場合には、IE7でもFirefoxでも304 Not Modifiedが返ります。

ここで2つの戦略があり得ます。つまり、

  • CacheIgnoreCacheControlはOffにする
  • Firefoxユーザが不幸になることは看過する
  • ただし、Expiresはキャッシュの量が限度に達するまでの任意の期間を指定できる

という戦略と、

  • CacheIgnoreCacheControlはOnにする
  • Firefoxユーザも幸せ
  • ただし、ブラウザからサーバーサイドキャッシュを無効化する手立てがないので、Expiresに指定する時間も応分に短めに設定する

という戦略です。このサイトのタグアーカイブでは試しに下の戦略を採用してみています。

mt-search.cgiでLast-Modified, ETagヘッダを返すようにするパッチ

先のエントリーでは、mt-search.cgiのレスポンスにExpiresヘッダを追加するパッチを示しましたが、ここではLast-Modified, ETagヘッダも追加するためのパッチを示しておきます。

MT-3.34-MT-App-Search.patch

このパッチを当てれば、先のエントリーのBootstrap.pmへの修正は必要ありません。

Feb 12, 2007

mt-search.cgiを mod_cacheで超高速化する!!

かなり前からApache 2.2.xを使っているのですが、mod_cache/mod_disk_cacheなんていうモジュールが存在することに全然気がついていませんでした。このモジュールはサーバサイドでコンテンツキャッシングを実現するもので、CGIなどを使って生成される動的なコンテンツのレンダリング結果の再利用を可能にします(静的なコンテンツもキャッシュできますが、応答時間が問題になることはありませんし、一般的にはクライアントサイドでキャッシングされます)。キャッシュするコンテンツは時間制約の強くないものである必要があります。例えば、コメンティングシステムなどではユーザが行った操作をコンテンツに即座に反映する必要があるために適していませんが、検索システムやCMSのように対象となるデータセットが一定以上の間隔で更新されると期待され、レンダリング結果が変化しないような場合にはとても有効です。

Movable Type 3.3以降ではタグアーカイブの生成にmt-search.cgiが使われていてまたえらく遅いのですが、これを超高速化できるのではないかと思って試してみました。

httpd.confの記述

まず、何はなくともmod_cache, mod_disk_cacheをロードしましょう。

LoadModule cache_module libexec/apache22/mod_cache.so
LoadModule disk_cache_module libexec/apache22/mod_disk_cache.so

次にmod_disk_cache - Apache HTTP サーバmod_cache - Apache HTTP サーバを読んで必要な設定を加えます。最小限の設定は以下のようになります。

<IfModule mod_cache.c>
    <IfModule mod_disk_cache.c>
        CacheRoot /var/www/cache
        CacheEnable disk /MTDIR/mt-search.cgi
    </IfModule>
</IfModule>

httpd.confの編集が済んだらapachectl restartします。

MT::Bootstrapの修正

HTTP/1.1: Caching in HTTPあたりを読むと、クエリ文字列付きのHEAD/GETリクエストでは明示的に有効期限をサーバが返さない限り、キャッシュ機構はレスポンスをfreshなものとして取り扱ってはならない旨が書かれています。

一方、mod_cacheにはCacheIgnoreNoLastModという、上記のRFC2616の項をいい感じに無視してくれそうなディレクティブが用意されています。しかし、これを有効にした場合には、通常の「ETag、Last-Modfied、Expiresヘッダのいずれか一つを持ち、クエリ文字列のない」URLに加え、「ETag、Last-Modfied、Expiresヘッダのいずれもないが、クエリ文字列のない」URLのコンテンツもキャッシュされるようになりますが、「ETag、Last-Modfied、Expiresヘッダのいずれもなく、クエリ文字列がある」URLは依然としてキャッシュされません。

結局のところ、mod_cacheにキャッシュしてもらうには、mt-search.cgiが「ETag、Last-Modfied、Expiresヘッダのいずれか」を返答するように変更する必要があるということです。と言っても難しい変更ではなく、MT 3.34を例にとると以下の小変更で済みます。

--- lib/MT/Bootstrap.pm.bak	Wed Jan 10 12:11:30 2007
+++ lib/MT/Bootstrap.pm	Mon Feb 12 02:12:43 2007
@@ -63,11 +63,15 @@
                     local $SIG{__WARN__} = sub { $app->trace($_[0]) };
                     MT->set_instance($app);
                     $app->init_request(CGIObject => $cgi);
+                    $app->set_header('Expires' => '+1h')
+                        if $class eq 'MT::App::Search';
                     $app->run;
                 }
             } else {
                 $app = $class->new( %param ) or die $class->errstr;
                 local $SIG{__WARN__} = sub { $app->trace($_[0]) };
+                $app->set_header('Expires' => '+1h')
+                    if $class eq 'MT::App::Search';
                 $app->run;
             }
         };

この例ではキャッシュの有効期間を1時間に設定していますが、お好みで変更するとよいでしょう。

性能は?

試しに100回ほど問い合わせてみました。

キャッシュなしの場合:

$ ab -n 100 'http://example.org/MTDIR/mt-search.cgi?tag=x&blog_id=1'
(snipped)

Server Software:        Apache/2.2.4
Server Hostname:        example.org
Server Port:            80

Document Path:          /MTDIR/mt-search.cgi?tag=x&blog_id=1
Document Length:        3877 bytes

Concurrency Level:      1
Time taken for tests:   41.432825 seconds
Complete requests:      100
Failed requests:        0
Write errors:           0
Total transferred:      423600 bytes
HTML transferred:       387700 bytes
Requests per second:    2.41 [#/sec] (mean)
Time per request:       414.328 [ms] (mean)
Time per request:       414.328 [ms] (mean, across all concurrent requests)
Transfer rate:          9.97 [Kbytes/sec] received

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    0   0.0      0       0
Processing:   404  413  22.5    410     598
Waiting:      399  408  22.5    404     592
Total:        404  413  22.5    410     598

Percentage of the requests served within a certain time (ms)
  50%    410
  66%    410
  75%    410
  80%    410
  90%    414
  95%    415
  98%    516
  99%    598
 100%    598 (longest request)

キャッシュありの場合:

$ ab -n 100 'http://example.org/MTDIR/mt-search.cgi?tag=x&blog_id=1'
(snipped)

Server Software:        Apache/2.2.4
Server Hostname:        example.org
Server Port:            80

Document Path:          /MTDIR/mt-search.cgi?tag=x&blog_id=1
Document Length:        3877 bytes

Concurrency Level:      1
Time taken for tests:   0.487676 seconds
Complete requests:      100
Failed requests:        0
Write errors:           0
Total transferred:      426592 bytes
HTML transferred:       387700 bytes
Requests per second:    205.05 [#/sec] (mean)
Time per request:       4.877 [ms] (mean)
Time per request:       4.877 [ms] (mean, across all concurrent requests)
Transfer rate:          853.03 [Kbytes/sec] received

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    0   0.0      0       0
Processing:     0    4  41.1      0     411
Waiting:        0    4  41.0      0     410
Total:          0    4  41.1      0     411

Percentage of the requests served within a certain time (ms)
  50%      0
  66%      0
  75%      0
  80%      0
  90%      0
  95%      0
  98%      0
  99%    411
 100%    411 (longest request)

41.43秒から0.49秒に高速化!!!

もう少しちゃんと見ると、キャッシュありの方は最初の一回目のリクエストに411msecかかっていて、残りの99回分のリクエストには77msecしか要していません。したがって、キャッシュヒット時には500倍以上速くなっているということです。条件によって結果はいろいろ変わってきますけどね。

ついでなので、FastCGI (mod_fcgid)でキャッシュあり・なしのデータも追加しておきます。

FastCGI (mod_fcgid) + キャッシュなしの場合:

$ ab -n 100 'http://example.org/MTDIR/mt-search.fcgi?tag=x&blog_id=1'
(snipped)

Server Software:        Apache/2.2.4
Server Hostname:        example.org
Server Port:            80

Document Path:          /MTDIR/mt-search.fcgi?tag=x&blog_id=1
Document Length:        3877 bytes

Concurrency Level:      1
Time taken for tests:   2.945091 seconds
Complete requests:      100
Failed requests:        0
Write errors:           0
Total transferred:      419600 bytes
HTML transferred:       387700 bytes
Requests per second:    33.95 [#/sec] (mean)
Time per request:       29.451 [ms] (mean)
Time per request:       29.451 [ms] (mean, across all concurrent requests)
Transfer rate:          138.88 [Kbytes/sec] received

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    0   0.0      0       0
Processing:    29   29   1.3     29      42
Waiting:       28   28   1.4     28      42
Total:         29   29   1.3     29      42

Percentage of the requests served within a certain time (ms)
  50%     29
  66%     29
  75%     29
  80%     29
  90%     29
  95%     29
  98%     29
  99%     42
 100%     42 (longest request)

FastCGI (mod_fcgid) + キャッシュありの場合:

$ ab -n 100 'http://example.org/MTDIR/mt-search.fcgi?tag=x&blog_id=1'
(snipped)

Server Software:        Apache/2.2.4
Server Hostname:        example.org
Server Port:            80

Document Path:          /MTDIR/mt-search.fcgi?tag=x&blog_id=1
Document Length:        3877 bytes

Concurrency Level:      1
Time taken for tests:   0.107752 seconds
Complete requests:      100
Failed requests:        0
Write errors:           0
Total transferred:      426570 bytes
HTML transferred:       387700 bytes
Requests per second:    928.06 [#/sec] (mean)
Time per request:       1.078 [ms] (mean)
Time per request:       1.078 [ms] (mean, across all concurrent requests)
Transfer rate:          3860.72 [Kbytes/sec] received

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    0   0.0      0       0
Processing:     0    0   3.0      0      30
Waiting:        0    0   2.9      0      29
Total:          0    0   3.0      0      30

Percentage of the requests served within a certain time (ms)
  50%      0
  66%      0
  75%      0
  80%      0
  90%      0
  95%      0
  98%      0
  99%     30
 100%     30 (longest request)

まとめると私の環境でのmt-search.cgiの応答時間は、キャッシュヒット時には約0.8msec、キャッシュミス時にはCGI版で約400msec、FastCGI版で約30msec、ということになります。念のため、キャッシュミス時の応答時間はアプリケーションにも依存しますし、キャッシュヒット時の応答時間はキャッシュしているデータのサイズに依存します。

また、このエントリーで書いたmod_cacheを使った高速化手法は、mt-search.cgi以外の任意のアプリケーションに適用可能です。冒頭でも触れましたが、比較的ルーズなコンシステンシを実現すればいいようなコンテンツ配信の高速化には絶大な効果がありますね。

Feb 9, 2007

Trac野郎はじめました。

Movable Typeプラグインやらいろいろなツールをcode.as-is.netで公開してきましたが、ついに私もTrac野郎になりました。

もともとViewVCMoinMoinを組み合わせて使っていましたが、どちらもさっぱりアップデートがないのでつまらなくなってきてしまったので。

特に設定に悩むようなところはありませんでしたが、私はMT関係とそれ以外用にsubversionリポジトリを分けていたのでそれを一個にまとめる必要がありました。Tracプロジェクトを2つに分ければ一個にまとめる必要もないのですが、両方メンテナンスするのは明らかに面倒すぎますから。

subversionリポジトリのマージは、同じ名前のディレクトリがなければ以下のようにしてしまえばいいだけです。

$ svnadmin create [new-repository-path]
$ svnadmin dump --incremental --quiet [old-repository-path1] > dumpfile
$ svnadmin dump --incremental --quiet [old-repository-path2] >> dumpfile
$ svnadmin load [new-repository-path] < dumpfile

私の場合、同名のディレクトリがあったので実はかなり苦労しましたが、その方法を思い出すといやな汗が出てくるので書きません。ちなみにリポジトリを上の方法でマージすると、リビジョン番号が日時順と一致しなくなりますが、細かいことは気にしない方針で。コミッターは私しかいませんし、過去のchangesetなんて大抵役に立ちません。

というわけで、プラグインなどへの改善意見などは下記でも受け付けております。

New Ticket - Ogawa::Code - Trac

追記: やっぱりスパムが来るんだなあ。とりあえずanonymousではticket作れないようにした。OpenID認証するようにしようっと。

ちなみにhttpd.confには以下のように書いてあります。

# Trac
<IfModule mod_python.c>
    <Location /project1>
        SetHandler mod_python
        PythonHandler trac.web.modpython_frontend
        PythonOption TracEnvParentDir /var/www/projects
        PythonOption TracUriRoot /
    </Location>
</IfModule>
<IfModule !mod_python.c>
    <Directory /usr/local/share/trac/cgi-bin>
        AllowOverride None
        Options None
        Order allow,deny
        Allow from all
    </Directory>
    <IfModule mod_fcgid.c>
        ScriptAlias /project1 /usr/local/share/trac/cgi-bin/trac-project1.fcgi
    </IfModule>
    <IfModule !mod_fcgid.c>
        ScriptAlias /project1 /usr/local/share/trac/cgi-bin/trac-project1.cgi
    </IfModule>
</IfModule>
 
# Trac authentication
<Location "/*/login">
    Require valid-user
    AuthType Digest
    AuthName "TracAuthRealm"
    AuthUserFile /var/www/auth/trac.digest
</Location>

肝は、URLのルートにプロジェクトディレクトリを配したいが、URLのルートにはrobots.txtなどを必要に応じて配したい、ということ。

mod_pythonを使う場合には、新たにプロジェクト(project2)が追加された場合でも「Location /public」の部分を「LocationMatch "/(project1|project2)"」のように変更するだけで済みます。

mod_fcgid、mod_cgiを使う場合には、厄介なことに複数のインスタンスにせざるを得ないでしょう。プロジェクトごとにブートストラップ用のスクリプトを用意し、「ScriptAlias /project2 /usr/local/share/trac/cgi-bin/trac-project2.fcgi」を追加していくことになります。

Feb 6, 2007

どこでもConvert Line Breaksするための Tips

Rightfields でも改行したい! - greenplastic.net

convert_breaksはフィルターではなく、特定のMTタグが持っているオプションに過ぎませんから、MTExtraFieldValueに与えても機能しません。

解決方法は2つあって、ひとつは下のようなグローバルフィルタを提供する極小プラグインを使う方法。

package MT::Plugin::ConvertBreaksAnywhere;
use strict;
use MT 3.3;
use base qw(MT::Plugin);
 
my $plugin = __PACKAGE__->new({
  name => 'ConvertBreaksAnywhere',
  global_filters => {
    'convert_breaks_anywhere' => sub { MT::Util::html_text_transform($_[0]) }
  }
});
MT->add_plugin($plugin);

このプラグインを使えば、どんなMTタグでもフィルタを適用することができます。

<$MTExtraFieldValue field="movie_introduction" convert_breaks_anywhere="1"$>

もうひとつは、実はMovable Typeでは「__default__」という名前のテキストフィルタが定義されていて、その中身はConvert Line Breaksそのものになっているので、それを利用するという方法。上のプラグインを使わずに

<$MTExtraFieldValue field="movie_introduction" filters="__default__"$>

と書くことができます。こっちの方法はプラグインいらずで多分ダイナミックパブリッシングの場合にも使えますが、ドキュメントされていない仕様です。

Feb 4, 2007

DeleteAndRebuild Plugin公開

Movable Typeで公開状態にあるエントリーを削除したときに依存関係のあるアーカイブの再構築が自動的に行われない(全再構築を行わなくてはならない)という問題が某所で指摘されていた。

そんなに過去のエントリーを削除する機会があるのかというのはさておき、下のような超簡単なプラグインを用意しさえすれば、エントリーを削除したときに関連するアーカイブの再構築が自動的に行える。

DeleteAndRebuild - ogawa - エントリーの削除時に関連するアーカイブの再構築を自動的に行うプラグイン。 - Google Code


以下は蛇足。

それはそれとしてもうちょっとだけ深い話をすると、MTではMTEntryNext, MTEntryPrevious, MTArchiveNext, MTArchivePreviousをネストしたテンプレートを書くことができる。例えば、個別エントリーアーカイブで以下のように書ける。

<ul>
<MTEntryPrevious>
  <MTEntryPrevious>
    <MTEntryPrevious>
      <li>3つ前: <a href="<$MTEntryPermalink$>"><$MTEntryTitle$></a></li>
    </MTEntryPrevious>
    <li>2つ前: <a href="<$MTEntryPermalink$>"><$MTEntryTitle$></a></li>
  </MTEntryPrevious>
  <li>1つ前: <a href="<$MTEntryPermalink$>"><$MTEntryTitle$></a></li>
</MTEntryPrevious>
</ul>
 
<ul>
<MTEntryNext>
  <li>1つ後: <a href="<$MTEntryPermalink$>"><$MTEntryTitle$></a></li>
  <MTEntryNext>
    <li>2つ後: <a href="<$MTEntryPermalink$>"><$MTEntryTitle$></a></li>
    <MTEntryNext>
      <li>3つ後: <a href="<$MTEntryPermalink$>"><$MTEntryTitle$></a></li>
    </MTEntryNext>
  </MTEntryNext>
</MTEntryNext>
</ul>

このネストはいくらでも深くすることができる。他にも例えば、個別エントリーアーカイブにそのエントリーが属するカテゴリーに含まれる他のエントリーの一覧を生成させることもできる。

何が言いたいかというと、MTの「テンプレート言語」がこのような性質(=つまりは容易に非定型な依存関係を作り出せるという性質)の「言語」である以上、エントリーに何らかの変更を行った場合に厳密にすべての依存関係をconsistentに反映するには、テンプレート「プログラム」の依存性解析をしない限り、全再構築するしか方法がないということである。

これに対して、新規エントリーを投稿したときなどに自動的に行われる(部分的な)再構築や前掲のプラグインが実現する(部分的な)再構築は、インデックステンプレートと、そのエントリーの前後のエントリーと、そのエントリーの属している日時別アーカイブ・カテゴリーアーカイブしかその対象としない。だから実は不完全である。例えば、上記のテンプレート片を含む個別エントリーアーカイブがあった場合にはエントリーを削除したときにconsistentに更新されないアーカイブが存在することになる。要は不完全ではあっても実用上は問題ないだろうという判断に基づいてある種の最適化・省力化をしているに過ぎないのだ。

では、静的生成ってダメダメじゃんやっぱりダイナミックパブリッシングがいいんじゃんということになるかというと、それはまったく違う。というのも、ダイナミックパブリッシングは静的生成時にバッチ的を行うレンダリング処理をユーザのページリクエスト時にオフロードするだけだからだ。

もしキャッシュがなくリクエストの度にレンダリングを行うのならば、そのオーバーヘッドの総計は静的生成の場合を容易に上回り得る。もしキャッシュがあり最初のリクエスト時のレンダリング結果を再利用するのならば、エントリーへの変更がなされる度にすべてのキャッシュは正しく無効化されなくてはならない。後者の場合には平均的に見て全再構築するのと同程度の処理コストを払わねばならない。処理が時間的に分散されることのメリットを強調できるかもしれないが、それならば静的生成でもRebuildQueueのようなエフォートがある。RebuildQueueでは、再構築処理を時間的にも空間的にも分散して実行するフレームワークを提供しているようだ。

この話はまともなテンプレートエンジンを備えていない(というかPHPのテンプレート機能をそのまま使う)Wordpressでもほとんどそのまま当てはまると思う。コードの抽象化不足と私自身の忍耐不足のせいもあって、Wordpressのコードをちゃんと読む機会を逸し続けているのだけど、何か特別な魔法があるとは考えにくい、それだけは間違いない。

Feb 3, 2007

Google Tokyoの技術講演会に参加したので簡単コメント

昨日のことですがセルリアンタワーでやっていたGoogle Tokyoの技術講演会に参加してきました。まあ割と面白かったと思います。質疑時間が短すぎてちゃんと質問できなかったのでコメントがてら書いておきます。

南野さんのtalk:

  • 世界中で単一のエンジニアリングチームがすべての設計文書・ソースなどを共有していることに関して。アクセスコントロールを考慮しなくて良いことは事務的コストの削減に大いに役立つが、実際にエンジニア・インターンがsingle point of failureになり得る。NDAを結ぶから平気というのもおかしな話で、個人に負わせ得る責任にはおのずと限界があるのであって、全情報の流出による損失がそれを上回るのであれば対策が必要なのは明らかではないか。まだ流出したことがないというので今後の展開を待とう。
  • サービスの継続性に関して。短期的なゴールを定めて企業やエンジニアの行動を決めるのは結構。そんなのはどの会社でもやっている。そうではなくて長期的なサービスの継続性を実現するための具体的な方策を持っているのか。実際に現在のメールサービスが50年続くというのはもちろん冗談だが、実用的に意味のあるスパンに渡ってサービスし続けられるという確証をユーザに示す必要があると思う(エンジニアが短期的なゴールを強調すればするほど)。

Derrickさんのtalk:

  • Web検索のデータの規模が2万人のGmailのデータの規模にも満たないというのは興味深い話。メタファーで話すと、全世界の図書館にある全書籍の文字数より日々交わされる会話に費やされる文字数の累積の方が大きいのは当然のことだが、スケールとしてはなかなか掴みにくい。GmailとWebの比較は端的なインスタンスを示してもらったようなものだ。究極的にGoogleが奉仕すべき対象人数というのはいったいどれくらいなのだろうか。2000万? 2億? 20億?
  • Google Mailの検索はロジックとしてはWeb検索と違わないのかもしれないが、その性能には大いに不満が残る。いったいなぜなのだろうか。素朴に思いつくのは、メールデータではWebと違ってconversationで定義できるようなルーズなグループ構造しか存在しないためアルゴリズム的に検索精度を上げる既存の試みの一部が役に立たないことと、検索結果の解釈に関して認知科学的な要因が無視あるいは排除できないことである。後者についてもう少し述べると、ユーザの曖昧な検索要求に対して検索エンジンが与える結果がどうあれ、ユーザは自分の所有している(と信じている)メールデータに対する検索においては、(自分が所有しているわけではない)Webデータの検索に比べて、より正確で漏れがなく完全な検索結果を求める傾向があり、簡単には満足しないように思えるということである。Googleのエフォートはこうした問題を解決するのだろうか。
  • Deep searchの可能性について。例えばメッセージに含まれるURLの指し示す内容を検索対象に含めたり、メッセージの重み付けに使用したりすることが可能だと思うが、そうしたことは検討されているか。

工藤さんのtalk:

  • Bigtableの説明はかなり端折られていたせいもあってよく理解できなかった部分もあるが、要はGFS分散ファイルシステム上へのLog structured merge-tree (LSM-tree)の一実装である、という理解でよいのか。また、Bigtableの提供するrowに対する自動負荷分散機能は本当に有効なのか。通常のアプリケーションでどれくらいの不均衡が生じ得、それがこれだけ均衡化されてefficientになるんだよというevidenceを示してくれないと分からない(これは私が職業的にそう思っちゃうだけ?)。このあたりは論文を読むと分かるのかな。
  • Underlying layerのabstractionをシンプルに保つのはまったくもって賛同。んで、ハードウェア化を考えるような人材は?

私としては、もうちょっと技術的に突っ込んだ話をしてくれてもいいかなという印象でした。こうしたオープンハウス的な催しが継続されることを期待しています。

About Me

My Photo

つくばで働く研究者

Total Pageviews

Amazon

Copyright 2012 Ogawa::Buzz | Powered by Blogger
Design by Web2feel | Blogger Template by NewBloggerThemes.com