パフォーマンス
ASP.NET アプリケーションのスケーリング戦略
Richard Campbell and Kent Alstad
この記事の内容 : :
- ASP.NET のアプリケーションとデータベースをスケーリングする
- コードを最適化する
- 効率的なキャッシュ
- アフィニティと負荷分散
|
この記事は次のテクノロジを使用しています:
ASP.NET
|

コンテンツ
ASP.NET のパフォーマンス アドバイザとして、私たちは普段、既にトラブルが起きている段階でプロジェクトにかかわります。多くの場合、声がかかるのは、アプリケーションの運用が実際に開始された後です。開発者にとってうまく機能したことが、ユーザーに対してはうまく機能しないのです。不満は、サイトが遅すぎるということです。経営陣は、これがテストで発見されなかった理由を知りたがりますが、開発陣は問題を再現できません。1 人以上の人間が、ASP.NET はスケーリングできないと言っています。よく聞く話だと思われるかもしれません。
世界で最もアクセス数の多い Web サイトのいくつかが、ASP.NET で動作しています。MySpace はその好例です。実際、このサイトはいくつかの異なるプラットフォームで運用された後、ASP.NET に移行されました。アプリケーションのスケールアップに伴い、パフォーマンスの問題が生じることがあります。そのような場合には、実際の問題が何であるのかを突き止め、その問題に対処するための最良の戦略を見つける必要があります。直面する最も大きな課題は、アプリケーションのパフォーマンスを隅から隅までカバーする測定方法のセットを作成することです。問題の全体を見なければ、どこにエネルギーを集中すればよいかがわかりません。
パフォーマンス方程式
2006 年 9 月、NetForecast の Peter Sevcik 氏と Rebecca Wetzel 氏が、『Field Guide to Application Delivery Systems』という論文を発表しました。この論文は、ワイド エリア ネットワーク (WAN) アプリケーションのパフォーマンスの向上に焦点を当て、図 1 に示すような方程式を含んでいました。この方程式は WAN のパフォーマンスに適用されるものですが、少し変更を加えることで、Web アプリケーションのパフォーマンス測定にも使用できます。変更した式を図 2 に示し、その各要素を図 3 で説明しています。

