クラウド コンピューティング
Windows Azure での複数ノードの同期
Josh Twist
クラウドはテクノロジが大きく変化していくことを表し、業界専門家の多くは、十年に一度の規模の変化になると予測しています。クラウドが実現する多くのメリットを考えると、これほどの盛り上がりも当然と言えます。ランニング コストの大幅な削減、高可用性、ほぼ無制限のスケーラビリティなどは、クラウドのメリットのごく一部にすぎません。
もちろん、このような変化の結果として、業界では多数の課題に直面します。特に現在、多くの開発者が課題に直面しています。たとえば、クラウド独特の機能を利用するのに最適なシステムを構築するにはどうすればよいでしょう。
都合の良いことに、マイクロソフトが 2 月に Windows Azure Platform をリリースしました。このプラットフォームには、高可用性を維持しつつ莫大な数のユーザーをサポートできるアプリケーションの作成に役立つ、適切な規模の解決策が多数用意されています。しかし、クラウドに配置したときにアプリケーションの能力が最大限に発揮されるよう、融通性というクラウドのほぼ間違いなく最高の能力を活用できるかどうかは、そのシステムの開発者にかかっています。
融通性はクラウド プラットフォームの特性です。数か月ではなく数分単位で Web ファームに新しいサーバーを追加できるため、追加のリソース (処理能力、ストレージなど) を需要に応じて準備できます。同様に、追加したリソースをすぐに除去できることも重要です。
クラウド コンピューティングの基本理念は、使用量に応じて課金する従量制ビジネス モデルです。Windows Azure では、ノード (仮想マシンで実行している Web ロールまたはワーカー ロール) の配置時間に応じて課金されるため、ノードが不要になったときや業務の閑散期にノード数を削減することが、コスト削減に直結します。
したがって、システム管理者による入力や構成を最小限に抑え、追加ハードウェアの準備に自動的に対応する、融通性のあるシステムを作成することが非常に重要です。
シナリオ 1: 注文番号の作成
先日、私は非常に幸運にも、Windows Azure を使用して既存の Web アプリケーション インフラストラクチャをクラウドに移行することを目指した概念実証に取り組みました。
アプリケーションのデータがパーティション分割に適した性質を備えていたことから、最有力候補は Windows Azure テーブル ストレージでした。このパフォーマンスの高い、シンプルなストレージ メカニズムは、ほぼ無制限にスケーラビリティをサポートすることでは最適な選択でしたが、1 つだけ一意識別子に関して大きな問題点がありました。
移行対象のアプリケーションでは、ユーザーが注文を行うと、注文番号が発行されていました。SQL Server か SQL Azure を使用すれば、単純な数値形式の一意識別子を簡単に生成できましたが、Windows Azure テーブル ストレージには自動インクリメントされる主キーがありません。代わりに、Windows Azure テーブル ストレージを使用する開発者は、次のような GUID を作成してテーブルの "キー" として使用することになります。
505EAB78-6976-4721-97E4-314C76A8E47E
GUID を使用する場合の問題は、ユーザーが扱いにくいことです。電話越しにオペレーターに GUID の注文番号を読み上げたり、スケジュール帳にメモを取ったりする必要がある場合を想像してみてください。当然、GUID は同時にすべてのコンテキストで一意になる必要があるため、非常に複雑になります。しかし、注文番号が一意になる必要があるのは Orders テーブル内だけです。
Windows Azure での簡易一意 ID の作成
GUID の問題に対して、次のような比較的単純な解決策を多数検討しました。
- SQL Azure を使用した一意 ID の生成: さまざまな理由から、概念実証では既に SQL Azure よりも Windows Azure テーブル ストレージを高く評価していました。これは主に、それぞれがデータを処理する多数のスレッドを備えた多数のノードにシステムをスケールアウトする必要があったためです。
- ブロブ ストレージを使用したインクリメント値の管理: Windows Azure ブロブ ストレージに一元管理用のカウンターを格納します。ノードで注文番号を読み取って更新し、単純な連続注文番号を生成するメカニズムが提供し、複数ノードから使用できるようにします。しかし、新しい注文番号が毎秒多数必要となる負荷の高いシステムでは、このメカニズムで競合が発生すると、システムのスケーラビリティが低下する可能性があります。
- ノード間での一意 ID の振り分け: 一意注文番号を生成する、軽量のメモリ内カウンターを作成します。すべてのノードで番号が一意になるよう、各ノードに注文番号の範囲を割り当てます (図 1 参照)。
図 1 ID が一意になるよう各ノードに割り当てる注文番号の範囲
ノード | 範囲 |
A | 0 ~ 1,000,000 |
B | 1,000,001 ~ 2,000,000 |
しかし、この方法では多数の問題が発生します。割り当てられた範囲の番号を使い切ってしまったらどうなるのでしょう。一度に数百ものノードがシステムに追加された場合はどうなるのでしょう。ノードがクラッシュし、Windows Azure ランタイムによって新しいノードに置き換えられた場合はどうなるでしょう。この方法では、管理者が割り当て済みの範囲をしっかりと監視して、構成を適切に保ったり、データの破損に備えたりすることに注意を払う必要があります。
これらの方法よりもはるかに洗練された方法、つまり、ノード単位の構成が不要で、競合がほとんど発生せず、必ず一意性が保証される解決策が必要です。そこで、私は 2 つ目と 3 つ目の解決策を組み合わせる解決策を作成しました。
この解決策の考え方はかなり単純で、ブロブ ストレージ内の小さなテキスト ファイルに最後に発行した注文番号を格納します。新しい注文番号が必要になると、ノードからこのブロブにアクセスして、値をインクリメントし、その値をストレージに書き戻します。もちろん、この読み取り/増加/書き込みの処理中に、同じ目的で別のノードがブロブにアクセスする可能性は十分あります。なんらかの方法で同時実行を制御しないと、注文番号が一意にならず、データが破損するおそれがあります。今までなら、複数のノードがブロブを同時に操作しないように、ロック メカニズムを作成することを検討していたはずです。しかし、ロックは負荷が高いため、スループットと高いスケーラビリティを実装の指針となるテーマとしている場合、ロックは避けるべきです。
したがって、ロックではなく、オプティミスティック同時実行制御を使用する方が適しています。オプティミスティック同時実行制御では、複数のアクターからリソースを操作できます。アクターがリソースを取得すると、リソースのバージョンを示すトークンがアクターに発行されます。更新の実行時には、変更対象のリソースのバージョンを示すためにこのトークンを含めることができます。既に別のアクターがリソースを変更していると、更新が失敗し、元のアクターが最新バージョンを取得して更新を再試行できます。更新時に競合が発生する可能性が低い場合、オプティミスティック同時実行制御は適切に機能します。ロックのコストと複雑さを回避でき、リソースを破損から保護できます。
ピーク時にはシステムで新しい注文番号が毎秒約 100 件発行されるとします。この場合、毎秒 100 件のブロブ更新要求が発生するため競合が発生する可能性が非常に高くなり、結果として何回も再試行が繰り返されることで悪循環に陥ります。そこで、競合が発生する可能性を下げるために、一定範囲の注文番号を割り当てることにしました。
この動作をカプセル化するために作成したクラスが、UniqueIdGenerator クラスです。このクラスでは、構成可能なチャンク内の値をインクリメントすることで、一定範囲の注文番号をブロブ ストレージから削除します。各 UniqueIdGenerator クラスで同時に 1,000 件の注文番号を確保すれば、ブロブは平均で 10 秒おきにのみ更新されることになるため、競合が発生する可能性が大きく低下します。各 UniqueIdGenerator クラスでは、確保した 1,000 件の注文番号を自由に発行でき、同じブロブ リソースを参照するこのクラスの他のインスタンスは、同じ注文番号を発行しません。
この新しいコンポーネントを簡単にテストできるように、UniqueIdGenerator クラスを特定のストレージ メカニズムから分離する IOptimisticSyncStore というインターフェイスを指定しました。このインターフェイスには他にもメリットがあり、将来、このコンポーネントでは、さまざまな種類のストレージを必要に応じて使用することもできます。このインターフェイスを次に示します。
public interface IOptimisticSyncStore
{
string GetData();
bool TryOptimisticWrite(string data);
}
ご覧のとおり、これはメソッドを 2 つだけ備えた非常に単純なインターフェイスです。1 つ目のメソッドはデータを取得し、2 つ目のメソッドはデータを更新します。2 つ目のメソッドはブール値を返し、false の場合は、オプティミスティック同時実行制御のエラーが発生していて更新を再試行する必要があることを表します。
ブロブ ストレージを使用する IOptimisticSyncStore インターフェイスの実装は、コード サンプルに含めています (詳細についてはこの記事の最後を参照してください)。実装のほとんどの部分は簡単ですが、オプティミスティック同時実行制御の実装方法を理解するには、TryOptimisticWrite メソッドの詳細を確認することをお勧めします。
必須条件とエンティティ タグ (ETag) があるため、Windows Azure ブロブ ストレージ内のリソースを更新する際にオプティミスティック同時実行制御を使用するのは簡単です。必須条件は、HTTP 要求が成功するには true になる必要があると開発者が宣言するステートメントです。Web サーバーでステートメントが false に評価されると、サーバーは HTTP ステータス コード 412 "必須条件に失敗しました" を返します。ETag も HTTP 仕様に含まれており、ブロブなどのリソースの特定のバージョンを識別します。次のように、ブロブを変更する場合は ETag も変更します。
try
{
_blobReference.UploadText(
data,
Encoding.Default,
new BlobRequestOptions {
AccessCondition = AccessCondition.IfMatch(
_blobReference.Properties.ETag) });
}
必須条件をコードで指定するには、BlobRequestOptions 型を使用して、AccessCondition プロパティを設定します。次のように、このアクセス条件が満たされていない場合 (たとえば、ブロブを取得した直後に別のノードによってそのブロブが更新された場合)、ETag が一致せず、StorageClientException がスローされます。
catch (StorageClientException exc)
{
if (exc.StatusCode == HttpStatusCode.PreconditionFailed)
{
return false;
}
else
{
throw;
}
}
return true;
実装では例外に PreconditionFailed ステータス コードがあるかどうかチェックして、このインスタンスに false を返します。他の種類の例外は重大なエラーなので、処理とログ記録を続行するために再スローします。例外が発生していない場合は更新が実行されたことになるため、true を返します。図 2 に、UniqueIdGenerator クラスのコード全体を示します。
図 2 UniqueIdGenerator クラス全体
public class UniqueIdGenerator
{
private readonly object _padLock = new object();
private Int64 _lastId;
private Int64 _upperLimit;
private readonly int _rangeSize;
private readonly int _maxRetries;
private readonly IOptimisticSyncStore _optimisticSyncStore;
public UniqueIdGenerator(
IOptimisticSyncStore optimisticSyncStore,
int rangeSize = 1000,
int maxRetries = 25)
{
_rangeSize = rangeSize;
_maxRetries = maxRetries;
_optimisticSyncStore = optimisticSyncStore;
UpdateFromSyncStore();
}
public Int64 NextId()
{
lock (_padLock)
{
if (_lastId == _upperLimit)
{
UpdateFromSyncStore();
}
return _lastId++;
}
}
private void UpdateFromSyncStore()
{
int retryCount = 0;
// maxRetries + 1 because the first run isn't a 're'try.
while (retryCount < _maxRetries + 1)
{
string data = _optimisticSyncStore.GetData();
if (!Int64.TryParse(data, out _lastId))
{
throw new Exception(string.Format(
"Data '{0}' in storage was corrupt and " +
"could not be parsed as an Int64", data));
}
_upperLimit = _lastId + _rangeSize;
if (_optimisticSyncStore.TryOptimisticWrite(
_upperLimit.ToString()))
{
return;
}
retryCount++;
// update failed, go back around the loop
}
throw new Exception(string.Format(
"Failed to update the OptimisticSyncStore after {0} attempts",
retryCount));
}
}
コンストラクターは 3 つのパラメーターを受け取ります。最初のパラメーターは、先ほど説明した BlobOptimisticSyncStore など、IOptimisticSyncStore の実装です。2 つ目のパラメーターは rangeSize で、ブロブから割り当てる番号の範囲の大きさを表す整数値です。この範囲が大きくなるほど、競合が発生する可能性が低下します。ただし、このノードがクラッシュした場合に失われる番号の数も増加します。最後のパラメーターは maxRetries です。これは、オプティミスティック同時実行制御のエラー発生時にジェネレーターでブロブの更新を試行する回数を示す整数値です。この回数を超えると、例外を発生します。
NextId メソッドは、UniqueIdGenerator クラス唯一のパブリック メンバーで、次の一意番号を取得するために使用します。クラスのすべてのインスタンスがスレッドセーフになるように、また、たとえば Web アプリケーションを実行しているすべてのスレッドからインスタンスを共有できるように、メソッドの本体は同期されます。if ステートメントでは、ジェネレーターが範囲の割り当ての上限に達したかどうか確認し、達している場合は UpdateFromSyncStore メソッドを呼び出してブロブ ストレージから新しい範囲を取得します。
最後の UpdateFromSyncStore メソッドは、クラスで最も興味深い部分です。IOptimisticSyncStore インターフェイスの実装は、前回発行された割り当ての上限を取得するために使用します。上限値はジェネレーターの範囲の大きさ分インクリメントされ、ストレージに書き戻されます。単純な while ループでメソッド本体を囲むことで、TryOptimisticWrite メソッドが false を返した場合の再試行回数が適切になるようにします。
次のコード スニペットは、uniqueids というコンテナー内の ordernumber.dat ファイルを指定した BlobOptimisticSyncStore を使用している、作成中の UniqueIdGenerator クラスを示しています (注: ブロブ ストレージのコンテナーには小文字の名前を指定する必要があります)。
IOptimisticSyncStore storage = new BlobOptimisticSyncStore(
CloudStorageAccount.DevelopmentStorageAccount,
"uniqueids",
"ordernumber.dat");
UniqueIdGenerator
generator = new UniqueIdGenerator(storage, 1000, 10);
このインスタンスでは、一元管理される範囲から 1,000 件の ID を削除し、オプティミスティック同時実行制御のエラー発生時には 10 回再試行してから例外をスローします。
UniqueIdGenerator クラスの使用はさらに簡単です。新しい一意の注文番号が必要な場合は、次のように NextId メソッドを呼び出すだけです。
Int64 orderId = generator.NextId();
サンプル コードには、複数のスレッドを使用して一意注文番号をすばやく割り当て、SQL データベースに書き込む Windows Azure のワーカー ロールを含めています。このインスタンスで SQL を使用しているのは、すべての注文番号が一意であることを証明するためです。この条件に違反すると主キー違反が発生し、例外がスローされます。
この方法のメリットは、アプリケーションの有効期間の最初にブロブを作成して値を 0 に設定すること以外に、システム管理者が行わなければならない作業がないことです。UniqueIdGenerator クラスによって、開発者の設定に基づいて ID の割り当てが管理され、エラー発生時に適切に回復され、非常に融通性が高い環境でも簡単にスケール変換されます。
シナリオ 2: Release the Hounds!
アプリケーションに課せられるもう 1 つの興味深い要件に、発生タイミングが事前にほぼ判明している特定のイベントが発生したら、即座に大量のデータを処理しなければならないというものがあります。処理の性質上、イベントが発生するまではデータをまったく処理できません。
このようなシナリオでは、明らかにワーカー ロールを選択することになります。この場合、イベントに応じて必要な数のワーカー ロールのプロビジョニングを行うよう Windows Azure に指示できます。ただし、新しいロールのプロビジョニングには 30 分もかかることがあり、このシナリオでは処理速度が重要です。 そのため、事前にロールを準備し、管理者がそのロールを解き放つまで一時停止状態にしておきます。私はこの操作に「Release the Hounds! (猟犬を解き放つ)」という名前を付けました。検討できる方法は 2 つあったので、それぞれについて順番に説明します。
Windows Azure のワーカー ロールは、ロールを配置している時間に基づいて課金されるため (CPU の使用状況に基づく課金ではありません)、この方法は単にイベントに応じてワーカー ロールを作成する場合よりもコストが高くことに注意が必要です。ただし、こうしたシナリオのユーザーにとって、できる限り高速に処理を開始するためにこの投資には価値があることは明らかです。
方法 I: ポーリング
最初の方法 (図 3 参照) では、ステータス フラグを Windows Azure ブロブに格納して集中管理し、このフラグを各ノードから定期的にポーリングして、処理を開始できるかどうか判断します。
図 3 ステータス フラグを集中管理してノードからポーリング
ノードを一時停止状態から解き放つには、クライアント アプリケーションでこのフラグを単純に true に設定します。設定したら、次回のポーリングで各ノードが解き放たれます。この方法の主な問題点は遅延で、最大、次回のポーリングまでポーリング間隔分遅延する可能性があります。ただし、実装が非常に単純で信頼性の高いメカニズムです。
この設計の例は、サンプル コードに含めた PollingRelease クラスで示しています。テストしやすいように、UniqueIdGenerator クラスとほとんど同じ方法で、フラグの格納メカニズムをインターフェイスで隠蔽して抽象化しました。IGlobalFlag インターフェイスと、サンプル コード付属のブロブ ストレージ向け実装を図 4 に示します。
図 4 IGlobalFlag インターフェイスとブロブ ストレージ向け実装
public interface IGlobalFlag
{
bool GetFlag();
void SetFlag(bool status);
}
public class BlobGlobalFlag : IGlobalFlag
{
private readonly string _token = "Set";
private readonly CloudBlob _blobReference;
public BlobGlobalFlag(CloudStorageAccount account, string container,
string address)
{
var blobClient = account.CreateCloudBlobClient();
var blobContainer =
blobClient.GetContainerReference(container.ToLower());
_blobReference = blobContainer.GetBlobReference(address);
}
public void SetFlag(bool status)
{
if (status)
{
_blobReference.UploadText(_token);
}
else
{
_blobReference.DeleteIfExists();
}
}
public bool GetFlag()
{
try
{
_blobReference.DownloadText();
return true;
}
catch (StorageClientException exc)
{
if (exc.StatusCode == System.Net.HttpStatusCode.NotFound)
{
return false;
}
throw;
}
}
}
この例では、ファイルの内容に関係なく、ブロブ ストレージにファイルが存在しているだけで true を返していることに注意してください。
PollingRelease クラス自体は単純で (図 5 参照)、Wait というパブリック メソッドが 1 つあるだけです。
図 5 PollingRelease クラス
public class PollingRelease
{
private readonly IGlobalFlag _globalFlag;
private readonly int _intervalMilliseconds;
public PollingRelease(IGlobalFlag globalFlag,
int intervalMilliseconds)
{
_globalFlag = globalFlag;
_intervalMilliseconds = intervalMilliseconds;
}
public void Wait()
{
while (!_globalFlag.GetFlag())
{
Thread.Sleep(_intervalMilliseconds);
}
}
}
このメソッドは、IGlobalFlag の実装のステータスが false となっている限り、すべての呼び出し元をブロックします。次のコード スニペットは、PollingRelease クラスの使用例を示しています。
BlobGlobalFlag globalFlag = new BlobGlobalFlag(
CloudStorageAccount.DevelopmentStorageAccount,
"globalflags",
"start-order-processing.dat");
PollingRelease pollingRelease = new PollingRelease(globalFlag, 2500);
pollingRelease.Wait();
"globalflags" というコンテナーを指定して、BlobGlobalFlag のインスタンスを作成しています。PollingRelease クラスでは、2.5 秒ごとに "start-order-processing.dat" というファイルの有無をポーリングします。このファイルが存在しない間、Wait メソッドの呼び出しはすべてブロックされます。
方法 II: リッスン
2 つ目の方法では、Windows Azure AppFabric サービス バスを使用して、すべてのワーカー ロールと直接同時に通信し、ワーカー ロールを解き放ちます (図 6 参照)。
図 6 Windows Azure AppFabric サービス バスを使用したすべてのワーカー ロールとの同時通信
サービス バスは大規模なメッセージングと接続を行うサービスで、やはり Windows Azure 上に構築されます。サービス バスを利用すると、分散アプリケーションのさまざまなコンポーネント間でセキュリティが確保された通信を容易に行えます。サービス バスには、ネットワーク アドレス変換 (NAT) の境界の背後に配置されていたり、IP アドレスが頻繁に変更されたりするなど、他の方法では通信が困難な 2 つのアプリケーションを接続するのに最適な方法が実現されます。Windows Azure AppFabric サービス バスの概要の詳細についてはこの記事で扱う範囲を超えているため触れませんが、MSDN (msdn.microsoft.com/library/ee706736、英語) には優れたチュートリアルが用意されています。
この方法を例を挙げて示すために、PollingRelease クラスと同様に Wait というパブリック メソッドが 1 つある、ListeningRelease というクラスを作成しました。次のように、このメソッドはサービス バスに接続し、ManualResetEvent イベントを使用して、シグナルを受信するまでスレッドをブロックします。
public void Wait()
{
using (ConnectToServiceBus())
{
_manualResetEvent.WaitOne();
}
}
ConnectToServiceBus メソッド全体を図 7 に示します。このメソッドでは、System.ServiceModel アセンブリと Microsoft.ServiceBus アセンブリに含まれる型を使用して、Windows Azure AppFabric サービス バス経由で UnleashService クラスをクラウドに公開します (図 8 参照)。
図 7 ConnectToServiceBus メソッド
private IDisposable ConnectToServiceBus()
{
Uri address = ServiceBusEnvironment.CreateServiceUri("sb",
_serviceNamespace, _servicePath);
TransportClientEndpointBehavior sharedSecretServiceBusCredential =
new TransportClientEndpointBehavior();
sharedSecretServiceBusCredential.CredentialType =
TransportClientCredentialType.SharedSecret;
sharedSecretServiceBusCredential.Credentials.SharedSecret.
IssuerName = _issuerName;
sharedSecretServiceBusCredential.Credentials.SharedSecret.
IssuerSecret = _issuerSecret;
// Create the single instance service, which raises an event
// when the signal is received.
UnleashService unleashService = new UnleashService();
unleashService.Unleashed += new
EventHandler(unleashService_Unleashed);
// Create the service host reading the configuration.
ServiceHost host = new ServiceHost(unleashService, address);
IEndpointBehavior serviceRegistrySettings =
new ServiceRegistrySettings(DiscoveryType.Public);
foreach (ServiceEndpoint endpoint in host.Description.Endpoints)
{
endpoint.Behaviors.Add(serviceRegistrySettings);
endpoint.Behaviors.Add(sharedSecretServiceBusCredential);
}
host.Open();
return host;
}
図 8 UnleashService クラス
[ServiceBehavior(InstanceContextMode= InstanceContextMode.Single)]
public class UnleashService : IUnleashContract
{
public void Unleash()
{
OnUnleashed();
}
protected virtual void OnUnleashed()
{
EventHandler temp = Unleashed;
if (temp != null)
{
temp(this, EventArgs.Empty);
}
}
public event EventHandler Unleashed;
}
UnleashService クラスは Windows Communication Foundation (WCF) によって 1 つのインスタンスとしてホストされ、IUnleashService コントラクトを実装します。このコントラクトには、Unleash というメソッドが 1 つだけあります。ListeningRelease クラスでは、先ほど示した Unleashed イベントを通じて Unleash メソッドの呼び出しをリッスンします。ListeningRelease クラスでこのイベントが検出されると、現在 Wait メソッドの呼び出しをすべてブロックしている ManualResetEvent が設定され、ブロックされているスレッドがすべて解き放たれます。
サービスの構成では、NetEventRelayBinding を使用しました。NetEventRelayBinding はサービス バスを通じてマルチキャストをサポートするため、任意の数のパブリッシャーとサブスクライバーが 1 つのエンドポイント経由で通信できます。このブロードキャスト通信の性質上、以下の IUnleashContract インターフェイスに示すように、すべての操作を一方向にする必要があります。
[ServiceContract]
public interface IUnleashContract
{
[OperationContract(IsOneWay=true)]
void Unleash();
}
エンドポイントは、共有シークレット (ユーザー名と複雑なパスワード) を使用してセキュリティ保護されます。これらの詳細設定により、インターネットにアクセスできるすべてのクライアントから Unleash メソッドを呼び出すことができます。たとえば、サンプルに含まれている Administrator Console アプリケーションからも呼び出すことができます (図 9 参照)。
図 9 Administrator Console アプリケーション
ListeningRelease クラスを使用した方法では、PollingRelease クラスによって生じる遅延はなくなりますが、対処が必要な遅延は他にもあります。ただし、ステートレスな性質がリッスンによる方法の主な問題点です。たとえば、解き放つシグナルを送信した後にプロビジョニングされたノードには、そのイベントが発生しないため、一時停止状態のままになります。もちろん、サービス バスとブロブ ストレージ内のグローバルなフラグを組み合わせるのが明らかな解決策ですが、これについては読者の皆さんへの課題として残しておきましょう。
サンプル コード
付属のサンプル ソリューションは、code.msdn.microsoft.com/mag201011Sync (英語) からダウンロードできます。このソリューションには、必須コンポーネントの一覧やセットアップと構成の手順を記載した ReadMe ファイルも含めてあります。サンプルでは、1 つのワーカー ロールで ListeningRelease、PollingRelease、および UniqueIdGenerator の各クラスを使用します。
Josh Twist は、英国の開発者向けプレミア サポート チームの主任アプリケーション開発マネージャーです。彼のブログは thejoyofcode.com (英語) です。
この記事のレビューに協力してくれた技術スタッフの David Goon、Morgan Skinner、および Wade Wegner に心より感謝いたします。