=pod =head1 大規模なeコマースサイトを Apache と mod_perl で構築する B Perrin Harkins (Translated by Tatsuhiko Miyagawa Emiyagawa@cpan.orgE) =head1 よくある神話 大規模な eコマース ウェブサイトの構築となると、アドバイスはそこら中に あります。デベロッパーたちは、C++ や Java (このどちらかは好みで)で作ら れたサイトでなければ、大量のトラフィックを処理できない、と言ってくるこ とでしょう。アプリケーションサーバベンダーは、パッケージングされたオー ルインワンのソフトウェアが必要だ、と主張するでしょう。ハードウェアベン ダーは、大規模サイトの運営には最高級のメガマシンが必要だ、と言ってくる でしょう。本記事では、われわれがどのようにして、主にオープンソースのソ フトウェアや日用ハードウェアを使って大規模 eコマースサイトを構築したか を紹介します。われわれはこれを実現したのです、あなたにも、可能なことで す。 =head1 Perl はセーブする Perl は長らくの間、CGI スクリプトを記述する際の言語として人気がありま した。Perl はラピッドな開発とフレキシビリティを同時に実現しています。 I はいまも O'Reilly のトップセールスを記録し、コミュ ニティのサポートも豊冨です。しかし近頃、Perl は、いくつかの方面からの 攻撃にさらされてきました。シリアスな開発には動作が遅く、Perl で書かれ たコードはメンテナンスしにくい、と中傷する人々がいるのです。 Apache モジュールの C は、Perl のパフォーマンス情勢を劇的に 改善しました。Apache の中に Perl インタプリタを埋め込むことによって、 Java サーブレットと同様のパフォーマンスを得ることができ、大規模なサイ ト構築における優れた選択枝となります。Perl のオブジェクト指向機能を利 用し、基本的なコーディングルールを守ることによって、楽にメンテナンスで き、他の言語に劣らないコードを構築することができるのです。 =head1 アプリケーションサーバの撰択 Apache, mod_perl と CPAN (Comprehensive Perl Archive Network) で入手可 能なオープンソースコードを組み合わせれば、商用アプリケーションサーバと 同様な機能を利用可能です。 =over =item * セッション管理 =item * ロードバランシング =item * 永続データベース接続 =item * 高度なHTMLテンプレート =item * セキュリティ =back それに、商用プロダクトでは得ることができないものもあります。たとえば、 メーリングリストを通じて、デベロップメントチームに直接コンタクトをとっ たり、パッチを待つことなく自分自身で問題を修正することができます。さら に、システムの各パートはあなたの管理下におくことができ、making you limited only by your team's abilities. =head1 ケーススタディ: eToys.com われわれが1999年に eToys にやってきたとき、多くのインターネットのスター トアップ会社に参加したみなさんと、ほとんど同じ経験をしました。システム は MySQL と連係する CGI スクリプトをベースにしていました。スタティック なファイルの吐きだしとダイナミックなコンテンツ生成は同じマシンでリソー スを共有していました。CGI のコードはほとんどが Perl4 流のスタイルで書 かれており、モジュラー化されていませんでしたが、小規模のチームが短期間 でつくったのですから、驚くべきことではありませんでした。 われわれの主要なタスクは、クリスマス期に予測されるトラフィックに耐えう るようにシステムをスケールアップする方策を把握することでした。おもちゃ 産業は季節モノで、売上のピーク時と、そうでない時期の差が巨大です。サイ トは前回のクリスマスを耐え切ることができず、MySQL データベースではそれ 以上の規模は無理なようでした。 Oracle への移行はすでにおこなわれ、DBA チームがすでに配置されていまし た。ソフトウェアの再設計をしている時間はなかったため、クリスマスまでに、 パフォーマンスを改善させることはなんでも実行しなくてはなりませんでした。 =head2 Apache::PerlRun による救済 C は CGI から C への移行をスムーズにするた めのモジュールです。CGI環境をシミュレートし、C でコードを書 くことによるメリットの一部(すべてではない)を提供します。このモジュール を使ってデータベースの永続コネクションを C によって実現し、 クリスマスまでに C と Oracle への基本的な移行をクリスマスま でにすませることができました。そして、クリスマスラッシュに備えて新しい ハードウェアの準備もできました。 トラフィックのピークは8週間つづきました。そのほとんどの間、必死になっ ていろんなものを修正し、神経質になりながらなにか異常がおこらないかを待っ ていました。ですが、わたしたちはなんとかやりとげました。その間、以下の ような統計を得ることができました。 =over =item * 60 - 70,000 セッション/時 =item * 800,000 ページビュー/時 =item * 7,000 オーダー(注文)/時 =back Media Metrix によれば、eToys は eBay, Amazon についで3番目にトラフィッ クの多い eコマースサイトでした。 =head1 新たなアーキテクチャの計画 2000年にむけて、再設計をしなければいけないのは明白でした。現状のシステ ムではすでに限界に達しており、先延ばしにしていた困難な問題にとりくむ必 要がありました。 新たなシステムの目標には、オフラインでのページ生成をやめることも含まれ ていました。旧システムでは各製品や製品カテゴリごとのHTMLページをバッチ ジョブで生成し、スタティックなファイルに吐きだしていました。このやり方 は、製品データベースが小規模であれば、スタティックファイルの方がパフォー マンスがよいため、効率的だったでしょう。しかし、その頃サイトに子供用ブッ クストアを追加しており、製品データベースの規模はすごい勢いで大きくなり、 すべてのページを生成するのに必要とされる時間が無視できなくっていました。 よって、顧客がほんとうに興味をもってクリックしたときにページを生成し、 かつ安定したパフォーマンスを維持するような戦略が必要でした。 データベーススキーマの作りなおしや、コードをモジュラー化して、お互いの コードに踏み込むことなく、開発作業をチーム間で共有できるようにする要望 もありました。新たなコードベースは、追加され続ける機能要望にも対応でき るような柔軟性も備えなければならなかったのです。 チームの全員が Perl のオブジェクト指向に豊富な経験を持っていたわけでは なかったため、Randal Schwartz や Damian Conway を招いてトレーニングセッ ションをおこなってもらいました。コーディング標準をいくつか作成し、デザ イン設計をおこない、システムを構築しました。 =head1 2000年のクリスマスを切り抜ける キャパシティの予想では、ピークトラフィックの数字は、前年の3倍でした。 その数字でテストし、結果は大体以下のようになりました。 =over =item * 200,000+ セッション/時 =item * 2,500 万+ ページビュー/時 =item * 20,000+ オーダー/時 =back ソフトウェアは切りぬけることができました。ルータの1個はいかれてしまい ましたが。この年もまた、シーズンでは 3番目にトラフィックに多いe-コマー スサイトとしてレイティングされました。 =head1 アーキテクチャ このシステムのマシン戦略はとてもありふれたものです。安価なIntel ベース のサーバ群、そしてそのフロントにロードバランサを並べ、データベースサー バにコストをかけます。 多くの商用パッケージ同様、フロントエンドの Web サーバ(プロキシサーバと 呼びます)のシステムと、動的にコンテンツを生成するアプリケーションサー バは分離しました。プロキシサーバもアプリケーションサーバもf5 Networks の専用ハードウェアによってロードバランスします。それぞれのシステムの詳 細は以下で説明します。 プロキシとアプリケーションサーバには、C では典型的なプラット フォームである、Linux を撰択しました。Linux ではリモート管理が容易なた め、クラスタリングのアプローチが可能になりました。Linux はセキュリティ や自動ビルドの機能などにより、新たにサーバを追加するのが容易でした。 データベースサーバは IMB の NUMA-Q マシンで、I を走らせてい ます。 =head2 プロキシサーバ プロキシサーバでは、C を組み込まずに、小さなバイナリの Apache を走らせています。この Apache には、いくつかのApache 標準モジュー ルと、セッションの Cookie を発行するカスタムバージョンの I をインストールしてあります。プロセスサイズが小さいため、 1台のマシンで 400 個程度のApache プロセスを走らせることが可能です。こ れらのサーバでは、画像ファイルへのリクエストは自身で制御し、ページへの リクエストはアプリケーションサーバに転送します。アプリケーションサーバ とは通常のHTTPリクエストによって通信し、アプリサーバから、しかるべきヘッ ダが返ってきた際には、ページアウトプットをキャッシュします。キャッシュ されたページは共有された Network Appliance filer 上の NFS パーティショ ンに保存されます。キャッシュからページを返すのはスタティックファイルと 同様、I<とても> 高速です。 このようにリバースプロキシをセットアップするのは、C を利用す る際に、通常推奨されるアプローチです。こうすれば、軽量のプロキシサーバ のプロセスによってコンテンツをクライアント(遅いコネクションの可能性が ある)に返すため、リソースを食う C を解放して、次のリクエスト に移ることができるためです。この設定がどのように役に立つかについての情 報は、C デベロッパーズガイド http://perl.apache.org/guide/ を見てください。 =head2 アプリケーションサーバ アプリケーションサーバでは、C、そしてそれ以外にもいくつかの プロセスを走らせています。I を用いて Perl オブジェクトの ローカルキャッシュを持っています。Web アプリケーションはここで動作し、 HTML テンプレートファイルのような共有リソースは NetApp filer の NFS に マウントされています。このセットアップが相当重いため、これらのマシンは デュアルCPU に 1GB の RAM という豪勢なものになっています。 =head2 検索サーバ さらにもう1つのサーバ群、検索専用のサーバがあります。検索が全体のトラ フィックの大きな割合を占めていたため、それ専用のリソースを用意し、アプ リケーションやデータベースの負荷を減らす価値がありました。 これらのサーバで動作させるソフトウェアは、社内で C++ を利用して開発し た、マルチスレッドのデーモンプロセスです。アプリケーションサーバは、 Perl モジュールを利用して検索サーバと通信します。検索デーモンは検索条 件のセットを受けとり、条件にマッチする製品のオブジェクトIDリストをソー トして返します。アプリケーションサーバは表示する製品のデータをデータベー スから引っ張ってきます。検索サーバは HTML やWeb インターフェースについ てはなにも関知しません。 このように、検索サーバで ID を検索し、次にオブジェクトデータを取得する アプローチは、パフォーマンスが良くないように見えますが、実際にはオブジェ クトデータはデータベースではなく、アプリケーション側のキャッシュから取 得されます。この設計では、データベースと検索サーバ間の重複データを最小 限に抑え、インデックスの再構築を容易にかつ高速におこなうことができます。 また、データベースからプロダクトオブジェクトを取得する Perl コードは、 どのように検索したかには関係ないため、そのまま再利用することができます。 検索デーモンは、標準的な逆引き単語リストのアプローチを使用しています。 インデックスは定期的に Oracle のデータから構築されます。もし Perl です べて実装するソリューションが良ければ、こうしたアプローチを実装したモジュー ルはCPAN にいくつかあります。C や C などです。われわれが独自にこれを実装したのは、 この部分のパフォーマンス要件がシビアで、返り値のIDをソートする規則がか なり複雑であったためです。 =head1 ロードバランスとフェイルオーバー クラスタ間でのロードバランスと、そのうちの1台かそれ以上のノードがダウ ンしていた場合のフォールトトレランスを実現するのには苦労しました。プロ キシサーバはランダム選択アルゴリズムによってバランスされています。ユー ザはリクエスト毎に異なるサーバに辿りつきます。これらのサーバはステート 情報をなにも保持していないため、目標は単に負荷を均一に分散させることに なります。 アプリケーションサーバは "sticky" なロードバランスをおこなっています。 つまり、一度あるユーザが特定のアプリケーションサーバにリクエストした場 合、そのユーザのセッション中の後続のリクエストはすべて同一のアプリサー バに転送されます。f5 ハードウェアは、ブラウザの cookie を利用してこれ を実現しています。 ロードバランサーは定期的にサービスのチェックをおこない、チェックに失敗 したサーバはローテーションから外します。サーバがこけた場合、そのマシン に割り当てられていたユーザはすべて別のマシンに移動されます。 アプリサーバが落ちた際にデータのロスが決して起こらないことを保証するた めに、データの更新はすべてデータベースに蓄積されます。結果として、ショッ ピングカートの中身のようなユーザデータは、アプリサーバのハードウェアの 悲劇的な障害のような場合でも、保護されます。こうしたことは、大規模な e コマースサイトでは必須です。 データベースは別のフェイルオーバシステムを持っていますが、ここでは立ち 入りません。ベンダーによって推奨されている標準的な方法にしたがっている のみです。 =head1 コード構造 コードは、もとは SmallTalk からはじまり、現在はよく Web アプリケーショ ンに適用される、Model-View-Controller パターンによって構造化されていま す。MVC パターンは、アプリケーションの責任を3つの異なるレイヤーに分割 する方法です。 Model レイヤーのクラスは、製品やユーザのような、ビジネス概念とデータを 表現します。これらには API がありますが、エンドユーザ向けのインターフェー スはありません。HTTP や HTML には関知せず、cron ジョブのような 非Web アプリケーションからでも利用可能です。データベースやその他のデータソー スと通信し、自身の永続管理もします。 Controller レイヤーは Web のリクエストを Model レイヤー上の適当なアク ションに変換します。パラメータのパーシング、入力チェック、Model オブジェ クトの取得、メソッド呼び出しなどを処理します。その後、適当な View を決 定し、結果のHTMLをユーザに送信します。 View オブジェクトは実際はHTMLテンプレートです。Controller は Model オ ブジェクトのデータを View に転送し、View が Web ページを生成します。こ れらは Template Toolkit という、Perl で書かれた強力なテンプレートシス テムで実装されています。テンプレートは基本的な条件文とループがあり、ロ ジックのフォーマットを表現するのに十分な機能を備えています。テンプレー トにアプリケーションの制御フローが埋め込まれることはありません。 =head1 キャッシング パフォーマンス戦略のコアは、複数階層化されたキャッシュシステムです。ア プリケーションサーバでは、データオブジェクトは、ローカルディスク上の共 有メモリにキャッシュされています。アプリケーション側で、データオブジェ クトがどれだけの間、データベースと同期しないでよいかを指定し、その間の アクセスは高速なキャッシュによって処理されます。この種のキャッシュコン トロールは "time-to-live" として知られています。ローカルキャッシュは I データベースを用いて実装されています。オブジェクトは CPAN の標準モジュール I でシリアライズされます。 データオブジェクトは、高い粒度のエクスパイアを実現するために、必要に応 じて分割されます。たとえば、製品の在庫は他の製品データに比べて頻繁にアッ プデートされます。製品データを分割することによって、在庫だけは短いエク スパイア期間でデータベースと同期させ、その他の製品データのエクスパイア は長いままにしておくことができます。 アプリケーションサーバのオブジェクトキャッシュは IP マルチキャストプロ トコルと C でかかれたカスタムデーモンによって製品データを共有していま す。ある製品が1つのサーバのキャッシュに配置されると、そのデータは他の サーバのキャッシュに複製されます。このテクニックは製品データへのアクセ スの局所性が高いため、よい結果をもたらしました。2000 年のクリスマスシー ズンの間、キャッシュは 99% のヒットを記録し、それによってデータベース の負荷は大きく減りました。 データオブジェクトのキャッシュに加え、製品の詳細ページのような、ユーザ に依存しないページ全体もキャッシュ可能です。アプリケーションはページで 使われるデータオブジェクトのエクスパイアを最小に設定し、プロキシサーバ にページエクスパイア期間を、標準の I ヘッダで指示します。プロ キシサーバは NFS 上に、生成されたページをキャッシュします。このように してキャッシュされたページは、スタティックなページと同様のパフォーマン スを得ることができます。 緊急の修正ができるように、われわれは C にフックを追加して、 指定したURLのキャッシュコピーの削除ができるようにしました。これによっ て、まちがった情報をすぐに修正して反映させることができました。 C のキャッシュのもう1つの利点は、I リク エストの自動処理にあります。C にその機能があるため、これを われわれ自身で実装する必要はありませんでした。 =head1 セッショントラッキング ユーザは HTTP cookie によって、セッションIDが割り当てられます。この処 理はプロキシサーバ上の、カスタマイズされた C によっておこ なわれます。プロキシサーバで処理することによって、キャッシュされたペー ジにアクセスしているユーザもセッションIDが割り当てられることが保証され ます。セッションIDは、サーバサイドに保存されるデータへのキーに過ぎませ ん。ユーザセッションはアプリケーションサーバに割り当てられ、そのサーバ が利用できなくなるまで利用し続けます。これは "sticky" なロードバランシ ングと呼ばれます。セッションデータやその他のユーザが操作したデータ -- ショッピングカートの中身など -- はオブジェクトキャッシュとデータベース の双方に書き込まれます。二重に書き込むことにより、パフォーマンスペナル ティは若干生じますが、後続のリクエストでデータベースを見ない分、高速に リードアクセスを実現できます。サーバがダウンして、ユーザが別のサーバに 割り当てられた場合でも、単にデータベースからもう一度取得しなおせばよい のです。 =head1 セキュリティ 大規模な eコマースサイトはあらゆるアタックの絶好のターゲットとなります。 こうしたシステムを設計する際には、攻撃されることを想定し、マシンレベル と同時に、アプリケーションレベルでもキュリティを意識して構築する必要が あります。 一番のルールは"クライアントを信用するな!" です。ユーザ固有のデータをク ライアントに送出する際には、複数レベルの暗号化で、保護されます。SSL は、 重要なデータのやりとりを、ネットワークトラフィックのスヌーピングから保 護します。"セッションハイジャック"(他のユーザのセッションにアクセスす るために、セッションIDを改ざんすること)を防ぐために、セッション cookie に Message Authentication Code (MAC) を含めています。これは CPAN の C モジュールを利用して、こちらのサーバ内でしかわからない seed フレーズを使って生成されます。MAC アルゴリズムによってセッション cookie の ID をチェックすれば、データが何者かによって改ざんされたもの でないことが証明されます。 状態を保持する情報を HTML フォームやURLに配置する必要があり、かつそれ をユーザに明らかにしたくないシチュエーションでは、CPAN の C モジュールを使って、暗号化と復号化をします。C モジュールか らスタートするとよいでしょう。 単純な過負荷アタックから防御するために、ユーザが大規模なリクエストをサー バに送出して、サービス停止させようとした場合には、アプリケーションサー バへのアクセスがスロットルシステムによってコントロールされます。このコー ドは Randal Schwartz による C モジュールをベース にしています。それぞれのユーザへのアクセスはNFS上の小さなログにトラッ キングされます。プログラムは、1ユーザの一定期間内のリクエスト数の上限 を指定します。 MAC の使用や、暗号化、過負荷防御など、Web セキュリティに関する情報は、 O'Reilly の I や I を見てみることをお勧めします。 =head1 例外(Exception)処理 このシステムを計画した際、実装する言語として Java を利用することを検討 しました。Perl でいくことに決定しましたが、Java の見事な例外ハンドリン グ機能はなくては困るものでした。ラッキーなことに、CPAN の Graham Barr の Error モジュールによって、同様の機能を Perl でも実現できます。 Perl でもすでに、ランタイムのエラーをトラップして、例外オブジェクトを 投げることはサポートしていますが、Error モジュールはそれに加えて、いく つかの見事な syntactic sugar を提供しています。次のコードサンプルは、 このモジュールを使った典型的なものです。 try { do_some_stuff(); } catch My::Exception with { my $E = shift; handle_exception($E); }; このモジュールで、独自の例外クラスを作成し、特定のタイプの例外をトラッ プすることができます。 これによる大きなメリットの1つは、I との連係です。I の I フラグをたてて、例外をトラップしたい場所に try ブロック を利用すると、I モジュールによって、I のエラーが I オブジェクトに変換されます。 try { $sth->execute(); } catch Error with { # roll back and recover $dbh->rollback(); # etc. }; このコードでは、エラーによって、データベースのトランザクションをロール バックしなくてはならないことを示しています。実際には、ほとんどの C のエラーは、予想外のことがデータベースに起こって、現在の操作が 継続できないことを意味しています。これらの例外はトップレベルの、すうべ てのリクエストを囲む C ブロックに伝播させることができます。ここ でエラーがキャッチされた場合には、スタックトレースをロギングし、ユーザ には親切なエラーページを送信します。 =head1 テンプレート HTML とアプリケーションデータをはめこむロジックは、ともにテンプレート に保持されています。CPAN モジュールの I