Microsoft Azure

Microsoft Azure で分散キャッシュを使用する

Iqbal Khan
Jeremiah Talkar

.NET アプリケーションでクラウドを使用するのに Microsoft Azure を選択する開発者が急増しています。Azure は豊富なクラウド機能を備えているだけでなく、Microsoft .NET Framework と完全に統合されます。さらに、Java、PHP、Ruby、Python のアプリにも適合します。Azure に移行するアプリの多くはトラフィック量が多いことから、Azure では高いスケーラビリティが完全にサポートされることが期待されます。スケーラブルな環境で重要な要素となるのが、インメモリ分散キャッシュです。

今回は、この分散キャッシュの概要と、分散キャッシュによって実現できることを説明します。

ここで説明するのは、汎用のインメモリ分散キャッシュ機能で、Azure Cache または NCache for Azure 特有の機能ではありません。Azure に配置されている .NET アプリケーションにインメモリ分散キャッシュを使用すると、以下に示す 3 つの主なメリットが得られます。

  1. アプリケーションのパフォーマンスとスケーラビリティ
  2. ASP.NET のセッション状態、ビュー ステート、およびページ出力のキャッシュ
  3. イベントによる実行時データの共有

アプリケーションのパフォーマンスとスケーラビリティ

Azure では、アプリケーションのインフラストラクチャを容易に拡張できます。たとえば、高いトランザクション負荷が見込まれる場合は、Web ロール、ワーカー ロール、または仮想マシン (VM) を簡単に追加できます。しかしこのように柔軟性がある一方で、データ記憶域が、アプリケーションを拡張する際のボトルネックになります。

ここで役立つのがインメモリ分散キャッシュです。データをいくつでもキャッシュでき、負荷の高いデータベース読み取りを 90% も減らすことができます。データベースにかかるトランザクション負荷も同様に削減できます。その結果、データベースのパフォーマンスが向上し、トランザクションの負荷を増やすことができるようになります。

リレーショナル データベースとは異なり、インメモリ分散キャッシュによる拡張は直線的です。読み取りトラフィックの 90% がデータベースではなくキャッシュに送られるにもかかわらず、通常、インメモリ分散キャッシュがスケーラビリティのボトルネックになることはありません。キャッシュ内のデータはすべて複数のキャッシュ サーバーに分散されます。キャッシュ サーバーは、トランザクション負荷が増加するにつれて簡単に追加することができます。図 1 に、アプリケーションにキャッシュを使用するよう指示する方法を示します。

図 1 .NET アプリケーションでのインメモリ分散キャッシュの使用

// Check the cache before going to the database
  Customer Load(string customerId)
{
// The key for will be like Customer:PK:1000
  string key = "Customers:CustomerID:" + customerId;
  Customer cust = (Customer)Cache[key];
  if (cust == null)
{
// Item not found in the cache; therefore, load from database
  LoadCustomerFromDb(cust);
// Now, add this object to the cache for future reference
  Cache.Insert(key, cust, null,
  Cache.NoAbsoluteExpiration,
  Cache.NoSlidingExpiration,
  CacheItemPriority.Default, null );
}
  return cust;
}

インメモリ分散キャッシュは、リレーショナル データベースよりも高速で、高いスケーラビリティを実現できます。理解しやすいように、図 2 にいくつかパフォーマンス データを示します。ご覧のとおり、スケーラビリティは直線的に向上しています。これをリレーショナル データベースや、ASP.NET セッション状態の記憶域と比べれば、そのメリットを理解できると思います。

図 2 一般的な分散キャッシュのパフォーマンス数値

クラスター サイズ 1 秒あたりの読み取り数 1 秒あたりの書き込み数
2 ノード クラスター 50,000 32,000
3 ノード クラスター 72,000 48,000
4 ノード クラスター 72,000 64,000
5 ノード クラスター 120,000 80,000
6 ノード クラスター 144,000 96,000

ASP.NET のセッション状態、ビュー ステート、およびページ出力のキャッシュ

Azure でインメモリ分散キャッシュを使用すると、ASP.NET セッション状態、ASP.NET ビュー ステート、および ASP.NET 出力のキャッシュにもメリットがあります。ASP.NET セッション状態はどこかに保存する必要がありますが、これはスケーラビリティの大きなボトルネックになる場合があります。Azure では、ASP.NET セッション状態を SQL データベース、Azure テーブル ストレージ、またはインメモリ分散キャッシュに保存することができます。