Figure 3 パフォーマンス方程式の要素
| 変数 |
定義 |
| R |
応答時間。ユーザーが (リンクをクリックするなどして) ページを要求してから、ユーザーのコンピュータ上にページ全体がレンダリングされるまでの合計時間。通常は、秒単位です。 |
| ペイロード |
ブラウザに送信される合計バイト数であり、マークアップおよびすべてのリソース (CSS、JS、イメージ ファイルなど) を含みます。 |
| 帯域幅 |
ブラウザとの間の転送速度。これは、非対称の場合があり、特定のページが複数のソースから生成されている場合には、複数の速度を表すこともあります。通常は、平均されて、単一の帯域幅がバイト/秒の単位で表されます。 |
| AppTurns |
特定のページが必要とするリソース ファイルの数。これらのリソース ファイルには、CSS、JS、イメージ、およびページのレンダリング中にブラウザによって取得される他のファイルが含まれます。方程式では、HTML ページは、AppTurns の項の前にラウンドトリップ時間 (RTT) を追加することで個別に表されます。 |
| RTT |
転送されるバイト数に関係なく、ラウンドトリップにかかる時間。すべての要求が、ページそれ自体に対して最低 1 RTT を費やします。通常は、ミリ秒単位です。 |
| 同時要求数 |
ブラウザがリソース ファイルに対して同時に行う要求の数。既定では、Internet Explorer は 2 つの同時要求を実行します。この設定は調整できますが、調整することはまれです。 |
| Cs |
サーバー上の計算時間。これは、コードが実行され、データベースからデータを取得し、ブラウザに送信する応答を作成するのにかかる時間です。単位はミリ秒です。 |
| Cc |
クライアント上の計算時間。これは、ブラウザが画面上に実際に HTML をレンダリングし、JavaScript を実行し、CSS 規則を実現するなどの作業にかかる時間です。 |
Figure 1 The Original Performance Equation (画像を拡大するには、ここをクリックします)
Figure 2 The Web Version of the Performance Equation (画像を拡大するには、ここをクリックします)
方程式を手に入れたところで、問題となるのは各要素の測定です。最後の値である応答時間は、比較的測定が簡単です。プロセス全体の所要時間を厳密に測定するツールがいくつかあります。
ペイロードは各種のツール (
websiteoptimization.com/services/analyze は優れた選択肢の 1 つ) を使用して測定でき、帯域幅 (speedtest.net を参照) とラウンドロビン時間 (Ping を使用) も同様です。
websiteoptimization.com/services/analyze のようなツールでも、Web ページの HTML、CSS、JavaScript、イメージなどのサイズをレポートできます。同時要求数は、基本的に定数です (Internet Explorer
® では既定で 2)。
残りは Cs と Cc ですが、これには追加の開発作業が必要になります。ページの実行が開始される厳密な秒数を記録して、実行の完了時にその時間を現在の時刻から減算するようなコードを、ASP.NET ページで記述するのは、比較的単純です。クライアント側でも同じことが言えます。HTML ページの先頭で小さな JavaScript を実行し、時刻を記録してから、ページの完了時に OnLoad イベントが発生した時点で、その時刻を減算することができます。
実際、パフォーマンス方程式を利用する Web サイトをデバッグ モードで構築する場合は、これらすべての要素に対してコーディングが可能です。そして、そうすることには適切な理由があります。ブラウザでパフォーマンス方程式の要素を定型的にレンダリングできれば、パフォーマンス上の問題がどこにあるのかを簡単に検出できます。
たとえば、ユーザーが別の大陸からアクセスするような ASP.NET アプリケーションで、帯域幅が低い場合を考えます。長い ping 時間 (> 200 ms) と低い帯域幅 (< 500 kbps) により、合計のペイロードとアプリケーション内のラウンドトリップ数がユーザーに対して大きな影響を与えます。そのようなユーザーのコンテキストでアプリケーションを捉えることは、開発者が経験する内容とは大きく異なるという点で、非常に重要です。
スケーリングの問題
私たちはコンサルタントとして、テスト環境では良好に動作するアプリケーションが実際の運用では性能が低いという場合、おそらくスケーリングの問題であるとわかっています。通常、テスト環境と実運用環境との違いは、同時実行ユーザーの数だけです。アプリケーションの性能が常に低いという場合は、スケーリングではなくパフォーマンスの問題です。
スケーリングを改善するために採用できる戦略は 3 つあります。特化、最適化、および分散です。それらの適用方法は場合によって異なりますが、実際の戦略は明快で一貫性があります。
特化の目的は、問題の特定のためにアプリケーションを小さな部分に分割することです。たとえば、イメージ、CSS、JS ファイルなどの静的リソースを、ASP.NET サーバーの外部に移すことを考えます。ASP.NET 用に調整されたサーバーは、そのようなファイルの提供に対して特に適しているわけではありません。したがって、リソース ファイルの提供用に調整された別個の IIS サーバーのグループを使用すると、実行するアプリケーションのスケーラビリティが大きく違ってきます。
圧縮や暗号化 (SSL 用) を頻繁に実行する場合は、SSL 専用のサーバーを設定すると役に立ちます。圧縮と SSL 終端用に特化されたハードウェア デバイスも考慮してください。
サーバー層を分割するためのより従来的な戦略では、データ アクセスや複雑な計算用に、Web ページを実際に生成するものとは別個のサーバーが求められるかもしれませんが、私はむしろ、3 台の Web サーバーと 2 台のビジネス オブジェクト サーバーよりは、すべてを実行する 5 台の Web サーバーを使用します。Web サーバーとビジネス オブジェクト サーバーの間で行われるプロセス外呼び出しは、すべて大きなオーバーヘッドを生じます。
特化は、既知の利益が期待される場合にのみ行う必要があります。また、最も速いソリューションが常に最良とは限りません。スケーラビリティの目標は、パフォーマンスの一貫性です。負荷の増加に伴ってパフォーマンスの範囲は狭める必要があります。ユーザーが 1 人であっても 1,000 人であっても、特定のページをすべてのユーザーに対して同じ時間で表示する必要があります。
最終的には、より効果的にスケーリングできるようサーバーのコードを最適化する必要があります。サーバー上の計算時間を除き、パフォーマンス方程式のほぼすべての要素は、線形にスケーリングされます。より多くの帯域幅をいつでも追加することができ (その時期はかなり簡単に判断できます)、クライアント上の計算時間はクライアントの数が増えても変化しません。パフォーマンス方程式の他の要素も、スケーリングについては一貫性があります。ただし、サーバー上の計算時間は、ユーザー数が増えるごとに調整が必要になります。
コードを最適化する
サーバー コードの最適化のコツは、テストを実行して、実際に違いが得られるかどうかを確認することです。プロファイリング ツールを使用して、アプリケーションを分析し、アプリケーションが最も多くの時間を費やしている箇所を突き止めます。プロセスの全体が、経験に基づいている必要があります。ツールを使用して、改良するコードを見つけ、そのコードを改良し、パフォーマンスが実際に向上したことをテストで確認し、それを何度も繰り返します。実際に大規模なサイトでは、このようなパフォーマンスのチューニングをゴールデン ゲート ブリッジの塗装作業にたとえることがよくあります。橋全体を塗り終わったころには、また最初の部分を塗り直す時期になっているというわけです。
私はいつも、スケーリングの開始点が分散であると信じている人が多いことに驚いています。「もっとハードウェアを追加しろ」と彼らは叫びます。誤解しないでください。ハードウェアの追加が有用であることは間違いありません。ただし、特化と最適化がなければ、その成果はごく小さなものになります。
特化では、アプリケーションの小さな部分を必要に応じて分散できます。たとえば、イメージ サーバーを分離した場合、イメージ サービスをアプリケーションの残りの部分から独立して簡単にスケーリングできます。
また、最適化によって特定の操作に必要な作業量を減らすと、分散にもメリットが得られます。これは、同じユーザー数にスケーリングするために必要なサーバー数の削減に、直接つながります。
負荷分散
サーバーを追加するために必要な分散を実現するには、それらのサーバー間でアプリケーションを複製し、負荷分散を行います。負荷分散には、Windows Server® 2003 のすべてのエディションに付属するサービスである、ネットワーク負荷分散 (NLB) を使用できます。NLB を使用すると、負荷分散の関係においてすべてのサーバーが同等なパートナーとなります。すべてのサーバーが負荷分散に対して同じアルゴリズムを使用し、すべてのトラフィックに対して 1 つの共有仮想 IP アドレス上で監視を行います。負荷分散アルゴリズムに基づき、各サーバーは、特定の要求に対してどのサーバーが動作するかを知っています。クラスタ内の各サーバーは、ハートビートを送信して、自分が動作中であることを他のサーバーに知らせます。サーバーに障害が発生すると、そのサーバーのハートビートが停止し、他のサーバーが自動的にそれを補償します。
NLB は、多数のユーザーがよく似た要求を行っている場合は良好に機能します。ただし、一部の要求が他の要求よりもずっと大きな負荷を生むような状況では、補償メカニズムはうまく機能しません。さいわい、そのような状況に対しては、ハードウェア負荷分散ソリューションがあります。
アフィニティ
最終的に、効果的な分散を実現するための課題は、アフィニティをなくすことにあります。たとえば、Web サーバーが 1 台しかない場合、そのサーバーにセッション データを格納することは完全に合理的です。しかし、Web サーバーが複数ある場合は、どこにセッション情報を保持すればよいでしょうか。
1 つのアプローチは、それを Web サーバー上に保持し、アフィニティを使用することです。これは本質的に、特定のユーザーからの最初の要求が負荷分散され、それ以降、そのユーザー/セッションからの要求はすべて、最初の要求と同じサーバーに送信されることを意味します。これは単純なアプローチであり、すべての負荷分散ソリューションでサポートされ、場合によっては、合理的でさえあります。
ただし、長い目で見ると、アフィニティは悩みの種となります。セッション データをプロセス内で保持すると高速かもしれませんが、ASP.NET ワーカー プロセスがリサイクルされると、それらのセッションはすべて無効になります。そして、ワーカー プロセスのリサイクルは多くの理由によって実行されます。高負荷の場合、IIS は ASP.NET のワーカー プロセスがハングしていると判断し、リサイクルすることがあります。実際、IIS 6.0 では既定で 23 時間ごとにワーカー プロセスがリサイクルされます。これは調整可能ですが、いずれにしても、ユーザーはプロセス中にセッション データを失う可能性があります。サイトが小さいうちは、これはそれほど大きな問題ではありませんが、サイトが大きくなりアクセス頻度が高まるにつれ、重要な問題となっていきます。それだけではありません。
IP アドレスによって負荷分散を行っている場合、あるサーバーが (AOL のような) メガプロキシによってヒットされると、負荷全体を 1 台では処理できなくなります。さらに、サーバーをアプリケーションの新しいバージョンで更新することがより困難になります。ユーザーがサイト上で作業を完了するまで何時間も待つか、またはユーザーをセッションから追い出して困惑させるかのいずれかになります。そして、信頼性が問題となってきます。サーバーを失い、多数のセッションを失うのです。
アフィニティをなくすことが、分散の重要な目標です。そのためには、セッションの状態データをプロセス外に移動する必要があります。それは、スケーラビリティを高めるためにパフォーマンスを低下させることを意味します。セッションをプロセス外に移すと、セッション データは、すべての Web サーバーがアクセスできる場所に記録されます。SQL Server® 上、または ASP.NET 状態サーバー上です。これは、web.config で設定されます。
また、プロセス外セッションをサポートするために必要なコーディング作業もあります。Session オブジェクトに格納されるすべてのクラスは、Serializable 属性でマークされる必要があります。これは、そのクラス内のすべてのデータが無視されるように、各データをシリアル化可能にするか、または NonSerialized とマークする必要があることを意味します。クラスをマークアップしない場合、シリアライザが動作してセッション データをプロセス外に格納するときに、エラーが発生します。
最後に、セッションをプロセス外に移動することは、セッション オブジェクト内のデータが多すぎることに気付く良い手段となります。なぜなら、ページ要求ごとに、そのように大きな BLOB データをネットワーク上で 2 回送受信するというコストがかかるためです (ページの先頭での取得時に 1 回と、ページの末尾で返すときに 1 回)。
Session オブジェクトに決着をつけたら、メンバシップやロール マネージャのような他のアフィニティ問題を追求します。それぞれの問題には、アフィニティをなくすための固有の困難があります。しかし、ASP.NET アプリケーションを真にスケールアップさせるためには、見つけられるあらゆる形のアフィニティをあぶり出し、それを解消する必要があります。
ここまでに述べたすべての戦略は、スケーリングを必要とする実際上すべての Web アプリケーションに適用できます。事実、これらの戦略は、任意のテクノロジを使用したほとんどどのようなアプリケーションのスケーリングにでも適用されます。この後は、いくつかの ASP.NET 固有の技法について見ていきましょう。
ペイロードを最小化する
パフォーマンス方程式を見ると、ペイロードが大きな影響を持つことがわかります。特に、限られた帯域幅を扱っている場合にはそうです。ペイロードのサイズを小さくすると、応答時間が向上し、送受信するバイト数が減ることでスケーリング上の利益も得られ、帯域幅のコストも節約できます。
ペイロードのサイズを減らすためにできる最も単純なことの 1 つは、圧縮を有効にすることです。IIS 6.0 では、静的ファイル、動的に生成される応答 (ASP.NET ページなど)、またはその両方を圧縮するかどうかを指定できます (図 4 を参照)。
Figure 4 Configuring Compression Server-Wide in IIS 6.0 (画像を拡大するには、ここをクリックします)
IIS 6.0 は要求に応じて静的ファイルを圧縮し、指定した圧縮ファイル キャッシュに格納します。動的に生成される応答に対しては、コピーは格納されません。これらは毎回圧縮されます。IIS 7.0 は、圧縮に関してはより洗練されていて、頻繁に使用されるファイルだけを圧縮します。
圧縮はプロセッサ サイクルを消費しますが、一般に、専用 Web サーバー上には余分なプロセッサ容量が多くあります。ただし、IIS 7.0 はさらに最適化され、プロセッサが本当にビジーになると、圧縮作業を中断します。また、Web サーバー自体とは独立に圧縮を行う専用デバイスもあります。
ペイロードの削減に関連したもう 1 つの領域として、ViewState があります。開発中には、ViewState の使用量が手に負えなくなることがよくあります。ほとんどの Web コントロールは何らかの ViewState を使用しており、コントロールが集中しているページでは、ViewState が何千バイトにも及ぶ場合があります。ViewState の使用を減らすには、それを必要としないコントロールでは ViewState を無効にすることです。場合により、開発者は ViewState を減らすためにコントロール自体を削除することがありますが、それは必ずしも必要ではありません。今日の Web コントロールのほとんどは、過度の ViewState の問題に敏感であるため、そのサイズをきめ細かく制御できるようになっています。また、コードやアプリケーションの実行方法を変えずに ViewState を削除したり置換したりできるハードウェア デバイスもあります。
ペイロードのサイズを減らすための最も効果的なテクノロジの 1 つが、AJAX です。AJAX は実際にはペイロードのサイズを小さくするわけではありません。単に、ブラウザに送信する合計バイト数を増やしつつ、ペイロードの見かけのサイズを減らすだけです。AJAX を使用すると、親ページが小さくなるため、最初のレンダリング時間が高速になります。その後、そのページ内の個々の要素が、サーバーにそれぞれ要求を発行して、データを格納します。
実質的に、AJAX はペイロードを長い時間に振り分けることで、他の部分を読み込んでいる間、ユーザーに何かが表示されるようにしています。そのため、AJAX を使用すると、ユーザーの全体的な操作感が向上しますが、作業の真のコストを測定するには、パフォーマンス方程式に戻る必要があります。一般に、AJAX は、クライアント上の計算時間を、ときには劇的に、パフォーマンスが許容できなくなるほど増加させます。
ページ全体を要求する代わりに、サーバーへの AJAX ラウンドトリップで個々の要素にデータを格納すると、正味のラウンドトリップ回数は減ります。ただし、多くの場合、特定のユーザーに対するラウンドトリップの合計数は増加します。AJAX によってパフォーマンスが向上したか低下したかを判断するには、入念なテストが必要です。
キャッシュ
ASP.NET アプリケーションのスケーリングの専門家は、キャッシュについて多くを語ります。基本的に、キャッシュとは、データをユーザーの近くに移動することです。標準的な ASP.NET アプリケーションでは、大規模な最適化作業が行われるまでは、ユーザーが必要とするデータはほとんどすべてデータベース内に存在し、要求のたびにデータベースから取得されます。キャッシュを使用すると、そのような状況が変わります。ASP.NET は、実際には 3 つのキャッシュ方式をサポートしています。ページ キャッシュ (出力キャッシュとも呼ばれる)、部分ページ キャッシュ、およびプログラム キャッシュ (データ キャッシュとも呼ばれる) です。
ページ キャッシュは、最も単純なキャッシュ方式です。これを使用するには、ASP.NET ページに @OutputCache ディレクティブを追加し、有効期限に関する規則を含めます。たとえば、ページを 60 秒間だけキャッシュするように指定できます。このディレクティブを配置すると、そのページの最初の要求は通常どおり処理され、データベースや、ページの生成に必要なその他のリソースがアクセスされます。その後、そのページは Web サーバー上のメモリに 60 秒間だけ保持され、その間の要求はすべてメモリから直接データが提供されます。
この例は単純明快ではありますが、残念ながら、ページ キャッシュの基本的な現実を無視しています。実質的にすべての ASP.NET ページは、その全体を特定の時間だけキャッシュできるほど静的ではありません。そこで、部分ページ キャッシュを使用することになります。部分ページ キャッシュでは、ASP.NET ページの特定の位置をキャッシュ可能とマークできるため、ページ内で定期的に変更される部分だけが計算されます。これはより複雑ですが、効果的です。
おそらく、最も強力な (そして最も複雑な) キャッシュ形式は、ページで使用されるオブジェクトに焦点を合わせた、プログラム キャッシュでしょう。プログラム キャッシュの最も一般的な用途は、データベースから取得されたデータの格納です。
データのキャッシュに関して最も明確な問題は、キャッシュした後で元のデータが変更されている可能性があることです。キャッシュの期限切れは、あらゆる形のキャッシュを実装する際に直面する、最大の問題です。ただし、メモリについても検討する必要があります。
ビジーな ASP.NET サーバーでは、さまざまな理由によって、メモリが大きな問題となります。ASP.NET ページを計算するときには必ず、ある程度のメモリが使用されます。そして、Microsoft® .NET Framework は、メモリを非常にすばやく割り当てながら、ガベージ コレクションによって、比較的ゆっくり解放するように設定されています。ガベージ コレクションと .NET のメモリ割り当てに関する議論は、それ自体が 1 つの記事であり、これまでに何回も書かれています。ここでは、ビジーな Web サーバーでは、ASP.NET アプリケーションに対して使用可能な 2 GB のメモリ空間に対する需要が大きいとだけ言っておけば十分でしょう。理想的には、そのメモリ利用のほとんどは、Web ページの計算に使用される変数や構造体に割り当てられるため、一時的なものです。
ただし、プロセス内セッションやキャッシュ オブジェクトなどの永続的なメモリ オブジェクトについては、メモリの使用量がずっと大きな問題になります。そしてもちろん、これらの問題は、アプリケーションが本当にビジーのときにしか表面化しません。
次のようなシナリオを考えてみましょう。あなたの Web サイトは、ある新しいマーケティング プロモーションをきっかけに、何千人ものユーザーからのアクセスを集め、多大な収益を上げています。このサイトでは、良好な応答時間を維持するために、ページの一部やデータ オブジェクトのグループを可能な限りキャッシュしています。ユーザーからの各ページ要求がそれぞれ少量のメモリを消費するため、メモリ消費量のバーは上昇し続けます。ユーザーの増加に従って、バーの上昇も速くなります。また、キャッシュ オブジェクトやセッション オブジェクトによって大きく跳ね上がることもあります。
メモリの合計使用量が ASP.NET の既定のキャッシュ メモリ制限値の 90 パーセントに近づいたため、ガベージ コレクションが呼び出されます。ガベージ コレクタは、メモリ空間を探索し、永続的なメモリ オブジェクト (キャッシュ オブジェクトやセッション オブジェクトなど) を選別しながら、既に使用されていないメモリ (Web ページの計算に使用されていたメモリ) を解放します。未使用メモリの解放は高速ですが、永続オブジェクトの選別は低速です。したがって、永続オブジェクトの数が多いほど、ガベージ コレクタの作業は困難を増します。このような問題の発生は、perform.exe 内で Gen 2 コレクションの数が多いことから識別できます。
ガベージ コレクションの実行中、その ASP.NET サーバーではページが提供されないことを思い出してください。すべてがキューに保持され、ガベージ コレクション プロセスの完了を待ちます。また、IIS もそれを監視しています。プロセスに長い時間がかかりすぎ、ハングしている可能性があると判断した場合、IIS はワーカー スレッドをリサイクルします。これにより、それらの永続メモリ オブジェクトがすべて破棄されるため、大量のメモリがすばやく解放されますが、何人かのユーザーを困惑させることになります。
現在は、空きメモリが少ない場合にプログラム キャッシュから自動的にオブジェクトを削除する ASP.NET 用のパッチが提供されていて、それは表面的には良いアイデアのように思えます。クラッシュよりはいいでしょう。ただし、キャッシュから削除したものはすべて、コードによって最終的に元に戻されることに注意してください。
何かをキャッシュした時点で、それが間違っているというリスクを負うことになります。たとえば、部品のデータベースおよび対応する注文ページを例として考えます。部品ページの最初の実装では、ページのレンダリングごとに、在庫に残っている部品数をデータベースから取得する要求が含まれます。これらの要求を分析すると、99 パーセントの時間は、同じ数値を何度も取得していることに気づきます。それは、キャッシュできるのではないでしょうか。
これをキャッシュする単純な方法の 1 つは、時間に基づいてキャッシュすることです。そこで、部品の在庫を 1 時間にわたってキャッシュすることにします。この手法の欠点は、だれかが部品を購入した後で、そのページに戻ったときに、表示されている在庫数が依然として同じであるということです。そのことにはクレームがあるでしょう。ただし、それよりずっと問題なのは、だれかが部品を購入しようとして、それが実際には売り切れているにもかかわらず、まだ在庫があるように表示されることです。入荷待ちシステムを構築することはできますが、いずれにしても、顧客の失望を招くことになります。
おそらく問題なのは、有効期限の処理方法でしょう。時間で区切るだけでは不十分です。部品をだれかが購入するまで在庫数をキャッシュしておき、その後でキャッシュ オブジェクトを期限切れにすることもできます。これはより論理的ですが、ASP.NET サーバーが複数ある場合にはどうなるでしょうか。どのサーバーにアクセスしたかによって、部品の在庫数が異なることになります。新しい在庫 (在庫数に加算される) の受け取りは Web アプリケーションを経由しないことを考えると、これは方法自体が誤っています。
期限切れを ASP.NET サーバー間で同期させることもできますが、これには注意が必要です。キャッシュ オブジェクトと Web サーバーの数が増えるほど、Web サーバー間で生成する対話の量は、幾何学的に増加します。
キャッシュの有効期限がパフォーマンスに与える影響も、慎重に検討する必要があります。高負荷条件下では、キャッシュ オブジェクトを期限切れにすると、多くの不満を生む可能性があります。たとえば、データベースから結果が返るのに 30 秒間かかるような高コストのクエリがあるとします。負荷が高いときには、このページが毎秒 1 回要求されるため、この高コストを節減するために、このクエリをキャッシュすることにします。
キャッシュ オブジェクトを処理するコードはかなり単純です。データベースから必要なときにデータを取得する代わりに、アプリケーションは最初にキャッシュ オブジェクトにデータが格納されているかどうかを確認します。格納されている場合は、キャッシュ オブジェクトからのデータを使用します。格納されていない場合は、データベースからデータを取得するコードを実行してから、キャッシュ オブジェクトにそのデータを格納します。その後コードは、通常どおりに実行を続けます。
ここで、次のような問題が生じます。30 秒間かかるクエリを受け取り、このページを毎秒実行している場合、キャッシュ項目にデータを格納している間に、他の要求が 29 個受信され、そのすべてがそれぞれ、データベースに独自のクエリを実行してキャッシュにデータを格納しようとします。この問題を解決するには、他のページ実行がデータベースからデータを要求しないようにスレッド ロックを追加できます。
しかし、もう一度シナリオをよく見てみましょう。最初の要求が受信され、キャッシュ項目にデータが格納されていないことを確認し、コードにロックを適用し、クエリを実行してキャッシュ オブジェクトにデータを格納します。最初の要求がまだ実行されている間、1 秒後に 2 番目の要求が到着し、キャッシュ オブジェクトにデータが格納されていないことを確認しますが、ロックがかかっているため、ブロックされます。以降 28 個の要求についても同様です。そして、最初の要求が処理を完了し、ロックを削除して、実行を継続します。他の 29 個の要求はどうなるでしょうか。既にブロックはされていませんので、同じように実行を継続します。ただし、それらの要求は、キャッシュ オブジェクトにデータが格納されているかどうかの確認を既に終えています (そして、その時点では格納されていませんでした)。したがって、各要求はロックを適用しようとし、そのうちの 1 つが成功して、再びクエリを実行します。
問題がわかりましたでしょうか。最初の要求がキャッシュ オブジェクトの処理を完了した後に到着した他の要求は正常に実行されますが、クエリの実行中に到着した要求は、困難な状況に陥ります。これに対処するコードを記述する必要があります。ある要求がロックに遭遇した場合、そのロックが解除されたときには、図 5 に示されるように、その要求はキャッシュ オブジェクトにデータが格納されているかどうかを再度確認する必要があります。おそらくその時点で、キャッシュ オブジェクトにはデータが格納されています。それが最初にロックが適用された理由であるためです。ただし、ロック中に他のコードがキャッシュ オブジェクトを再度期限切れにし、データが格納されていない可能性もあります。

