当社比100倍のススメ
ウェブ開発の世界で、コードや設計の見直しが必要なのに、あまり時間や労力を割いてもらえないのが、速度の問題である。セキュリティなら見直しどころかとにかく素早く確実に対処する必要があるわけで、これは有無をいわさず誰もが取り掛かるであろうが(そうでなければ転職するべき)、遅いソフトウェアについては、まあ動いてはいるわけだし、もっと優れたハードウェアに載せ替えることで対応できるならそちらを選択するのも現実的だと考えられがちだ。高速化なんて、実際やってみても成果が上がるとは限らず、また開発者も無理な日程でリリースしてきたコードを今更また眺めるのは苦痛でしかない。いっそ書き直してやるならまだしも、あれやこれやと最適化するのは考えるだけでも面倒くさい。
しかし、実際には遅いウェブサイトは機会損失であり、いくら昔と比べてハードウェアが安価になり、仮想化技術が発達したとはいえ、それだって無料だったり無限に増設可能なわけではない。PCサーバ1台の増設をサービスのプロデューサに納得してもらうのは結構難しいことだ。仮想環境だってたくさん立ち上げるにはそれなりの実環境が必要になる。不景気の中で運用コストが上がる解決方法しか提示できないのは競争力に問題があるといわざるをえない。よって、スケールアウトは常に正しい解決ではない。
もちろん、最初から最大限に速度を追求してシステムを組むのは素晴らしいことで、誰もが実践するべきだ。しかし、そんなに簡単なことなら速度が問題になることなどあり得ないわけで、実際にはこの点を追求し過ぎるとリリースしなければいけない期日を守るのがとても難しくなってしまう。遅いのも機会損失だが、リリースできなければ機会が存在さえしなくなってしまうので、エンジニアリング過多は根治されるべき疾患だ。
期末が近づいてきたので、この2年くらいの自分の仕事をざっと整理してみたのだが、一昨年の暮れから今年の始まりにかけて、よく考えたらずっと既存のソフトウェアの高速化ばかりやってきた気がする。最初の立ち上げから1年以上経過したサービスがとうとう音を上げてしまったこともあれば、作ったばかりなのに社内の別チームから要求された速度が到底達成不可能だったので急遽なんとかしなければいけなくなったケースもある。恨み言をいわせてもらえば、ハードウェアやシステム構成の選定は当の検品屋連中だったりするので、そいつらの見積りが甘いのが原因なのだが、お客さんの求めるものがあってのソフトウェアであるわけで、理不尽だと叫んでもプログラムは速くはなってくれない。
そんなとき、誰もが思いつくのは、出力データや処理結果をキャッシュして速度を稼ぐ方法だろう。ご多分に漏れず自分もだいたいいつもそうやってきた。ウェブ上のプログラムには、大変ありがたいことにエントリーポイントがひとつしかない。そう、リクエストを受け付ける場所だ。出所の同じHTTPのリクエストは同時に一ヶ所にしか届かない。そこから、中間の処理があって、出力先は最終的にはリクエストを送ってきたクライアントになる。
前提条件 => 処理実行 => 処理結果
ネットワークの遅延や巨大なレスポンスによるクライアント側の負担といった問題は、普通はそんなには起きない。単純に考えると、結局はウェブ上のサービスはリクエストという前提条件があり、ソフトウェアによる処理があり、レスポンスという処理結果があるだけだ。
ということは、前提条件と処理結果の関係を把握して、その近道を探すのが高速化の第一歩ということになる。ここでのチェックポイントは
「前提条件(リクエスト)と処理結果(レスポンス)の関係」
の検討だ。自分で作業する場合は、まず
(1)リクエストをグループ化することができるか
を考える。メソッド(GETや、POSTときどきDELETEやPUTみたいな動詞)、リクエストURI、クエリで処理結果が同じになるグループを作成することができれば近道は見えたようなものだ。
例:GETのみ、docomoのゲートウェイを通過、FOMA端末、/listsへのアクセス、クエリは「type=used&maker=toyota」であればレスポンスは共通
この場合、リクエストを受け付けた際に既に同じレスポンスを返したかどうか判定して、していなければ通常の処理を実行してキャッシュを作成、キャッシュが作成済みであれば処理をすっ飛ばしてキャッシュからデータを返す、という単純な対応が可能かもしれない。キャッシュの実装内容はシステムのI/O負荷により異なるだろうが、そこは後で検討すればいい。ストラテジーパターンで変更できるようにするとか、まあいろいろ手はある。
そこで問題になるのが、動的コンテンツを提供する場合、まあ当然ながら静的なコンテンツで速度が問題になるのはほとんどあり得ないだろうから当たり前だが、レスポンスとして返されるコンテンツの内容の更新頻度だ。リクエスト毎に内容が変わるのであれば、単純なキャッシュでは対応できない。そこで
(2)更新頻度のグループ化
について検討することになる。いまどきカウンタなんか載せているサイトは絶滅しているだろうが、それでも刻一刻と在庫状況が変化したり、コメント数が増えていくようなコンテンツはたくさんある。
ページ全体が同じ頻度で更新されることは稀だろう。同じリクエストとみなしていいグループに常に新しいContent-typeを返すことはあり得ないが、オークションの入札状況のようにサーバ側のリソースに変化があればそれを常に反映する必要のある部分もあれば、人気の検索語のように1時間くらいに1回更新しても誰も気づかないようなものもある。
例:更新頻度最大 = 入札状況、更新頻度中程度 = 残り3時間以内の商品、更新頻度低 = ホットな検索語
更新頻度のグループがあまりに細分化されている場合は、運用者とよく話し合って、可能な限り単純化していくことが必要になる。サービスの品質に影響がない程度に、できれば3パターンくらいになるとちょうどいい。
3パターンくらいであれば、キャッシュした全体のデータに部分的なキャッシュを組み合わせてレスポンスを作り上げるだけのプログラムをリクエストを受けた場所で処理して済ませることができる。
それから、もっとサーバに楽をさせてあげたいときは
(3)クライアント側に処理を肩代わりできるか
を検討する。PC向けサービスであればクッキーに格納できるデータもあるだろう。JavaScriptにあらかじめ呼び出されそうなデータを格納してしまってもいい。検索候補を表示するのに正直にデータを毎回取得したりせず、サーバ側からは定期的に更新するだけのデータをブラウザ側に先に送っておけば、毎回非同期通信が走ることもない。
もちろん、第三者からアクセスされてはいけないデータもあるので、なんでもかんでもこのやり方が正しいわけではないが、なんでもかんでもサーバ側で処理するのも同様に正しくはない。でも、推測不可能なハッシュ値を格納しておいてそれをキーとしてmemcachedにデータを探しに行って、なければDBに問い合わせるといった対応も可能だ。
3はちょっと話が逸れたが、1と2は「前提条件(リクエスト)と処理結果(レスポンス)の関係」の検討だ。これらがクリアになれば、途中の処理をどれだけ端折ってレスポンスを作成することができるか考えるのはそんなに難しいことではなくなる。
そうなると次にやるべきことは
「処理実行の副作用への対処」
ということになる。アクセスログのように勝手にサーバ側で実行される副作用なら深く考えることもないだろうが(カスタム形式に必要な処理があれば別だ)、検索履歴の保存やアフィリエイトからのアクセスの記録など、サーバ側に直接レスポンスとは関係のない副作用が生じるケースがあれば、それに対応しておかないとサービスには深刻な影響がある。関数型言語をかじったことのある人なら、ある関数が副作用を持つことで前提条件と結果の関係だけで安心することができないことを充分教育されているだろうから、この副作用というものについては手続型言語しかやったことのない人に比べれば敏感かもしれない。
というわけで、単純な考察に相応しい単純な結論として、高速化が可能なプログラムを作るには、上の手続きの反対をまず心がけることだ。リクエストがグループ化可能なレベルになるように要件を定義して、出力するコンテンツの更新頻度をなるべく単純なグループに分類可能に保ち、それから副作用に慎重になる。RESTfulなサービスにする意味はリクエストのグループ化に貢献することにある。更新頻度の単純化と副作用を最小限にすることはキャッシュ戦略を容易にしてくれる。アルゴリズムやプログラミング手法で高速化することも非常に重要であり、キャッシュは万能ではないが、ウェブサイトを100倍速くしたいなら、まずはこんなところから手をつけてみるといいのではないかと思われる。
当然ながら、RAMが128MBでCPUがCeleronの500MHzというサーバで1秒間に100もの動的コンテンツへのリクエストを受けるといった無謀な環境で大冒険している人は、こんなことを検討する必要はこれっぽっちもない。手元の中古のデスクトップを抱えてデータセンタに駆けつけてケーブルを引っこ抜いて差し替えて、全部のリクエストに「ごめんね」と返すだけの設定が完了したら、あとはなるべく遠くまで逃げることだ。追いかけてくる連中も、そんなに賢いことはあり得ないので、深刻に気に病むこともない。
PubsubSearch
このサイトの右上の検索ボックスに手を入れて、Ferret + Rubyで全文検索するようにした。Pubsubhubbubを使って更新内容がPOSTリクエストとしてHubに送信されるので、Ferretの検索インデックスはHubからのPOSTを受け付けるSubscriberが更新する。
サイト更新 --POST--> Pubsubhubbub --POST--> Ferretのインデックス更新
で?といわれたら、それ以上でも以下でもないとしかいいようがない。しかも小っ恥ずかしいことに新規追加にバグがあったので直した。
ソーシャルネットワークのなりすましテクニック
ブルース・シュナイアーの最近のエッセイに、ソーシャルネットワークのなり済ましテクニックが紹介されていた。
ソーシャルネットワークを使って成りすましをやってのける方法という記事があるらしく、シュナイアーのエッセイはその紹介なのだが、手口は次の通り(元記事ではMySpaceとFacebookだけど日本なのでmixiとgreeにしよう):
・適当な人にmixiでフレンドリクエストを送る
・フレンド登録してくれる人がいたらgreeでその人たちを探す
・greeでも見つかった人がいたら、そこでもフレンドリクエストを送る
これで準備完了。フレンドリクエストを送った相手が犠牲者になる。
・犠牲者のmixiとgreeの友達リストを見比べて片方にしか登録していない人を探す
・見つかったら、その人の写真とプロフィールをコピーして新規にアカウントを作る
・作ったアカウントから犠牲者にフレンドリクエストを送る
これで、かなり信憑性のある偽アカウントが出来上がる。あとは詐欺師のアイデア次第。
「今度飲みに行こうよ!18日に新宿とかどう?」で、やって来たところをボコボコにして金品を奪うとか。
確かに、複数のソーシャルネットワークを使っているとこれは引っかかりそうだ。気をつけないとなあ。
ステルス・サーチエンジン
何をしているのかはわからないけど、ステルス検索というサービスをやっているらしいBlekkoという会社があるようだ。
dmesgに怪しげなログが残っていた。普通、DoS攻撃とかで残るようなログなので、それにしてはヌルい回数だからおかしいなとIPアドレスを調べてみたら、Blekkoという組織からのアクセスだった。
TCP: Treason uncloaked! Peer 38.108.180.137:31690/80 shrinks window 数字:ずらずら. Repaired.
Blekkoとは:
$ whois 38.108.180.133 [Querying whois.arin.net] [Redirected to rwhois.cogentco.com:4321] [Querying rwhois.cogentco.com] [rwhois.cogentco.com] %rwhois V-1.5:0010b0:00 rwhois.cogentco.com 38.108.180.133 network:ID:NET-266CB40018 network:Network-Name:NET-266CB40018 network:IP-Network:38.108.180.0/24 network:Postal-Code:94065 network:State:CA network:City:Redwood City network:Street-Address:100 Marine Parkway network:Org-Name:Blekko network:Tech-Contact:ZC108-ARIN network:Updated:2008-09-17 10:42:07 network:Updated-by:John Knowles %ok
ステルス・サーチというくらいだから、なるほどackを返さないとかやってるみたいだ。とりあえず実害はないので気にするのはやめる。
Yahoo Japan APIの文章校正を試す
というわけで、先日遊んだ形態素解析に引き続き、今度はYahoo Japanの校正支援を使ったデモを作って遊んでみた。APIの詳細はこちら。
まだServices_Yahoo_JPには校正支援用の機能は実装されていないようだったので、形態素解析の中を見てちょろっと変えたらすぐに動いた。問題ないようなら後で投稿しておこう。
単純なAPIなので、filterを指定してあとはレスポンスを待つだけ。
<?php require_once 'Services/Yahoo/JP/V1.php'; try{ $yahoo = Services_Yahoo_JP_V1::factory('kousei'); $yahoo->withAppID($app_id); $yahoo->setSentence($keyword); $yahoo->setFilter('1,2,3'); $result = $yahoo->submit(); } catch(Services_Yahoo_Exception $e){ $error = '接続に失敗したような気がします。'; } $xml =& $result->xml; if(count($xml)){ foreach($xml as $k => $row){ print_r($row); } }else{ print('正しい日本語みたいです。'); } ?>
こんな感じで実装できる。あんまりYahoo側の仕様がわかっていないから、とりあえず動いたところで放置している。
Yahoo JapanのAPI、まめな病人
ブスコバンのアレルギーらしく、健康診断が終わってからずっとひどい頭痛だ。今日はちょっと家で休んでいる。
Yahoo JapanのAPI用に既存のPEARパッケージを拡張したServices_Yahoo_JPがある(作者サイト)。面白そうなのでさっそく使ってみた。
テキスト解析はこんな風に使える。例えばテキストの読みがなを取得する。
<?php require_once 'Services/Yahoo/JP/MA.php'; $keyword = 'これは日本語です。'; $app_id = 'Your APP ID'; try{ $yahoo = Services_Yahoo_JP_MA::factory('parse'); $yahoo->withAppID($app_id); $yahoo->setSentence($keyword); $result = $yahoo->submit(); } catch(Services_Yahoo_Exception $e){ $error = 'すいません、YahooJapanに接続できませんでした。'; } if($result){ $xml =& $result->xml->ma_result->word_list->word; foreach($xml as $key => $val){ print($val->reading . "\n"); } } ?>
なるほど。で、何か面白いことでもできないかと考えたが、Yahooから戻ってくる読みがなをさらにローマ字に変換して、Mac OS Xに付属のsayコマンドで読み上げてみるのはどうだろう。
$ say -o 出力ファイル "読み上げるテキスト"
上の書式で変換できるので、フロントエンドのPHPと連携してみれば簡単だ。
そこでふと思いついた。sayコマンドは声の指定も可能なのだが、プリセットされたデータには歌いながらしゃべるパターンもあったはずだ。それを指定すれば、入力したテキストを歌うこともできるじゃないか。
$ say -o 出力ファイル -v "声のパターン名" "読み上げるテキスト"
そうと決まれば話は早い。というわけでデモを作成した。(Proof of conceptということで無茶なことはしないでくださいね)
テキストが入力されると、バックエンドのMacBookにHTTPで通信してプロシージャを呼び出し、MacBook側はテキストを元に音声ファイルを作成してMP3に変換、最終的に作られたファイルをDocmentRoot以下に設置してファイルのパスを返す。フロントエンドのウェブサーバにリバースプロキシでMacBookのローカルファイルにアクセス可能にしてあげていっちょあがり。文字で書くとややこしいが、図にするといたって単純。
追記:Yahoo JapanのAPI側で数字の読み方は返してくれないみたいだ。
37signalsのシステム構成
37signalsといえば、Getting Realにこんな一節がある:
まだ発生してもいない問題のために時間を費やすな
ユーザーが10万人に達するのに2年かかると予想されるのに、今日10万人のユーザーを想定する必要があるのだろうか?今は3人のプログラマーがいれば事足りるのに、8人を雇う必要があるだろうか?
今は2台のサーバーでまかなえているのに、12台を準備する意味があるのだろうか?
とにかく進めよう
起こってもいない問題の解決方法を考えるために、人は時間を費やしがちです。そんなことに意味があるか! 私達がBasecampを開始したときは、顧客に請求書を送る手はずすら整っていませんでした。しかし1ヶ月サイクルで請求を行うことになっていたので、(サービスが開始してから)30日間の猶予があることはわかっていました。もっと緊急な問題を解決するためにサービス開始までの時間を費やし、私達が請求書の問題に取り組み始めたのはサービス開始後でした。 請求書の発行はうまくいきました(それにこのおかげで、シンプルな解決策をとることができたわけです)。
そして、いまや同社のサービスはBasecampのアカウントが200万、ユーザのアップロードしたファイルが5.9テラバイトという規模になった。つまり、問題は発生したわけで、じゃあどんな対応をしたのかというのが上の記事なわけだ。そう考えるとなんだか興味深い。
Services_Amazon、SimpleDB
PEARのServices_Amazonが更新されてHTTP Proxyに対応していたのでさっそくアップデートした。それにしても、この更新がwarlus経由だったとは知らなかった。パッチを投げてるし偉いなあ。
ところで、アマゾンといえばSimpleDBなわけで、いくつかメモ。
PHP Classesにさっそくあがっている。中身未見。
もし、データベースがサービスのコアに関わるのであれば、これをメインに使うのはバカげている。それに、そもそもこれはリレーショナルデータベースですらない。でも、単純にメンテナンスしなくてもいいデータベースが必要なだけだという場合は、高可用性のデータベースを簡単に手に入れることが出来る利点がある。サービス規模が小さければそれなりに、大きくなれば相応の金額で使えるところもいい。