ベスト プラクティス : SharePoint オブジェクト モデル使用時のコーディング上の一般的な問題

概要 : SharePoint オブジェクト モデルを使用してカスタム コードを記述する開発者が直面する一般的な問題について学習してください (7 印刷ページ)。

Scott Harris、Microsoft Corporation

Mike Ammerlaan、Microsoft Corporation

2007 年 9 月

適用対象 : Windows SharePoint Services 3.0、Windows SharePoint Services 2.0、Microsoft Office SharePoint Server 2007

目次

  • SharePoint オブジェクト モデルに関するコーディング上の一般的な問題の概要

  • SharePoint オブジェクトを破棄する

  • データおよびオブジェクトをキャッシュする

  • スケーラブルなコードを記述する

  • まとめ

  • 追加情報

SharePoint オブジェクト モデルに関するコーディング上の一般的な問題の概要

より多くの開発者が SharePoint オブジェクト モデルを使用してカスタム コードを記述するにつれ、アプリケーションのパフォーマンスに影響する可能性がある一般的な問題に直面します。この記事では、これらの問題のいくつかに取り組み、それらの問題を特定および修正する方法を提言します。

次の領域は、開発者が SharePoint オブジェクト モデルを使用してカスタム コードを記述する際に直面する主要な問題を反映しています。

  • SharePoint オブジェクトを破棄する

  • データおよびオブジェクトをキャッシュする

  • スケーラブルなコードを記述する

SharePoint オブジェクトを破棄する

開発者が SharePoint オブジェクト モデルを使用してカスタム コードを記述する際に直面する可能性がある最大の問題の 1 つは、SharePoint オブジェクトを不適切に破棄することです。SharePoint オブジェクトの破棄の問題全体の詳細については、この記事では扱いませんが、問題を特定する方法、およびその修正方法の概要を示すことができます。SharePoint オブジェクトを破棄するためのベスト プラクティスの詳細については、「ベスト プラクティス: 破棄可能な Windows SharePoint Services オブジェクトの使用」を参照してください。

SharePoint object モデルでは、Microsoft.SharePoint.SPSite オブジェクトと Microsoft.SharePoint.SPWeb オブジェクトが、マネージ コード内に小さなラッパー (サイズは約 2 KB) として作成されます。このラッパーは、その後、平均サイズが 1 ~ 2 MB のアンマネージ オブジェクトを作成します。コードが次のコード例に類似しており、SPWeb.Webs コレクションに 10 個のサブサイトがあるとした場合、それぞれのメモリが平均 2 MB の合計 10 アイテムが作成されます (合計 20 MB)。

public void GetNavigationInfo()
{
   SPWeb oSPWeb = SPContext.Web;

    // .. Get information oSPWeb for navigation .. 

   foreach(SPWeb oSubWeb in oSPWeb.GetSubWebsForCurrentUser())
   {
      // .. Add subweb information for navigation .. 
    }
}

ユーザー負荷が小さいうちは、前述のシナリオでは問題が発生しません。しかし、ユーザー負荷が増大するにつれ、パフォーマンスの低下、ユーザー タイムアウト、予期しないエラー、また場合によっては、SharePoint アプリケーションまたはアプリケーション プールの障害が発生し始める可能性があります。ユーザーがページをヒットするたびに 10 個のオブジェクトが作成されるという前述の例を使用すると、メモリ使用量がいかに急増するかがわかります。

たとえば、次の表では、ユーザーが比較的短時間内にシステムをヒットしたときに、どれだけのメモリ量が割り当てられるかを示します。

表 1. ユーザー数の増加に応じた、最適な場合および最悪の場合のメモリ使用量

ユーザー数

最適な場合

最悪の場合

10

100 MB

200 MB

50

500 MB

1000 MB

100

1000 MB

2000 MB

250

2500 MB

5000 MB

システムをヒットするユーザー数が増大するにつれ、この状況は悪化します。メモリ使用量が増大するにつれ、システムの動作がおかしくなり始めます。たとえば、パフォーマンスが低下したり、アプリケーション プールがリサイクルされるか、iisreset コマンドが発行されるまで、障害状態になったりするなどします。