SQL データベースは、セッション状態を保存するのに最適ではありません。リレーショナル データベースは Blob ストレージを考慮して設計されていないにもかかわらず、ASP.NET セッション状態オブジェクトは Blob として保存されます。その結果、パフォーマンスが低下し、スケーラビリティのボトルネックが生じます。

同様に、Azure テーブル ストレージも Blob ストレージ向きではありません。Azure テーブル ストレージの目的は、構造化されたエンティティの保存です。SQL データベースよりもスケーラブルではありますが、ASP.NET セッション状態の理想的な保存場所ではありません。

Azure の ASP.NET セッション状態を保存するのに最も適しているのがインメモリ分散キャッシュです。他 2 つの選択肢よりも動作が速く、スケーラブルです。また、セッションを複製するため、キャッシュ サーバーが停止してもデータが失われません。分離された専用のキャッシュ層にセッションを保存すると、Web ロールと Web サーバーの VM がステートレスになり、セッション データを失うことなく停止できるようになります。

ASP.NET セッション状態をキャッシュで実行するのはパフォーマンスの点では理想的ですが、キャッシュが停止するとアプリケーション全体も停止します。そして、当然ながら、セッション内のあらゆるものも失われます。最近登場した Azure Redis Cache セッション状態プロバイダーを使用すれば、このような問題が発生するタイミングを事前に察知し、少なくともそのことをわかりやすくユーザーに表示できるようになります。

図 3 に、ASP.NET セッション状態を保存するためにインメモリ分散キャッシュを構成する方法を示します。

図 3 分散キャッシュにおける ASP.NET セッション状態の記憶域の構成

// Check the Cache before going to the database
  Customer Load(string customerId)
{
// The key for will be like Customer:PK:1000
  string key = "Customers:CustomerID:" + customerId;
  Customer cust = (Customer)Cache[key];
  if (cust == null)
{
// Item not found in the cach; therefore, load from database
  LoadCustomerFromDb(cust);
// Now, add this object to the cache for future reference
  Cache.Insert(key, cust, null,
  Cache.NoAbsoluteExpiration,
  Cache.NoSlidingExpiration,
  CacheItemPriority.Default, null );
}
  return cust;
}

今では ASP.NET MVC フレームワークで ASP.NET ビュー ステートを使用する必要がなくなりましたが、ASP.NET アプリケーションの大半はまだ ASP.NET MVC フレームワークに移行していません。そのため、今でも ASP.NET ビュー ステートを必要とするアプリケーションは少なくありません。

ASP.NET ビュー ステートは、帯域幅の大きな負荷となり、ASP.NET アプリケーションの応答時間を目に見えて遅くします。というのも、ユーザー 1 人あたり数百 KB もの負荷がかかるためです。これはポストバックの際に、ブラウザーとの間で不必要に送受信が行われることが原因です。この ASP.NET ビュー ステートを Web サーバー側でキャッシュして一意の識別子をブラウザーに送信するようにすると、応答時間が短くなり、帯域幅を占有する量も減少します。

Azure で、ASP.NET アプリケーションが複数の Web ロールまたは VM で実行されている場合、この ASP.NET ビュー ステートをキャッシュする最も無難な場所がインメモリ分散キャッシュになります。この場合、どの Web サーバーからでもビュー ステートにアクセスできます。インメモリ分散キャッシュの記憶域向けに ASP.NET ビュー ステートを構成する方法を以下に示します。

<!-- /App_Browsers/Default.browser -->
<browsers>
  <browser refID="Default" >
    <controlAdapters>
      <adapter
      controlType="System.Web.UI.Page"
      adapterType="DistCache.Adapters.PageAdapter"/>       
    </controlAdapters>
  </browser>
</browsers>

ASP.NET には、変更される可能性が低いページ出力をキャッシュできる、出力キャッシュ フレームワークも用意されています。これにより、次回の要求からはページを実行する必要がなくなるので、CPU リソースが節約され、ASP.NET の応答時間が短くなります。マルチサーバー配置でページ出力をキャッシュする最適な場所は、分散キャッシュです。こうすると、すべての Web サーバーからページ出力にアクセスできるようになります。さいわい、ASP.NET 出力キャッシュのアーキテクチャはプロバイダーベースなので、インメモリ分散キャッシュを簡単に追加することができます (図 4 参照)。

