ASP.NET の成功の理由の 1 つは、Web 開発者にとっての敷居の低さです。ASP.NET コードを作成するために、コンピュータ サイエンスの博士号は必要ありません。実際に、筆者が仕事で関係した多くの ASP.NET 開発者は独学でこれを習得しており、Microsoft Excel スプレッドシートを使用していた人が、C#、または Visual Basic を記述するようになり、現在は、Web アプリケーションを作成するようになっています。そして、一般的に、そうした開発者はすばらしい仕事をしています。
しかし、能力と共に役割も大きくなり、熟練の ASP.NET 開発者となっても、間違いに対する免疫がないことがあります。ASP.NET プロジェクトに対するコンサルティングを何年間か続けてきた中で、不具合を引き起こす間違いには、はっきりとした傾向があることに気付きました。まず、こうした間違いの中には、パフォーマンスに影響するものがあります。また、スケーラビリティを阻害するものもあります。さらに、開発者チームの貴重な時間を、バグや、予期しない動作の追跡に消費させてしまう場合もあります。
ここでは、ASP.NET アプリケーションを本番稼動させるまでの過程に潜む 10 個の注意点を紹介し、それらの対処方法を説明します。ここで挙げる例は、すべて、実際の会社での、実際の Web アプリケーションの構築に関する筆者の経験に基づきます。いくつかについては、ASP.NET 開発チームが直面した数々の問題の背景となった問題についても説明します。
LoadControl と出力キャッシュ
ユーザー コントロールを採用していない ASP.NET アプリケーションはあまりありません。マスタ ページの登場以前は、ユーザー コントロールを使用して、ヘッダーやフッターなどの共通コンテンツを分離していました。ASP.NET 2.0 においても、ユーザー コントロールは、コンテンツ、および動作のカプセル化を行う効果的な手段です。また、これは、ページをいくつかの領域に分割し、その領域のキャッシュ機能をページ全体とは別に制御する、フラグメント キャッシュと呼ばれる特殊な形式の出力キャッシュのための手段ともなっています。
ユーザー コントロールは、宣言的に、または命令的に読み込むことができます。命令的な読み込みは、Page.LoadControl に依存します。これは、ユーザー コントロールをインスタンス化し、Control 参照を戻します。ユーザー コントロールにカスタム型メンバ (たとえば、パブリック プロパティなど) が含まれる場合は、この参照をキャストし、コードからそのカスタム メンバにアクセスできます。図 1 に示したユーザー コントロールは、BackColor という名前のプロパティを実装しています。次のコードは、ユーザー コントロールを読み込み、BackColor に値を割り当てています。
protected void Page_Load(object sender, EventArgs e)
{
// ユーザー コントロールを読み込み、ページに追加します。
Control control = LoadControl("~/MyUserControl.ascx");
PlaceHolder1.Controls.Add(control);
// 背景色を設定します。
((MyUserControl)control).BackColor = Color.Yellow;
}
このコードは単純ですが、油断している開発者を捕らえる罠が潜んでいます。何が不具合となるかわかりますか?出力キャッシュに関連する問題であるとわかれば、それが正解です。このコードのサンプルは、正常にコンパイルされ実行します。しかし、次のステートメント (完全に正しいステートメントです) を MyUserControl.ascx に追加してみます。
<%@ OutputCache Duration="5" VaryByParam="None" %>これでページを実行すると、次のエラー メッセージを伴う InvalidCastException が発生します。
"型 'System.Web.UI.PartialCachingControl' のオブジェクトを型 'MyUserControl' にキャストできません。"
コードは、OutputCache ディレクティブなしでは正常に機能しますが、OutputCache ディレクティブが追加されると正常に機能しません。ASP.NET は、このような処理を想定していません。ページ (およびコントロール) は、出力キャッシュをまったく認識していないと思われます。これは、なぜでしょうか。
問題は、ユーザー コントロールの出力キャッシュが有効になると、LoadControl が、コントロールのインスタンスへの参照を戻さなくなることです。LoadControl は、代わりに、コントロールの出力がキャッシュされているかどうかに応じて、コントロールのインスタンスをラップする場合と、しない場合がある PartialCachingControl のインスタンスを戻します。このため、LoadControl を呼び出してユーザー コントロールを動的に読み込み、コントロール固有のメソッドおよびプロパティにアクセスするためにコントロール参照をキャストする場合は、コードが、OutputCache ディレクティブがある場合でも、ない場合でも機能するように考慮する必要があります。
図 2 は、ユーザー コントロールを動的に読み込み、戻されるコントロール参照をキャストする適切な方法です。この動作の流れは、次のようになります。
- ASCX ファイルに OutputCache ディレクティブがない場合、LoadControl は MyUserControl 参照を戻します。Page_Load は、MyUserControl への参照をキャストし、そのコントロールの BackColor プロパティを設定します。
- ASCX ファイルに OutputCache ディレクティブが含まれ、コントロールの出力がキャッシュされていない場合、LoadControl は PartialCachingControl への参照を戻します。このときの CachedControl プロパティには、基となる MyUserControl への参照が含まれます。Page_Load は、PartialCachingControl.CachedControl を MyUserControl にキャストし、そのコントロールの BackColor プロパティを設定します。
- ASCX ファイルに OutputCache ディレクティブが含まれ、コントロールの出力がキャッシュされている場合、LoadControl は PartialCachingControl への参照を戻します。このときの CachedControl プロパティは null です。この場合、Page_Load は何も行いません。コントロールの出力は出力キャッシュから配信されるため、コントロールの BackColor プロパティを設定することはできません。つまり、プロパティを設定できる MyUserControl が存在しません。
図 2 のコードは、.ascx ファイルに、OutputCache ディレクティブが存在する場合でも、存在しない場合でも機能します。これは、すばらしい手法とは言えませんが、面倒な問題を回避できます。単純な方が保守性も高い、とは必ずしも言えません。
セッションと出力キャッシュ
出力キャッシュに関して、ASP.NET 1.1、および ASP.NET 2.0 には、Windows Server 2003 と IIS 6.0 で稼動するサーバーで出力キャッシュされるページに影響する潜在的な問題があります。これまで、本番稼動の ASP.NET サーバーでのこの問題が顕在化したことが 2 回ありましたが、どちらの場合も出力キャッシュを無効にすることで解決されました。後になって、出力キャッシュを無効にせずに解決できる方法がわかりました。まず、最初に発生した問題の内容を説明します。
問題の始まりは、小規模な ASP.NET Web ファームで一般向けの電子商取引アプリケーションを稼動させたあるサイト (こでは Contoso.com とします) からの "クロススレッド" エラーが発生するという連絡でした。Contoso.com Web サイトを使用していると、入力したデータが突然失われ、代わりに他のユーザーのデータが表示される現象がときどき発生するということでした。少し調査した結果、クロススレッドという表現は正確ではなく、むしろ "クロスセッション" エラーと言える状態であったことがわかりました。Contoso.com は、セッション ステートにデータを保存しており、何らかの理由で、ユーザーがたまたま (ランダムに) 他のユーザーのセッションに接続されているように見えました。
チームのメンバの 1 人が、各 HTTP 要求および応答の主要な要素を、cookie のヘッダーなども含めてログに記録する診断ツールを作成しました。そのツールを Contoso.com の Web サーバーにインストールし、数日間、実行させてみました。結果は、明らかでした。おおよそ 100,000 要求ごとに 1 回、ASP.NET はまったく新しいセッションにセッション ID を正常に割り当て、そのセッション ID を Set-Cookie ヘッダーで戻し、その後、次の要求で、同じセッション ID (つまり、同じ Set-Cookie ヘッダー) を戻していました。これは、その要求が既に有効なセッションに関連付けられ、正常にそのセッション ID が cookie に出力されている場合でも行われていました。この結果、ASP.NET は、ランダムに、ユーザーを本来のセッションから切り離し、別のセッションに接続していることになりました。
私たちは、驚き、原因を調査しました。まず、Contoso.com のソース コードを調査しましたが、ここに問題はありませんでした。次に、アプリケーションが Web ファームでホストされていることが、この問題が関係ないことを確認するため、サーバーを 1 つだけ残して、残りをすべて無効にしました。問題は解決されませんでしたが、これは予想されていたことです。ログから、一致する Set-Cookie ヘッダーは、2 つの別のサーバーから配信されたものではないことがわかっていました。ASP.NET が誤って重複したセッション ID を生成するということ考えられません。ASP.NET は、.NET Framework RNGCryptoServiceProvider クラスを使用して、これらの ID を生成しており、セッション ID には、同じ ID が 2 回生成されることが決してないだけの十分な長さがあります (1 兆年ほどの間には、おこらないでしょう)。さらに、RNGCryptoServiceProvider が誤って重複した乱数を生成していたとしても、それは、ASP.NET が、有効なセッション ID を、新しい (一意でない) ID になぜか置き換える原因の説明にはなりません。
私たちは、直感的に出力キャッシュを確認してみることにしました。OutputCacheModule で HTTP 応答をキャッシュする場合には、これにより Set-Cookie ヘッダーがキャッシュされていないことに注意する必要があります。キャッシュされると、新しいセッション ID を含むキャッシュされた応答によって、キャッシュされた応答のすべての受信者 (キャッシュされた応答を生成した要求を行ったユーザーも含めて) が同じセッションに接続されます。ソース コードをチェックしたところ、Contoso.com では、2 つのページで出力キャッシュが有効になっていました。私たちは、そのキャッシュを無効にしました。驚いたことに、アプリケーションは、その後数日間、1 回もクロスセッションの不具合が発生しませんでした。そして、それから 2 年以上、エラーもなく稼動しています。また、別の企業の別の Web サーバーでの別のアプリケーションでも、まったく同じ状況がありました。Contoso.com の場合と同様に、出力キャッシュを無効にすることで問題は解決されました。
その後、マイクロソフトで、この動作は OutputCacheModule の問題が原因であることが確認されました。(この記事が公開されるまでに、更新が入手可能になっている可能性もあります。) ASP.NET と IIS 6.0 が一緒に使用され、カーネル モード キャッシュが有効である場合、OutputCacheModule は、Http.sys に渡すキャッシュされた応答からの Set-Cookie ヘッダーの除去にときどき失敗します。以下は、バグが顕在化する原因となるイベントの具体的な流れです。
- 最近、サイトを訪れていないユーザー (つまり、該当するセッションを持っていないユーザー) が、出力キャッシュが有効であるページを要求します。ただし、現在、キャッシュには使用できる出力がありません。
- 要求は、ユーザーの新しく作成されたセッションにアクセスするコードを実行します。これにより、セッション ID cookie が、応答の Set-Cookie ヘッダーで戻されます。
- OutputCacheModule は、出力を Http.sys に提供しますが、応答からの Set-Cookie ヘッダーの除去に失敗します。
- Http.sys は、キャッシュされた応答を後続の要求で戻し、誤って、別のユーザーをこのセッションに接続します。
この事例からわかることは何でしょうか。セッション ステートと、カーネル モード出力キャッシュは共存できません。出力キャッシュが有効であるページでセッション ステートを使用し、アプリケーションが IIS 6.0 で実行されている場合は、カーネル モード出力キャッシュを無効にする必要があります。この場合でも、出力キャッシュの利益は得られます。ただし、カーネル モード出力キャッシュは、通常の出力キャッシュよりも大幅に高速であるため、キャッシュの効果は劣ります。この問題についての詳細は、support.microsoft.com/kb/917072 を参照してください。
カーネル モード出力キャッシュは、ページの OutputCache ディレクティブに VaryByParam="*" 属性を含めることにより、個々のページで無効にすることができます。ただし、この方法は、必要とするメモリを急増させる場合があります。安全な代替の方法としては、web.config に次の要素を含めることにより、アプリケーション全体でカーネル モード出力キャッシュを無効にします。
<httpRuntime enableKernelOutputCache="false" />また、カーネル モード出力キャッシュは、レジストリ設定によって、グローバル、つまりサーバー全体で無効にすることもできます。詳細については、support.microsoft.com/kb/820129 を参照してください。
セッションに関して何か不可解な現象が発生している場合、私は、どこかのページで出力キャッシュを使用していないかをお客様に確認するようにしています。使用している場合で、さらにホスト OS が Windows Server 2003 である場合は、カーネル モード出力キャッシュを無効にするように提案します。これで、通常、問題は解決します。問題が解決しない場合は、コードにバグがある可能性があります。注意してみてください。
フォーム認証チケットの有効期間
このコードに関する問題がわかりますか。
FormsAuthentication.RedirectFromLoginPage(username, true);一見、何の問題もないように思えますが、このコードは、ASP.NET 1.x アプリケーションでは、アプリケーションのどこかに、このステートメントによる機能低下に対処するコードがない限り、一切使用されるべきではないコードです。その理由について説明します。
FormsAuthentication.RedirectFromLoginPage は、2 つの処理を実行します。まず、これは、FormsAuthenticationModule によってログイン ページにリダイレクトされたときに要求していた、最初のページにユーザーをリダイレクトします。2 つ目に、これは、認証チケットを発行します (通常は、cookie によって配信されます。また、ASP.NET 1.x では、常に cookie によって配信されていました)。このチケットによって、ユーザーは、事前に定義された期間、認証された状態を保つことができます。
問題は、この期間です。ASP.NET 1.x では、RedirectFromLoginPage の第 2 パラメータで false を渡した場合、既定で 30 分で有効期限の切れる一時的な認証チケットが発行されます。(このタイムアウトの時間は、web.config の <forms> 要素の timeout 属性を使用して変更できます。) しかし、第 2 パラメータで true を渡した場合は、50 年間有効である永続的な認証チケットが発行されます。これは、事故の基です。認証チケットが盗まれた場合、盗んだ人は、そのチケットが有効である間、被害者の ID を使用して Web サイトにアクセスできることになります。認証チケットを盗む方法はたくさんあります。たとえば、公共の無線アクセス ポイントでの暗号化されていないトラフィックの盗聴、クロスサイト スクリプティング、および被害者の PC への物理的アクセスの取得などがあります。つまり、RedirectFromLoginPage に true を渡すことは、Web サイトのセキュリティを無効にするのと同じです。この問題は、ASP.NET 2.0 で修正されました。現在の RedirectFromLoginPage は、一時的な認証チケット、および永続的な認証チケットのどちらにも web.config で指定されたタイムアウトを反映します。
ASP.NET 1.x アプリケーションにおいての 1 つの解決策は、RedirectFromLoginPage の第 2 パラメータで true を一切渡さないようにすることです。しかし、この方法はあまり現実的ではありません。ログイン ページは、通常、"次回のために保存" などのチェックボックスを備えており、ユーザーがこれをチェックした場合には、一時的な認証チケットではなく、永続的なチケットを受信できるようになっているためです。これに代わる解決方法は、Global.asax (場合によっては、HTTP モジュール) のコードで、永続的な認証チケットを含む cookies を、それがブラウザに戻る前に変更することです。
図 3 に、このようなコードを示します。Global.asax 内のこのコードは、送信永続的フォーム認証 cookie の Expires プロパティを変更し、cookie の有効期限が 24 時間で切れるようにします。"新しい有効期限日付" とコメントされた行を変更することにより、必要に応じてタイムアウトを設定できます。
Application_EndRequest メソッドがローカル ヘルパー メソッド (GetCookieFromResponse) を呼び出して、送信応答に認証 cookie がないかをチェックしていることが奇妙に思えるかもしれません。このヘルパー メソッドは、ASP.NET 1.1 の別のバグを回避するための手段です。このバグは、HttpCookieCollection の string インデクサを使用して、存在しない cookie をチェックすると、応答に誤った cookie が追加されるというものです。GetCookieFromResponse として integer インデクサを使用することで、この問題が回避できます。
ビュー ステート: 沈黙のパフォーマンス キラー
ビュー ステートは、さまざまな点において非常に優れています。ビュー ステートの第一の機能は、ページ、およびコントロールがポストバックをはさんで状態を保持できるようにすることです。このため、従来の ASP で行っていたように、ボタンのクリックで TextBox のテキストを消失させないためのコードを記述する必要はありません。また、ポストバックの後に、データベースに再クエリを行い、DataGrid に再連結する必要もありません。
しかし、ビュー ステートには欠点もあります。これが大きくなりすぎたときには、沈黙のパフォーマンス キラーとなります。TextBox などのコントロールは、ビュー ステートで適切に処理できます。しかし、他のコントロール、特に、DataGrids、および GridViews は、表示される情報量に比例してビュー ステートを膨らませます。データを 200 行、または 300 行も表示している GridView は注意が必要です。ASP.NET 2.0 ビュー ステートのサイズは、ASP.NET 1.x ビュー ステートのおおよそ半分ですが、適切でない GridView は、それ 1 つでも、ブラウザと Web サーバー間の接続の有効帯域幅の 50 パーセント以上を簡単に消費してしまうことがあります。
ビュー ステートは、EnableViewState を false に設定することにより、コントロールごとに無効にできます。しかし、DataGrids など、コントロールによっては、ビュー ステートを自由に使用できない場合、機能の一部が失われることがあります。ビュー ステートを制御するより良い解決策は、それをサーバー上に保持することです。ASP.NET 1.x では、ページの LoadPageStateFromPersistenceMedium メソッド、および SavePageStateToPersistenceMedium メソッドをオーバーライドし、必要に応じてビュー ステートを処理できます。図 4 に示すコード内のオーバーライドは、ビュー ステートが、非表示フィールドではなく、セッション ステートに保持されるようにします。ビュー ステートのセッション ステートへの保存は、既定のセッション ステート プロセス モデルと組み合わせたときに特に効果があります。このモデルでは、セッション ステートは、ASP.NET ワーカー プロセスのメモリに保存されます。セッション ステートがデータベースに保管される場合は、ビュー ステートをセッション ステートに保持することでパフォーマンスが向上するか、または低下するかは、テストをして確認する必要があります。
ASP.NET 2.0 でも、同じ技法が使用できます。しかし、ASP.NET 2.0 には、ビュー ステートをセッション ステートに保持するためのより簡単な手段があります。まず、次のように、GetStatePersister メソッドが、.NET Framework SessionPageStatePersister クラスのインスタンスを戻す、カスタム ページ アダプタを定義します。
public class SessionPageStateAdapter :
System.Web.UI.Adapters.PageAdapter
{
public override PageStatePersister GetStatePersister ()
{
return new SessionPageStatePersister(this.Page);
}
}
次に、このカスタム ページ アダプタを、既定のページ アダプタとして登録します。登録するには、次のような App.browsers ファイルをアプリケーションの App_Browsers フォルダに配置します。
<browsers>
<browser refID="Default">
<controlAdapters>
<adapter controlType="System.Web.UI.Page"
adapterType="SessionPageStateAdapter" />
</controlAdapters>
</browser>
</browsers>
(ファイル名は、拡張子が .browsers であれば何でも構いません。) これで、ASP.NET は、ページ アダプタを読み込み、戻される SessionPageStatePersister を使用して、ビュー ステートを含めたすべてのページ状態を保持します。カスタム ページ アダプタの使用の欠点として、これは、アプリケーションのすべてのページでグローバルに機能します。一部のページでのみビュー ステートをセッションに保持する場合は、図 4 に示した技法を使用します。また、この技法は、ユーザーが同じセッション内で複数のブラウザ ウィンドウを作成する場合に、問題が発生することがあります。
SQL Server セッション ステート: もう 1 つのパフォーマンス キラー
ASP.NET では、セッション ステートをデータベースに簡単に保管できます。web.config 内のスイッチを切り替えるだけで、セッション ステートはバックエンド データベースに移動します。これは、Web ファームで稼動するアプリケーションにとって重要な機能です。ファーム内のすべてのサーバーが、セッション ステートの 1 つの共通リポジトリを共有できるようになるためです。データベース処理が増加するため、個々の要求のパフォーマンスは低下しますが、このパフォーマンスの低下の代わりに、スケーラビリティが向上します。
これは、適切であり正常に機能しますが、次の点を考慮しましょう。
- セッション ステートを使用するアプリケーションでも、セッション ステートを使用しないページがその大部分を占めることがあります。
- 既定で、ASP.NET セッション ステート マネージャは、要求されたページがセッション ステートを使用するかどうかに関わらず、1 回の要求ごとに、2 回のアクセス、つまり読み取りアクセスと書き込みアクセスをセッション データの保管場所に対して実行します。
つまり、SQL Server セッション ステート オプションを使用する場合、要求ごとに、2 回のデータベース アクセスがコストとして必要になります。これは、セッション ステートの処理を行わないページへの要求でも同じです。これは、サイト全体のスループットに対して、直接的な (かつ、マイナスの) 影響を及ぼします。