問題を特定する方法

次の質問を行うことで、この問題を簡単に特定できます。

  1. 特に大きな負荷がかかっているときに、アプリケーション プールが頻繁にリサイクルしますか?

    これは、メモリしきい値に到達したときに、アプリケーション プールがリサイクルするように設定されていることを前提としています。メモリしきい値は、800 MB ~ 1.5 GB (2 GB 以上の RAM があることが前提) に設定する必要があります。アプリケーション プールのリサイクルが 1 GB に近づいたときに発生するように設定すると、最適な結果が得られますが、どのような設定が環境に最も合っているかを実験で試す必要があります。リサイクルの設定が低すぎると、頻繁にアプリケーション プールのリサイクルが行われるため、パフォーマンス上の問題が発生します。設定が高すぎると、ページのスワップ、メモリの断片化、その他の問題のために、パフォーマンス上の問題が発生し始めます。

  2. 特に大きな負荷がかかっているときに、システムのパフォーマンスが低下しますか?

    メモリの使用量が増え始めるにつれ、システムは、メモリのページングやメモリの断片化の処理などで補正する必要があります。

  3. 特に大きな負荷がかかっているときに、システムがクラッシュしたり、タイムアウトやページ使用不可エラーなどの予期しないエラーが発生したりしますか?

    繰り返しますが、メモリ使用率が高くなるか、断片化されると、他の操作にメモリを割り当てられないため、一部の機能が失敗する可能性があります。多くの場合、コードで "メモリ不足" 例外が適切に処理されていないため、false または紛らわしいエラーが発生します。

  4. システムでカスタム Web パーツまたはサードパーティの Web パーツを使用していますか?

    ほとんどの Web パーツ開発者は、SharePoint オブジェクトを破棄する必要があること、およびその理由を認識していません。ガーベジ コレクションによって自動的にこの機能が実行されると考えているようですが、いかなる場合も正しくありません。

4 番と他の 1 つ以上の質問に "はい" と回答した場合、カスタム コードがアイテムを正しく破棄していない可能性がほぼ 90% あります。表 1 からわかるように、アイテムを適切に破棄していない利用頻度の高いページがただ 1 つあるだけで、いくつかの問題が発生します。次に、前述の GetNavigationInfo 機能を修正する方法の例を示します。

public void GetNavigationInfo()
{
   SPWeb oSPWeb = SPContext.Web;

   foreach(SPWeb oSubWeb in oSPWeb.GetSubWebsForCurrentUser()))
   {
      // .. Add subweb information for navigation ..
      oSubWeb.Dispose();
   }
}

foreach ループでは、新しい SPWeb オブジェクトがコレクションから取得されるたびに作成されます。ほとんどの開発者は、オブジェクトがスコープから消えたときにクリーンアップされると考えていますが、SharePoint オブジェクト モデルを使用している場合、クリーンアップは行われません。

問題を引き起こす可能性がある他の問題についても認識しておく必要があります。たとえば、サイトで RootWeb プロパティを呼び出した後、RootWeb.Dispose() メソッドを呼び出して、作成された SPWeb オブジェクトを破棄する必要があります。アイテムを適切に破棄する方法の詳細については、「ベスト プラクティス: 破棄可能な Windows SharePoint Services オブジェクトの使用」を参照してください。

注意

SPContext.Site プロパティまたは SPContext.Web プロパティから直接返されたアイテムは破棄しないでください。破棄すると、SharePoint システムが不安定になり、アプリケーションが失敗する可能性があります。

データおよびオブジェクトをキャッシュする

多くの開発者は、Microsoft .NET Framework キャッシュ オブジェクト (たとえば、System.Web.Caching.Cache) を使用して、メモリをよりうまく活用し、システムのパフォーマンス全体を向上しようとし始めています。ただし、多くのオブジェクトは "スレッド セーフ" ではなく、これらのオブジェクトをキャッシュすると、アプリケーションがクラッシュし、予期しないユーザー エラーや無関係のユーザー エラーが発生する可能性があります。