図 4 インメモリ分散キャッシュを追加するための ASP.NET 出力キャッシュの構成

<!-- web.config -->
<system.web>
  <caching>
    <outputCache defaultProvider="DistributedCache">
      <providers>
        <add name="DistributedCache"
          type="Vendor.Web.DistributedCache.DistCacheOutputCacheProvider,
            Vendor.Web.DistributedCache"
          cacheName="default"
          dataCacheClientName="default"/>
      </providers>
    </outputCache>
  </caching>
</system.web>

イベントによる実行時データの共有

Azure でインメモリ分散キャッシュの使用を検討するもう 1 つの理由は、実行時データの共有です。アプリケーションでは一般的に、以下の方法で実行時にデータを共有します。

  1. データへの変更を検出するためにリレーショナル データベースにポーリングする
  2. データベース イベントを使用する (SqlDependency、OracleDepedency など)
  3. MSMQ などのメッセージ キューを使用する

これらのアプローチはすべて基本的な機能を提供しますが、いずれもパフォーマンスやスケーラビリティに特定の問題があります。不必要なデータベース読み取りが多数行われるため、ポーリングは基本的にお勧めできません。また、データベースは、追加のデータベース イベントがなくても、それ自体がスケーラビリティのボトルネックになります。データベース イベントのオーバーヘッドが伴うと、トランザクション負荷が高い場合データベースが停止する頻度が多くなります。

メッセージ キューは、シーケンス処理されるデータ共有と、固定記憶域へのイベントの保存に特化しています。したがって、受信者が長期間イベントを受け取っていない場合や、アプリケーションが WAN 間に分散されていない場合は有効です。ただし、トランザクションの使用頻度が高い環境では、メッセージ キューはインメモリ分散キャッシュのように実行または拡張できない場合があります。

そのため、(複数のアプリケーションがシーケンス処理なしに実行時データの共有を行う必要があり、イベントを長期間にわたって保持する必要がない) トランザクションの使用頻度が高い環境では、実行時データの共有にインメモリ分散キャッシュを使用することをお勧めします。インメモリ分散キャッシュを使用すると、さまざまな方法 (いずれも非同期) で実行時にデータを共有することができます。以下にその方法を示します。

  1. 更新時および削除時の項目レベルのイベント
  2. キャッシュレベル、およびグループ/リージョン レベルのイベント
  3. 継続的なクエリベースのイベント
  4. トピックベースのイベント (パブリッシュ/サブスクライブ モデル)

最初の 3 つはキャッシュ内のデータ変更を監視するという点では同じですが、本質は異なります。アプリケーションは、イベントごとにコールバックを登録します。分散キャッシュには、キャッシュ内の対応するデータが変更されたとき常に「イベントを発生させる」役割があり、これによりアプリケーションのコールバックが呼び出されます。

項目レベルのイベントは、キャッシュされた特定の項目が更新または削除されたときに発生します。キャッシュレベル、およびグループ/リージョン レベルのイベントは、その "コンテナー" 内のデータが追加、更新、または削除されたときに発生します。継続的なクエリは、分散キャッシュ内のデータセットを定義するための検索条件から構成されます。分散キャッシュは、このデータセットでデータを追加、更新、または削除したとき常にイベントを発生します。以下のように、キャッシュの変更を監視するためにこれを利用できます。

string queryString = "SELECT Customers WHERE this.City = ?";
Hashtable values = new Hashtable();
values.Add("City", "New York");
Cache cache = CacheManager.GetCache(cacheName);
ContinuousQuery cQuery = new ContinuousQuery(queryString, values);
cQuery.RegisterAddNotification(
  new CQItemAddedCallback(cqItemAdded));
cQuery.RegisterUpdateNotification(
  new CQItemUpdatedCallback(cqItemUpdated));
cQuery.RegisterRemoveNotification(
  new CQItemRemovedCallback(cqItemRemoved));
cache.RegisterCQ(query);

トピックベースのイベントは汎用のイベントで、キャッシュ内のデータ変更に限定されるわけではありません。この場合に「イベントを発生させる」役割があるのはキャッシュ クライアントです。分散キャッシュはメッセージ バスのようなものになり、キャッシュに接続されている他のすべてのクライアントにイベントを転送します。

トピックベースのイベントでは、アプリケーションはパブリッシュ/サブスクライブ モデルでデータを共有することができます。パブリッシュ/サブスクライブ モデルでは、1 つのアプリケーションがデータを公開してトピックベースのイベントを発生させます。それ以外のアプリケーションはイベントを待機して、受け取ったらデータの使用を開始します。