図 5 不要なセッション ステート データベース アクセスの除去
これに、どのように対処できるでしょうか。たとえば、セッション ステートを使用しないページでは、それを無効にすることが考えられます。これは、この場合に限らず常に適切な考え方ですが、セッション ステートをデータベースに保管する場合には特に重要になります。図 5 は、無効にする方法を示しています。ページがセッション ステートを一切使用しない場合は、次のように、Page ディレクティブに EnableSessionState="false" を含めます。
<%@ Page EnableSessionState="false" ... %>このディレクティブは、セッション ステート マネージャが、各要求でセッション ステート データベースに対する読み取り、および書き込みを行わないようにします。ページが、セッション ステートからデータを読み取るのみで、書き込みを行わない場合 (つまり、ユーザーのセッションの内容を変更しない場合) は、次のように、EnableSessionState を ReadOnly に設定します。
<%@ Page EnableSessionState="ReadOnly" ... %>最後に、ページが、セッション ステートに対して読み取り、および書き込みのアクセスを必要とする場合は、EnableSessionState 属性を省略するか、または次のように、これを true に設定します。
<%@ Page EnableSessionState="true" ... %>このようにセッション ステートを制御することにより、ASP.NET は、実際に必要な場合にのみセッション ステート データベースにアクセスするようになります。不要なデータベース アクセスを取り除くことは、高パフォーマンス アプリケーションを構築するための第一歩です。
EnableSessionState 属性は、公開されていなかったわけではありません。ASP.NET 1.0 からドキュメントには記載されていますが、実際にこれを使用している開発者はあまりいません。おそらく、これは、メモリを使用する既定のセッション ステート モデルではそれほど重要ではなかったためと考えられます。しかし、SQL Server モデルでは重要です。
キャッシュされないロール
次のステートメントは、ASP.NET 2.0 アプリケーションの web.config ファイルによく見られます。また、ASP.NET 2.0 ロール マネージャを紹介するサンプルでもよく使用されています。
<roleManager enabled="true" />しかし、このステートメントの存在は、パフォーマンスに大きな悪影響を及ぼす場合があります。その理由を説明します。
既定で、ASP.NET 2.0 ロール マネージャは、ロール データをキャッシュしません。ロール マネージャは、ユーザーがロールに所属するか、また所属する場合はどのロールに所属するかを判断する必要があるときは、毎回、ロール データの保管場所に接続します。つまり、ユーザーが認証を済ませた後、ロール データを使用するすべてのページ (たとえば、セキュリティ上の理由から一部を非表示にしたサイト マップを使用するページ、および web.config のロール ベースの URL ディレクティブを使用してアクセスが制限されているページなど) では、ロール マネージャがロール データの保管場所に対してクエリを行う必要があります。ロールがデータベースに保管されている場合、要求ごとに、データベース アクセスが行われることになります。このアクセスは、簡単に取り除くことができます。解決策としては、次のように、ロール マネージャがロール データを cookie にキャッシュするように構成します。
<roleManager enabled="true" cacheRolesInCookie="true" />また、<roleManager> 属性を使用してを制御できるロール cookie の特性は他にもあります。たとえば、cookie を有効に保つ期間 (これは、ロール マネージャがロール データベースに接続する頻度を意味します) を制御できます。ロール cookie は、既定で、署名、および暗号化されるため、セキュリティの危険性は、ゼロにはならないとしても、低く抑えられます。
プロファイル プロパティのシリアル化
ASP.NET 2.0 のプロファイル サービスは、パーソナライズの設定、および言語設定など、ユーザーごとのデータの永続化に関する問題に対応するための、すぐに使用できるように用意されたソリューションです。プロファイル サービスを使用するには、個々のユーザーごとに永続化するプロパティを含む XML プロファイルを定義します。ASP.NET は、同じプロパティを含むクラスをコンパイルし、クラス インスタンスへの厳密に型指定されたアクセスを、ページに追加されたプロファイル プロパティを通して提供します。
プロファイルは柔軟性が高く、プロファイル プロパティとしてカスタム データ型を使用することもできます。しかし、ここに問題が潜んでいます。開発者が陥りやすい、実際にあった例を紹介します。図 6 は、Posts という名前の単純なクラスと、Posts をプロファイル プロパティとして使用するプロファイル定義です。しかし、このクラス、およびこのプロファイルは、実行時に予期しない動作をします。その理由がわかりますか。
問題は、Posts に _count という名前のプライベート フィールドが含まれており、クラス インスタンスを完全に取得したり、戻したりするためには、このフィールドをシリアル化、および逆シリアル化する必要があるという点です。_count はプライベートであり、ASP.NET プロファイル マネージャは、既定で、XML シリアル化を使用してカスタム型のシリアル化、および逆シリアル化を行うため、このフィールドのシリアル化、および逆シリアル化は行われません。XML シリアライザは、パブリックでないメンバを無視します。このため、Posts のインスタンスは、シリアル化、および逆シリアル化されますが、クラスのインスタンスが逆シリアル化されるたびに、_count は 0 にリセットされます。
1 つの解決策としては、_count をプライベートではなく、パブリックにします。また、_count を、パブリックな読み取りおよび書き込み可能プロパティでラップすることもできます。クラスの設計自体を変更しない、最善の解決策としては、Posts をシリアル化可能と指定し (SerializableAttribute を使用します)、さらにプロファイル マネージャが、クラス インスタンスのシリアル化、および逆シリアル化に .NET Framework のバイナリ シリアライザを使用するように構成します。バイナリ シリアライザは、XML シリアライザと異なり、アクセス可能性と関係なくフィールドをシリアル化します。図 7 に、修正したバージョンの Posts クラス、および付随するプロファイル定義を、変更がわかるように示します。
カスタム データ型をプロファイル プロパティとして使用し、そのデータ型にパブリックではないデータ メンバがあり、さらに型のインスタンスを完全にシリアル化するにはそのメンバをシリアル化する必要がある場合は、プロパティの宣言で serializeAs="Binary" 属性を使用し、型が確実にシリアル化されるようにすることに注意してください。この注意を怠ると、完全なシリアル化が行われず、プロファイルが機能しない理由の調査に無駄な時間をかけることになり兼ねません。
スレッド プールの飽和
実際に稼動している ASP.NET ページの中には、データベースへのクエリを実行し、そのクエリが戻るまでに 15 秒以上待つようなページがあります。(15 分かかるクエリも経験したことがあります。) こうした遅延は、戻されるデータのボリュームの影響であり、避けれらない場合もあります。しかし、データベース設計が適切でないことが原因の場合もあります。理由は何であれ、長いデータベース クエリ、または時間のかかるあらゆる種類の I/O 操作は、ASP.NET アプリケーションのスループットに悪影響を及ぼします。
この問題については既に詳細に説明したことがあるため、ここでは簡単に説明します。ASP.NET は、要求の処理に有限なスレッド プールを使用しています。データベース クエリ、Web サービス呼び出し、または他の I/O 操作の完了を待機するためにすべてのスレッドが使用された場合、その後の要求は、操作が完了しスレッドが空くまでキューに入れられます。パフォーマンスは、要求がキューに入ると急激に低下します。さらにキューがいっぱいになった場合、ASP.NET は、後続の要求を HTTP 503 エラーで失敗します。これは、実稼動の Web サーバーの実稼動のアプリケーションでは特に避けたい状況です。
解決方法として、非同期ページの使用があります。これは、ASP.NET 2.0 において、すばらしい機能でありながらあまり知られていないものの 1 つです。非同期ページへの要求は、1 つのスレッド上で開始しますが、I/O 操作が開始されたときに、そのスレッド、および IAsyncResult インターフェイスを ASP.NET に戻します。操作が完了すると、要求は IAsyncResult によって ASP.NET に通知を行い、ASP.NET は、プールから別のスレッドを取得し、要求の処理を終了させます。重要なことは、I/O 操作が行われている間、スレッド プールのスレッドが使用されていない点です。これにより、他のページ (時間のかかる I/O 操作を行わないページも含めて) への要求がキューに入ることを避けられるため、スループットが大幅に向上します。
非同期ページについての詳細は、MSDNマガジン の October 2005 issue の記事を参照してください。計算ではなく、I/O が関連して実行に数秒以上かかるページは、非同期ページとすることを検討してください。
非同期ページを開発者に説明すると、多くの場合、"すばらしいが、このアプリケーションでは必要ない" という答えが返ってきます。私は、次のように聞き返します。"データベースにクエリするページはありますか。Web サービスを呼び出すページはありますか。ASP.NET のパフォーマンス カウンタで、キューに入れられた要求、および平均待機時間に関する統計をチェックしたことがありますか。アプリケーションが現在は正常であるとしても、お客様の増加に伴って負荷が増加する可能性はありませんか。"
実際には、実用的な ASP.NET アプリケーションにおいて、どのような非同期ページも必要としないという状況はめったにないと考えれます。検討してみましょう。
偽装と ACL 認定
これは、簡単な構成ディレクティブです。私は、web.config でこれを見たときは、必ず、慎重に考えるようにしています。
<identity impersonate="true" />
このディレクティブは、ASP.NET アプリケーションにおいてクライアント偽装を有効にします。これは、要求を処理するスレッドに、クライアントを表すアクセス トークンを付属させるため、オペレーティング システムによって実行されるセキュリティ チェックが、ワーカー プロセスの ID ではなく、クライアントの ID に対して行われるようになります。ASP.NET アプリケーションにおいて、偽装が必要になることはめったにありません。私の経験では、開発者は、たいてい間違った理由からこれを有効にしています。理由を説明します。
開発者は、ファイル システムのアクセス許可を使用してページへのアクセスを制限できるようにするために、ASP.NET アプリケーションで偽装を有効にすることが非常によくあります。Bob が、Salaries.aspx を表示する許可を持っていない場合、開発者は、Bob が Salaries.aspx を表示することができないようにするために、Bob の読み取り許可を拒否するアクセス制御リスト (ACL) を設定して、偽装を有効にします。これは、よくある間違いです。ACL 認定には、偽装は必要ありません。ASP.NET アプリケーションにおいて Windows 認証が有効である場合、ASP.NET は、要求された各 .aspx ページの ACL を自動的にチェックし、呼び出し元にファイルを読み取る許可がない場合には要求を拒否します。これは、偽装が無効である場合でも行われます。
偽装することが正しい場合もあります。しかし、適切な設計をすることで、通常は偽装を避けることができます。たとえば、Salaries.aspx は、マネージャのみが使用可能である給与に関する情報をデータベースにクエリするとします。偽装することにより、データベースの許可を使用して、マネージャでないユーザーからの給与データへのクエリを拒否できます。しかし、偽装ではなく、Salaries.aspx の ACL 設定で、マネージャ以外には読み取り許可を与えないことで、給与データへのアクセスを制限することもできます。後者の手法では、偽装をまったくしなくて済むので、パフォーマンスが向上します。また、不要なデータベース アクセスを除去することにもつながります。セキュリティ上の理由で拒否させるためだけに、データベースへのクエリをする必要があるでしょうか。
ついでながら述べておくと、以前に、不用意なメモリ使用のために、定期的に再起動する必要のあった、従来の ASP アプリケーションの原因調査を手伝ったことがあります。ある若手の開発者が、ある SELECT ステートメントを、クエリされるテーブルに大きな画像が多数含まれていることを考慮せずに、SELECT * と変更していました。問題は、気付かれていないメモリ リークによってさらに悪化していました。(私の担当分野はマネージ コードです。) 何年間が正常に稼動していたアプリケーションが、突然、停止するようになった原因は、これまで、1 キロバイトか、2 キロバイトのデータを戻していた SELECT ステートメントが、数メガバイトを戻すようになったことです。不適切なバージョン管理も重なり、開発チームにとってはエキサイティングな日々となりました (夜に眠ったり、子供のサッカーの試合を観たりすることを退屈と考えるのであれば、まさにエキサイティングです)。
理論上、従来のメモリ リークは、全体がマネージ コードで構成される ASP.NET アプリケーションでは発生しません。しかし、非効率なメモリ使用は、ガベージ コレクションを頻繁に発生させることになるので、パフォーマンスに影響を及ぼします。ASP.NET アプリケーションでも、SELECT * には注意しましょう。
データベースのプロファイルの必要性
私は、コンサルタントであるので、たいていは、アプリケーションが想定どおりに稼動しないときに呼び出されます。最近、私たちのチームは、ASP.NET アプリケーションが、要求書で求められるスループット (1 秒あたりの要求数) の 100 分の 1 程度しか達成できない理由を調査する依頼を受けました。このとき明らかになったことは、Web アプリケーションのパフォーマンス低下の典型的な原因です。この教訓は、どのような場合にも注意しておく必要があります。
SQL Server プロファイラを実行し、アプリケーションと、バックエンドのデータベースとのやり取りを確認しました。最も極端な場合には、1 回のボタン クリックによって、データベースとのやり取りが 1,500 回以上も行われていました。このような方法では、高パフォーマンスなアプリケーションを構築することはできません。適切なアーキテクチャは、常に適切なデータベース設計から始まります。コードをどれほど効率的にしても、データベースの設計が適切でなければ、パフォーマンスは良くなりません。
データ アクセスのアーキテクチャが不適切となる原因として、次のような点が考えられます。
- 貧弱なデータベース設計 (通常、データベース管理者ではなく、開発者によって設計されます)。
- DataSets および DataAdapters、特に DataAdapter.Update の使用。これは、Windows フォーム アプリケーション、および他のシック クライアントには適切ですが、通常、Web アプリケーションには向いていません。
- 貧弱な要因分析による、貧弱な設計のデータ アクセス層 (DAL)、および比較的単純な操作の実行での CPU サイクルの大量消費。
問題に対処するには、まずその問題を特定する必要があります。データ アクセスの問題を特定するには、SQL Server プロファイラ、またはそれに相当するツールを実行し、背後で行われている処理を確認します。アプリケーションとデータベース間のトラフィックを検査しなければ、パフォーマンスのチューニングを行ったことにはなりません。ぜひ、試してみてください。驚くべき結果が明らかになるかもしれません。
まとめ
ここでは、実際の ASP.NET アプリケーションの構築プロセスにおいて、発生しやすい問題、およびその解決方法を説明しました。次のステップは、コードを詳細に確認し、ここで説明した問題を回避してみることです。ASP.NET は、Web 開発者にとっての敷居は低いですが、それは、アプリケーションが巧妙かつ確実で、そして高速にならないことの理由にはなりません。よく考え、これらの初心者にありがちなミスをしないように注意しましょう。
図 8 は、この記事で説明した注意点を確認するために使用できる簡単なチェックリストです。セキュリティの注意点についても同様のチェックリストを作成することができます。たとえば、次のようなリストです。
- 機密データを含む構成セクションを暗号化していますか。
- データベース操作に使用される入力をチェック、および検証していますか。また、出力として使用される入力を HTML エンコーディングしていますか。
- 保護されない拡張子を持つファイルを含む仮想ディレクトリがありますか。
これらは、Web サイト、サイトをホストするサーバー、およびそれらが依存するバックエンド リソースの完全性を評価するときに重要な確認事項です。