スレッド セーフではない SharePoint オブジェクトをキャッシュする

開発者は、クエリから返された SPListItemCollection オブジェクトをキャッシュすることで、パフォーマンスとメモリ使用率を向上しようとします。一般的にこれは良い方法ですが、SPListItemCollection オブジェクトには、スレッド セーフでない埋め込みの SPWeb オブジェクトが含まれているため、キャッシュしないようにする必要があります。たとえば、SPListItemCollection オブジェクトがスレッド A でキャッシュされるものとします。次に、他のスレッドがそのオブジェクトを読み込もうとすると、オブジェクトがスレッド セーフでないため、アプリケーションが失敗するか、おかしな動作を行う可能性があります。

詳細については、Microsoft.SharePoint.SPWeb クラスを参照してください。

スレッド同期を使用しない

開発者の中には、マルチスレッド環境 (既定では、インターネット インフォメーション サービスはマルチスレッドです) で実行されていることや、その環境の管理方法を認識していない開発者もいます。次のコード例は、Microsoft.SharePoint.SPListItemCollection オブジェクトが一部の開発者によってどのようにキャッシュされているかを示しています。

public void CacheData()
{
   SPListItemCollection oListItems;

   oListItems = (SPListItemCollection)Cache["ListItemCacheName"];
   if(oListItems == null)
   {
      oListItems = DoQueryToReturnItems();
      Cache.Add("ListItemCacheName", oListItems, ..);
   }
}

前述のコード例で問題となるのは、データ取得のクエリに 10 秒かかる場合、多数のユーザーが同時にそのページをヒットし、すべてが同じクエリを実行して、同時に同じキャッシュ オブジェクトを更新しようとすることです。これによって、同じクエリが 10、50、または 100 回実行されることがあるため、パフォーマンス上の問題が発生したり、複数のスレッドが同じオブジェクトを同時に更新しようとするため、クラッシュ (特に、マルチプロセス、ハイパースレッド コンピュータ上で) したりする可能性があります。この問題を修正するには、コードを次のように変更する必要があります。

public void CacheData()
{
   SPListItemCollection oListItems;

   lock(this)   {
      oListItems = (SPListItemCollection)Cache["ListItemCacheName"];
      if(oListItems == null)
      {
         oListItems = DoQueryToReturnItems();
         Cache.Add("ListItemCacheName", oListItems, ..);
     }
   }
}

注意

if(oListItems == null) コード ブロック内にロックを配置することで、パフォーマンスを若干向上させることができます。これを行った場合、データが既にキャッシュされているかどうかを確認中に、すべてのスレッドを一時停止する必要がなくなります。クエリからデータが返されるまでにどのくらいの時間がかかるかによっては、複数のユーザーが同時にそのクエリを実行する可能性も残っています。これは、特にマルチプロセッサ コンピュータを実行している場合に当てはまります。実行しているプロセッサ数が増え、クエリの実行時間が長くなればなるほど、if() コード ブロック内にロックを配置することで、問題が発生する可能性が高くなります。前述の例では若干パフォーマンスが低下する可能性がありますが、複数のクエリを同時に実行しない唯一の方法です。

このコードは、インターネット インフォメーション サービスで実行されている重要なセクション内の他のすべてのスレッドを一時停止し、オブジェクトが完全にビルドされるまで、他のスレッドがキャッシュされたオブジェクトにアクセスしないようにします。

前述の例は、スレッド同期の問題に対応していますが、スレッド セーフでないオブジェクトをキャッシュしているため、まだ正しいものではありません。スレッドの安全性に対応するには、SPListItemCollection オブジェクトから作成された DataTable オブジェクトをキャッシュできます。たとえば、前述の例を次のように変更します。

public void CacheData()
{
   DataTable oDataTable;
   SPListItemCollection oListItems;

   lock(this)
   {
      oDataTable = (DataTable)Cache["ListItemCacheName"];
      if(oDataTable == null)
      {
         oListItems = DoQueryToReturnItems();
         oDataTable = oListItems.GetDataTable();
         Cache.Add("ListItemCacheName", oDataTable, ..);
      }
   }
}