分散キャッシュのアーキテクチャ

トラフィック量の多いアプリケーションでは、ダウンタイムが許されません。このようなアプリケーションを Azure で実行する場合は、インメモリ分散キャッシュの以下の 3 つの側面が重要になります。

  1. 高可用性
  2. 直線状のスケーラビリティ
  3. データのレプリケーションと信頼性

キャッシュの弾力性は、インメモリ分散キャッシュを管理する重要な理由の 1 つです。インメモリ分散キャッシュの多くは、弾力性と高可用性を、以下の方法で実現します。

自己回復ピアツーピア キャッシュ クラスター: すべてのキャッシュ サーバーは、1 つのキャッシュ クラスターを形成します。ノードが追加または削除されたときに自身を調整するのが、自己回復ピアツーピア クラスターです。より強力なキャッシュは自己回復ピアツーピア キャッシュ クラスターを形成し、それ以外はマスター/スレーブ クラスターを形成します。ピアツーピアは動的で、キャッシュを停止することなくキャッシュ サーバーを追加または削除することができます。マスター/スレーブ クラスターは、停止している 1 つまたは複数の指定されたノードによってキャッシュ操作が妨げられるため、制限があります。memcached など一部のキャッシュは、どのようなキャッシュ クラスターも形成しないので、弾力性があると見なされません。

接続のフェールオーバー: キャッシュ クライアントは、アプリケーション サーバーと Web サーバーで実行され、キャッシュ サーバーにアクセスするアプリケーションです。接続のフェールオーバー機能はキャッシュ クライアント内にあります。つまり、クラスター内のキャッシュ サーバーが停止すると、キャッシュ クライアントはクラスター内の別のサーバーを探して動作し続けます。

動的構成: キャッシュ サーバーとキャッシュ クライアントの両方にこの機能があります。キャッシュ クライアントが構成の詳細をハードコーディングする必要はなく、キャッシュ サーバーはすべての変更を含むその情報を実行時にキャッシュ クライアントに伝達します。

キャッシュのトポロジ

キャッシュするのは、ASP.NET セッション状態などのデータベースに存在しないデータであることがほとんどなので、データの損失は大きな痛手です。たとえデータベースにデータが存在していても、キャッシュ ノードが停止したときに大量のデータが失われると、アプリケーションのパフォーマンスに大きく影響します。

このため、インメモリ分散キャッシュがデータをレプリケートすると便利です。ただし、レプリケーションを行えば、パフォーマンスが低下し、記憶域も消費されます。優れたインメモリ キャッシュは、データ記憶域とレプリケーションの多様なニーズに対処する一連のキャッシュ トポロジを提供します。以下にそれらを紹介します。

ミラー化されたキャッシュ: このトポロジには、アクティブ キャッシュ サーバーとパッシブ キャッシュ サーバーが 1 つずつあります。すべての読み取りと書き込みはアクティブ ノードに対して行われ、更新はミラーに非同期に適用されます。このトポロジは通常、専用のキャッシュ サーバーを 1 つだけ確保すること、およびアプリケーション/Web サーバーをミラーとして共有することが可能な場合に使用されます。

レプリケーション キャッシュ: このトポロジには、2 つ以上のアクティブ キャッシュ サーバーがあります。キャッシュ全体がすべてのサーバーにレプリケートされます。すべての更新は同期されていて、1 つの操作として全キャッシュ サーバーに適用されます。読み取りトランザクション数は、サーバーが追加されるごとに、直線的に拡張されます。ただし、ノードを追加しても、記憶域や更新トランザクションの容量は増えないのがデメリットです。

パーティション キャッシュ: このトポロジでは、キャッシュ全体がパーティション分割され、いずれのキャッシュ サーバーにも 1 つのパーティションが含まれます。キャッシュ クライアントは通常すべてのキャッシュ サーバーに接続するので、すべてのパーティションのデータに直接アクセスすることができます。サーバーを追加するごとに、記憶域とトランザクションの容量は直線的に増加します。ただし、レプリケーションが行われないので、キャッシュ サーバーが停止するとデータが失われるおそれがあります。