Figure 5 キャッシュ オブジェクトを確認し、ロックし、再確認する
// check for cached results
object cachedResults = ctx.Cache["PersonList"];
ArrayList results = new ArrayList();
if (cachedResults == null)
{
// lock this section of the code
// while we populate the list
lock(lockObject)
{
// only populate if list was not populated by
// another thread while this thread was waiting
if (cachedResults == null)
{
...
}
}
}
適切に動作するキャッシュ コードを書くことは困難な作業ですが、それによって得られる利益は膨大です。ただし、キャッシュは複雑さを増加させるので、慎重に使用する必要があります。その複雑さから本当にメリットが得られることを確認してください。キャッシュ コードは常に、次のような複雑なシナリオに対してテストする必要があります。同時に複数の要求が発生した場合にはどうなるでしょうか。期限切れが短時間で起こる場合はどうでしょうか。これらの質問への答えを知っている必要があります。それにより、キャッシュ コードがスケーリングの問題をさらに悪化させるような事態を避けることができます。
データベースをスケーリングする
Web サイトをスケーリングするための通常のアプローチは、スケールアップではなく、スケールアウトです。これは主に、ASP.NET のスレッドおよびメモリの制限と、時間が短いという Web 要求の特質によるものです。
ただし、データベースのスケーリングに関しては、通常の手法はスケールアップです。データベースは、1 つの巨大な箱とするか、またはクラスタ構成の 2 つの箱とします (ただし、ある時点で実際に実行されているデータベースは 1 つだけです)。しかし、すべての大規模な Web アプリケーションでは、最終的に 1 つのデータベースでは負荷を処理しきれなくなります。そのため、スケールアウトが必要です。これは可能です。単に Web アプリケーション自体に対するのと同じ戦略を適用するだけです。最初の手順は常に特化です。データベースを論理パーティションに分割します。これらのパーティションは、データ中心で、おそらくは地域ごとに区切られます。したがって、それぞれがデータベース全体の一部分を含む、複数のデータベースが作成されます。たとえば、1 つのサーバーには東海岸のデータ、もう 1 つのサーバーには西海岸のデータが格納されます。
ただし、本当に大規模な Web アプリケーションでは、データベースを読み取り用と書き込み用に分割します (図 6 を参照)。読み取り用データベースは読み取り専用であり、書き込み用データベースからレプリケーションによってデータを受け取ります。すべてのデータ クエリは、データをできるだけ高速に読み取るよう最適化されている、読み取り用データベースに渡されます。読み取り用データベースは、本質的に、高いレベルの分散が可能です。
Figure 6 Distributed Database
Architecture (画像を拡大するには、ここをクリックします)
すべてのデータ書き込み要求は、書き込みを効率的に行えるよう分割され調整されている、書き込み用データベースに送られます。レプリケーションによって、書き込み用データベースから読み取り用データベースに新しいデータが移動します。
そのような特化したデータベースを作成した結果、待ち時間が生じます。書き込まれたデータが読み取り用データベースに送られるのには時間がかかります。ただし、この待ち時間に対処できれば、スケーリングの潜在能力は膨大になります。
無限のスケーリング作業
アプリケーションが成長を続ける限り、スケーリングの作業も同様に増大し続けます。10,000 人の同時ユーザーに対して効果的に動作する ASP.NET の手法も、100,000 人のユーザーに対しては同じように効果的ではなく、100 万人のユーザーに対してはまたルールが変わります。もちろん、パフォーマンスは、アプリケーションに完全に依存します。ここでは、1,000 人以下のユーザーでスケーリングの課題を抱えているアプリケーションを見てきました。
効果的なスケーリングの鍵は、切る前に測る、ということです。テストを使用して、必要なところに労力を費やしていることを確認してください。作業をテストして、単に変更しただけではなく、実際に改善を行ったことを確認してください。スケーラビリティに対する最適化に焦点を合わせた開発サイクルであっても、その終わりには、最も遅い部分がどこであるかを知っておく必要があります。ただし、それはおそらく今日のユーザーにとっては十分に高速なので、後は将来のユーザーのために必要となる作業に専念できるでしょう。
Richard Campbell は、Microsoft Regional Director を務めると共に、ASP.NET の MVP であり、「.NET ロック - .NET 開発者のためのインターネット オーディオ トークショー」(
dotnetrocks.com) の共同ホストでもあります。何年かにわたって ASP.NET のパフォーマンスとスケーリングに関する企業のコンサルタントを務め、また、Strangeloop Networks の共同創設者の 1 人でもあります。
Kent Alstad は、Strangeloop Networks (
strangeloopnetworks.com) の CTO を務め、Strangeloop の出願中の特許すべての主執筆者または共同執筆者です。Strangeloop の設立に協力する前は、多くの高性能、高スケーラビリティ ASP.NET アプリケーションの構築やコンサルティングに携わっていました。