このコードは、DataTable オブジェクトからデータを取得します。DataTable オブジェクトの使用方法の詳細と使用例、および SharePoint アプリケーションを開発するための他のヒントについては、「Windows SharePoint Services で開発を行うためのヒント」を参照してください。

スケーラブルなコードを記述する

開発者の中には、複数のユーザーを同時に扱うために、スケーラブルなコードを記述する必要があることを認識していない開発者もいます。この良い例としては、各ページのすべてのサイトとサブサイト用に、またはマスタ ページの一部としてカスタム ナビゲーション情報を作成する場合が挙げられます。たとえば、企業内イントラネットに SharePoint サイトがあり、各部門に多数のサブサイトを持つ独自のサイトがある場合、そのコードは次のようになります。

public void GetNavigationInfoForAllSitesAndWebs()
{
   foreach(SPSite oSPSite in SPContext.Current.Site.WebApplication.Sites)
   {
      using(SPWeb oSPWeb  = oSPSite.RootWeb)
      {
         AddAllWebs(oSPWeb );
      }
   }
}
public void AddAllWebs(SPWeb oSPWeb)
{
   foreach(SPWeb oSubWeb in oSPWeb.Webs)
   {
      //.. Code to add items ..
      AddAllWebs(oSubWeb);
      oSubWeb.Dispose();
   }
}

前述のコードはオブジェクトを適切に破棄していますが、同じリストを繰り返し読み取っているため、問題が発生します。たとえば、10 個のサイト コレクションがあり、サイト コレクションごとに平均 20 個のサイトまたはサブサイトがある場合、同じコードを 200 回繰り返すことになります。ユーザー数が少数の場合、極端なパフォーマンスの低下は発生しません。ただし、システムにユーザーを追加すればするほど、問題が悪化します。表 2 にこのことを示します。

表 2. ユーザー数の増加に伴い、繰り返し数が増大する

ユーザー数

繰り返し数

10

2000

50

10000

100

200000

250

500000

コードはシステムをヒットする各ユーザーに対して実行されますが、データはすべてのユーザーに対して同じままです。この影響は、コードが何を実行しているかによって異なる可能性があります。場合によっては、コードを繰り返し実行しても、パフォーマンス上の問題が発生しないことがあります。ただし、前述の例では、システムは、コレクション内の各アイテムに対して、COM オブジェクトの作成 (コレクションからの取得時に SPSite オブジェクトまたは SPWeb オブジェクトが作成される)、このオブジェクトからのデータの取得、およびこのオブジェクトの破棄を行う必要があります。これによって、多数のパフォーマンス オーバーヘッドが生じます。

このコードをよりスケーラブルにしたり、複数ユーザーの環境用に細かく調整したりするには、どうしたらいいでしょうか? これは答えるのが難しい質問です。アプリケーションがどのような目的で設計されているかによって変わります。コードをスケーラブルにする方法をたずねる場合、次のようなことを考慮する必要があります。

  • データは静的 (ほとんど変更されない)、やや静的 (ときどき変更される)、または動的 (常時変更される) か?

  • データはすべてのユーザーに対して同じか、または変わるのか? たとえば、ログオンするユーザー、アクセスされるサイトの部分、または年の期間 (季節情報) によって変わるのか?

  • データに簡単にアクセスできるのか、またはデータが返されるまでに長時間かかるのか? たとえば、データは長時間実行される SQL クエリ、またはデータ転送でネットワーク遅延が発生するリモート データベースから返されるのか?

  • データは公開か、または高レベルのセキュリティが必要か?

  • データのサイズはどの程度か?

  • SharePoint サイトは単一サーバー上またはサーバー ファーム上にあるのか?

前述の質問に対する回答方法によって、コードをよりスケーラブルにし、複数のユーザーを扱えるようにする方法がいくつかあります。この記事の目的は、すべての質問またはシナリオに対する回答を示すことではなく、特定のニーズに適用できるいくつかの考え方を示すことです。次のセクションでは、いくつかの考慮対象領域を示します。