パーティションとレプリケーションの混合キャッシュ: このトポロジはパーティション キャッシュに似ていますが、各パーティションが少なくとも 1 つの異なるキャッシュ サーバーにレプリケートされる点が異なります。このため、キャッシュ サーバーが停止してもデータは失われません。パーティションは通常アクティブで、レプリカはパッシブです。アプリケーションがレプリカを直接操作することはありません。このトポロジは、パーティション キャッシュのように直線状に拡張するメリットを提供するだけでなく、データの信頼性も確保します。レプリケーションによって、わずかにパフォーマンスが低下し、記憶域が消費されます。

クライアント キャッシュ (ニア キャッシュ): キャッシュ クライアントは通常、アプリケーション/Web サーバーで実行されるので、キャッシュへのアクセスにはネットワーク トラフィックが関係するのが一般的です。クライアント キャッシュ (ニア キャッシュ) は、頻繁に使用されるデータをアプリケーションの近くに保持し、ネットワーク上のやり取りを抑えるローカル キャッシュです。このローカル キャッシュは、分散キャッシュに接続および同期されます。クライアント キャッシュは、InProc (アプリケーションのプロセス内) か、OutProc に設定できます。

Azure で分散キャッシュを配置する

Azure には、Microsoft Azure Cache、NCache for Azure、memcached など、複数の分散キャッシュが用意されており、どのキャッシュにも多様なオプションがあります。以下に示すのは、単一のリージョンで使用される場合に最も一般的な配置オプションです。

  1. In-Role Cache (併置または専用)
  2. Cache Service
  3. キャッシュ VM (専用)
  4. WAN 経由のキャッシュ VM (複数リージョン)

In-Role Cache: Azure では、併置または専用ロールで In-Role Cache を配置できます。「併置」とはアプリケーションが VM でも実行されているという意味で、「専用」はキャッシュのみで実行されているという意味になります。優秀な分散キャッシュは弾力性と可用性に優れていますが、キャッシュ クラスターにキャッシュ サーバーを追加したり削除すると、オーバーヘッドが生じます。そのため、キャッシュ クラスターは安定させるようにします。キャッシュ サーバーを追加または削除するのは、キャッシュ容量を増減する場合、またはキャッシュ サーバーが停止した場合のみに限定します。

Azure では簡単にロールを開始および停止できるので、In-Role Cache は他の配置オプションよりも不安定です。併置ロールでは、キャッシュは CPU リソースもメモリ リソースもアプリケーションと共有します。1 ~ 2 個の配置にはこのオプションを使用してもかまいませんが、パフォーマンスに悪影響を与えるため多数の配置には適していません。

専用の In-Role Cache を使用することも検討します。このキャッシュは、クラウド サービスの一環として配置され、サービス内でのみ可視になることに注意してください。このキャッシュを複数のアプリケーションで共有することはできません。また、このキャッシュは、サービスの稼働中のみ実行されます。そのため、アプリケーションを停止したときにもキャッシュを実行する必要がある場合、このオプションは使用できません。

Microsoft Azure Cache と NCache for Azure は、いずれも In-Role Cache を配置オプションとして提供します。いくらか微調整すれば memcached でもこの構成を実行できますが、memcached はデータをレプリケートしないので、ロールが再利用されるとデータが失われます。

Cache Service: Cache Service では、分散キャッシュがサービスとは独立して配置され、キャッシュを視覚的に確認できるビューが提供されます。この配置オプションでは、一定量のメモリと CPU 容量を割り当て、独自のキャッシュを作成します。Cache Service のメリットは、その簡潔さにあります。自分で分散キャッシュ ソフトウェアをインストールして構成する必要がありません。Azure が分散キャッシュ クラスターを代わりに管理するため、キャッシュ管理にかかる労力が軽減されます。このキャッシュを複数のアプリケーションから共有できるのもメリットの 1 つです。

Cache Service のデメリットは、アクセスに制限があるところです。オンプレミスの分散キャッシュとは異なり、クラスター内のキャッシュ VM を制御または操作することはできません。また、リードスルー、ライトスルー、キャッシュ ローダーなどのサーバー側コードを配置することができません。さらに、クラスター内のキャッシュ VM の数は Azure によって処理されるため制御できません。配置オプションは Basic、Standard、および Premium の中からのみ選択することができます。Microsoft Azure Cache は Cache Service を配置オプションとして提供しますが、NCache for Azure は提供しません。

