T

当社比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もの動的コンテンツへのリクエストを受けるといった無謀な環境で大冒険している人は、こんなことを検討する必要はこれっぽっちもない。手元の中古のデスクトップを抱えてデータセンタに駆けつけてケーブルを引っこ抜いて差し替えて、全部のリクエストに「ごめんね」と返すだけの設定が完了したら、あとはなるべく遠くまで逃げることだ。追いかけてくる連中も、そんなに賢いことはあり得ないので、深刻に気に病むこともない。

コメントを残す