生データをキャッシュする

System.Web.Caching.Cache オブジェクトを使用すると、データをキャッシュできます。このオブジェクトでは、データに対して 1 回クエリを実行し、他のユーザーがアクセスできるようにキャッシュに格納する必要があります。

データが静的な場合、キャッシュを設定して、データを一度だけ読み込んでアプリケーションを再起動するまで期限切れにならないようにするか、1 日に一度読み込んでデータの新鮮さを確保するようにできます。キャッシュ アイテムは、アプリケーションの起動時、最初のユーザー セッションの開始時、または最初のユーザーによる該当データへのアクセス試行時に作成できます。

データがやや静的な場合、キャッシュ アイテムを設定して、作成後特定の秒数、分数、または時間数内に期限切れになるようにできます。これによって、ユーザーが受け入れ可能な期間内にデータを更新できます。データが 30 秒間しかキャッシュされない場合でも、大きな負荷がかかっているときには、パフォーマンスの向上が見られます。コードの実行が、システムをヒットするすべてのユーザーに対して、毎秒複数回でなく、30 秒ごとに 1 回だけ行われるためです。

「データおよびオブジェクトをキャッシュする」で前に説明した問題について必ず検討してください。

表示する前にデータをビルドする

キャッシュ データの使用方法について検討してください。このデータを使用して実行時の決定を行う場合、データを格納する最適な方法は、データを DataSet オブジェクトまたは DataTable オブジェクト内に配置することです。その後、これらのオブジェクトに対してデータのクエリを実行し、実行時の決定を行うことができます。データを使用してリスト、テーブル、または書式設定されたページをユーザーに表示する場合は、表示オブジェクトをビルドし、そのオブジェクトをキャッシュ内に格納することを検討してください。実行時には、キャッシュからオブジェクトを取得し、そのレンダリング関数を呼び出してそのコンテンツを表示するだけです。レンダリングされた出力を格納することもできますが、セキュリティ上の問題が発生する可能性があります。また、キャッシュ アイテムがきわめて大きくなり、ページのスワップやメモリの断片化が多数発生する可能性があります。

単一サーバーまたはサーバー ファーム用にキャッシュする

SharePoint サイトのセットアップ方法によっては、いくつかのキャッシュ上の問題に別々の方法で対応する必要があります。データを常にすべてのサーバー上で同一にする必要がある場合、各サーバーで同じデータが確実にキャッシュされるようにする必要があります。これを確実に行う 1 つの方法は、キャッシュ データを作成し、そのデータを共通のサーバーまたは SQL データベースに格納することです。繰り返しますが、データへのアクセスにかかる時間、共通のサーバーに格納されるデータのセキュリティ上の問題について考慮する必要があります。

共通のサーバー上のデータをキャッシュするビジネス層オブジェクトを作成し、ネットワーク オブジェクトや API で利用可能な各種のプロセス間通信によってそのデータにアクセスすることもできます。

まとめ

SharePoint システムを確実に最適な状態で動作させるには、記述するコードに関する次の質問に回答できる必要があります。

  • コードで SharePoint オブジェクトを適切に破棄していますか?

  • コードでオブジェクトを適切にキャッシュしていますか?

  • コードで正しい種類のオブジェクトをキャッシュしていますか?

  • 必要に応じてコードでスレッド同期を使用していますか?

  • コードは、1000 人のユーザーに対しても、10 人のユーザーの場合と同様に効率的に動作しますか?

コードの記述時にこれらの問題について考慮していれば、SharePoint システムがより効率的に動作し、ユーザーの操作性がかなり向上することがわかります。また、システム内の予期しない失敗やエラーの回避にも役立ちます。

謝辞

この記事のテクニカル レビューで多大な支援をくださった次の諸氏に、感謝の念を示します。James Waymire 氏 (Microsoft Corporation)、Duray Akar 氏 (Microsoft Corporation)、Rashid Aga 氏 (Microsoft Corporation)、Roger Lamb 氏 (Microsoft Corporation)、および Jeff Lin 氏 (Microsoft Corporation)。

追加情報