キャッシュ VM: Azure に分散キャッシュを配置するもう 1 つのオプションです。こうすれば、分散キャッシュを完全に制御できます。Web ロール、ワーカー ロール、または専用 VM にアプリケーションを配置するときに、分散キャッシュ (ライブラリ) のクライアント部分を配置することもできます。また、ロールを作成するときに、Windows インストーラーを使用してキャッシュ クライアントをインストールすることができます。このため、OutProc クライアント キャッシュ (ニア キャッシュ) などの、より多くのインストール オプションが使用可能になります。

その後は、キャッシュ サーバーとして別の専用 VM を割り当て、分散キャッシュ ソフトウェアをインストールして、ニーズに合わせてキャッシュ クラスターを構築できます。これらの専用キャッシュ VM は安定していて、アプリケーションが停止しても実行し続けます。キャッシュ クライアントは、TCP プロトコルを通じてキャッシュ クラスターと通信します。キャッシュ VM には、以下 2 つの配置シナリオがあります。

  1. 仮想ネットワーク内に配置する: すべてのアプリケーション ロール/VM およびキャッシュ VM が、同じ仮想ネットワーク内に存在します。アプリケーションと分散キャッシュの間にエンドポイントはありません。このため、キャッシュ アクセスの速度が上がります。また、キャッシュは外界から完全に隠ぺいされるため、安全性は高くなります。
  2. 分離された仮想ネットワーク内に配置する: アプリケーション ロール/VM およびキャッシュ VM が、異なる仮想ネットワーク内に存在します。分散キャッシュは独自の仮想ネットワーク内にあり、エンドポイントを通じて公開されます。このため、キャッシュを複数のアプリケーションと複数のリージョンで共有することができます。ただし、Cache Service よりもキャッシュ VM の方がより細かく制御できます。

これら 2 つのキャッシュ VM の配置オプションはいずれも、すべてのキャッシュ サーバーに完全にアクセスすることができます。これにより、オンプレミスの配置と同じように、リードスルー、ライトスルー、キャッシュ ローダーなどのサーバー側コードを配置することが可能です。また、キャッシュ VM に完全にアクセスできるので、監視情報をより詳細に得られます。Microsoft Azure Cache はキャッシュ VM を配置オプションとして提供しませんが、NCache for Azure と memcached では提供されます。

現在、Managed Cache Service が一般提供されていて、これに伴い既存の Shared Caching Service は廃止される予定です。マネージ キャッシュの作成は、Azure ポータルではサポートされないため、Windows PowerShell コマンド ラインを使用して作成後、ポータルで管理する必要があります。

Web ロールおよびワーカー ロールから NCache for Azure にアクセスするには、専用 VM に NCache for Azure をインストールします。Web ロールとワーカー ロールに NCache for Azure をインストールすることもできますが、このやり方はお勧めできません。NCache for Azure はキャッシュ サービスとして提供されていないためです。また、Microsoft Azure には Azure Redis Cache も用意されていて、管理ポータルから利用できます。

WAN 経由のキャッシュ VM: クラウド サービスを複数のリージョンに配置している場合は、WAN 経由でキャッシュ VM を配置することを検討します。各リージョンでのキャッシュ VM の配置については、先ほど説明した内容のとおりです。ただし、以下に示す 2 つの要件が必要になる場合もあります。

  1. 複数サイトにおける ASP.NET セッション: ユーザーが再ルーティングされた場合、ASP.NET セッションを、あるサイトから別のサイトに移行することができます。これは、複数のアクティブなサイトに配置されているアプリケーションによくあるパターンです。このようなアプリケーションは、オーバーフローに対処する目的か、1 つのサイトが停止していることを理由に、トラフィックを再ルーティングします。
  2. キャッシュの WAN レプリケーション: キャッシュの更新をすべて、あるサイトから別のサイトにレプリケートすることができます。これは、アクティブ - パッシブ、またはアクティブ - アクティブの複数サイト配置になります。更新は、アクティブ - パッシブでは一方向に、アクティブ - アクティブでは双方向にレプリケートされます。WAN レプリケーションにより複数のサイトでキャッシュの同期が保たれますが、帯域幅が消費されることに注意してください。

重要なキャッシュ機能

インメモリ分散キャッシュは、使用時に大量のデータを処理します。基本的なインメモリ分散キャッシュは、ハッシュテーブル (キー、値) インターフェイスのみを提供します。エンタープライズレベルの分散キャッシュの中には、それに加えて以下の機能を提供するものもあります。

検索機能: 常にキーに基づいてデータを検索するよりも、別の論理データに基づいてキャッシュを検索する方がずっと簡単です。たとえば、多くの分散キャッシュでは、キャッシュされた項目を論理的にグループ化するため、グループとタグを使用することができます。また、.NET Framework のオブジェクト検索でよく使用される LINQ によってキャッシュを検索できる分散キャッシュも多くあります。中には、キャッシュを検索できる SQL のようなクエリ言語である、独自のオブジェクト照会言語 (OQL) を提供するものもあります。キー以外に属性に基づいて分散キャッシュを検索できると、分散キャッシュをリレーショナル データベースのように扱えます。図 5 に、そのような検索を実行する方法を示します。

図 5 分散キャッシュの LINQ 検索の構成

public IList<Customer> GetCutomers(string city)
{
  // Use LINQ to search distributed cache
  IQueryable<Customer> customers = 
    new DistCacheQuery<Customer>(cache);
  try {
    var result = from customer in customers
      where customer.City = city
      select customer;
    IList<Customer> prods = new List<Customer>();
    foreach (Customer cust in result) {
      customers.Add(cust);
    }
  }
  return customers;
}

リードスルーとライトスルー/後書き: 永続コードの大きなブロックをキャッシュ クラスターに移行したので、リードスルー ハンドラーとライトスルー ハンドラーによってアプリケーション コードが簡潔になります。アプリケーションは、キャッシュが自身のデータ ストアであると単純に推測します。これは、複数のアプリケーションでコードを再利用する 1 つの方法です。

アプリケーションが、キャッシュにないもの ("miss" といいます) をフェッチしようとするとき、キャッシュは常にリードスルーを呼び出します。miss が発生したとき、キャッシュはリードスルー ハンドラーを呼び出し、データベースからデータをフェッチします (図 6 参照)。分散キャッシュは、それをキャッシュに格納して、アプリケーションに返します。

図 6 分散キャッシュのリードスルー ハンドラー

public class SqlReadThruProvider : IReadhThruProvider
{
  // Called upon startup to initialize connection
  public void Start(IDictionary parameters) { ... }
  // Called at the end to close connection
  public void Stop() { ... }
  // Responsible for loading object from external data source
  public object Load(string key, ref CacheDependency dep)
  {
    string sql = "SELECT * FROM Customers WHERE ";
    sql += "CustomerID = @ID";
    SqlCommand cmd = new SqlCommand(sql, _connection);
    cmd.Parameters.Add("@ID", System.Data.SqlDbType.VarChar);
      // Extract actual customerID from "key"
      int keyFormatLen = "Customers:CustomerID:".Length;
      string custId = key.Substring(keyFormatLen, 
        key.Length - keyFormatLen);
      cmd.Parameters["@ID"].Value = custId;
    // Fetch the row in the table
    SqlDataReader reader = cmd.ExecuteReader();
    // Copy data from "reader" to "cust" object
    Customers cust = new Customers();
    FillCustomers(reader, cust);
    // Specify a SqlCacheDependency for this object
    dep = new SqlCacheDependency(cmd);
    return cust;
  }
}

また、データベースを自動的に更新するようにキャッシュを更新したとき、キャッシュによって常にライトスルー ハンドラーが呼び出されます。ライトスルー ハンドラーは、クラスター内の 1 つ以上のキャッシュ サーバーで実行され、データベースと通信します。ただし、ライトスルーが、遅延の後に、キャッシュ更新トランザクションとは関係なく非同期に呼び出される場合、この操作は「後書き」と呼ばれます。データベースの更新が完了するのを待機する必要がないため、後書きはアプリケーションのパフォーマンスを向上します。

キャッシュとリレーショナル データベースの同期: 分散キャッシュ内のほとんどのデータは、アプリケーション データベースのデータです。つまり、データのコピーは、データベース内のマスター コピーと、分散キャッシュ内のコピーの 2 つが存在していることになります。データベースのデータを直接更新し、インメモリ分散キャッシュにアクセスしないアプリケーションがあると、キャッシュ内のデータが古くなります。

分散キャッシュの中には、キャッシュのデータが古くならないよう、データベースとの同期機能を提供するものもあります。この同期機能は、イベント駆動型 (SqlDependency などのデータベース通知を使用) のものと、ポーリングベースのものがあります。ほぼリアルタイムに同期されるのがイベント駆動型ですが、キャッシュされた各項目に対し、データベース サーバーで別個の SqlDependency が作成されるので、オーバーヘッドが増えます。ポーリングは、1 つのデータベース呼び出しによって数千個もの項目を同期できるので、より効率的です。しかし、不必要なポーリング呼び出しでデータベースがあふれ返ってしまうのを防ぐため、同期に遅延が発生することが多くなります。つまり、ほぼリアルタイムに行われるデータベース同期、またはわずかな遅延はあるがより効率的なポーリングベースの同期のいずれかを選択することになります。

分散キャッシュでのリレーショナル データの処理: インメモリ分散キャッシュは、キーと値のオブジェクト ストアですが、その中でキャッシュされるほとんどのデータは、リレーショナル データベースのデータです。このデータには、一対多、一対一、および多対多のリレーションシップがあります。リレーショナル データベースは、参照整合性の制約、および連鎖更新と連鎖削除によってこれらのリレーションシップを強制しますが、キャッシュにも同様の機能が必要です。

1 つのキャッシュされた項目が別のキャッシュされた項目に依存するようにするためには、CacheDependency を使用します。別のキャッシュされた項目が更新または削除されたら、元のキャッシュされた項目が自動的に削除されます。CacheDependency は連鎖削除のように動作し、キャッシュのオブジェクト間の一対一および一対多リレーションシップを処理するのに便利です。一部のインメモリ分散キャッシュは、この機能も実装しています。図 7 に、CacheDependency を構成する方法を示します。

図 7 キャッシュ内のリレーションシップを管理するために CacheDependency を使用

public void CacheCustomerAndOrder(Customer cust, Order order)
{
  Cache cache = HttpRuntime.Cache;
  // Customer has one-to-many with Order. Cache customer first
  // then cache Order and create CacheDependency on customer
  string custKey = "Customer:CustomerId:" + cust.CustomerId;
  cache.Add(custKey, cust, null,
    Cache.NoAbsoluteExpiration,
    Cache.NoSlidingExpiration,
    CacheItemPriority.Default, null);
  // CacheDependency ensures order is removed if Cust updated or removed
  string[] keys = new string[1];
  keys[0] = custKey;
  CacheDependency dep = new CacheDependency(null, keys);
  string orderKey = "Order:CustomerId:" + order.CustomerId
    + ":ProductId:" + order.ProductId;
  // This will cache Order object with CacheDependency on customer
  cache.Add(orderKey, order, dep,
    absolutionExpiration,
    slidingExpiration,
    priority, null);
}

まとめ

Microsoft Azure は、強力で拡張可能なクラウド プラットフォームです。この環境において重要な要素となるのが、インメモリ分散キャッシュです。.NET Framework でアプリケーションを記述した方は、.NET 分散キャッシュの使用をぜひ検討してください。Java でアプリケーションを記述した方は、Java ベースの分散キャッシュ ソリューションが多数存在しているので、ニーズに合ったものを選ぶことができます。.NET Framework と Java を併用してアプリケーションを記述した方には、両者をサポートし、データの移植性を提供する分散キャッシュをお勧めします。ほとんどのキャッシュは完全に .NET か Java のいずれかに特化していますが、両者をサポートするものもあります。

PHP、Ruby、Python などのアプリケーションには、memcached を使用します。memcached では、これらすべての環境がサポートされます。memcached は弾力性のあるキャッシュではないので、可用性やデータ信頼性の面で足りない部分もあります。いずれにしても、分散キャッシュは運用環境において慎重に扱うべき部分であることを念頭に置いてください。したがって、お使いの環境で使用できるすべてのキャッシュ ソリューションを完全に評価して、ニーズに最も合ったものを選ぶ必要があります。


Iqbal Khan は、.NET および Microsoft Azure 分散キャッシュ製品の NCache を提供する、Alachisoft のテクノロジ エバンジェリストです。連絡先は、iqbal@alachisoft.com (英語のみ) です。

Jeremiah Talkar は、マイクロソフトの開発者およびプラットフォーム エバンジェリズム コーポレート チームの Microsoft Azure テクノロジ エバンジェリストで、製品エンジニアリング、コンサルティング、および販売の分野で 26 年の経験があります。連絡先は jtalkar@microsoft.com (英語のみ) です。

この記事のレビューに協力してくれた技術スタッフの Scott Hunter、Masashi Narumoto、および Trent Swanson に心より感謝いたします。