WCF P2P
ピア ネットワークでの状態共有の設計方法
Kevin Hoffman
この記事では、次の内容について説明します。
- 固定状態サーバー
- 選択状態サーバー
- サーバーレス データ ブラスト
- 最近ピア同期
|
この記事では、次のテクノロジを使用しています。
WCF
|

目次
近年、ピア ネットワーク経由で通信するアプリケーションが提供できる優れた処理能力と機能が注目されつつあります。いわゆるピア アプリケーションは、単純なファイル共有から、インスタント メッセージング (IM) や、共有ホワイトボード、Voice over IP (VoIP) 電話と会議、ソーシャル ネットワーキングなどの高度なコラボレーション アプリケーションまで、あらゆる範囲をカバーします。
Xbox 360® が Windows Media® Center を実行している別のコンピュータの存在を自動的に検出し、そのコンピュータとのオーディオやビデオの共有をすぐに開始して、Xbox® のコンピュータで再生できることを考えてみてください。ビジネス アプリケーションが同じようなピア認識能力と機能を備えていればすばらしいと思いませんか。あるユーザーが自分のデスクトップで新しいレコードを作成した場合に、そのアプリケーションを使用している組織内の別のユーザーが新しいレコードをすぐに見ることができたら、そして、それを実現するために中央サーバーをインストールして構成する必要がないとしたら、顧客にとってどれほど喜ばしいかを考えてみてください。
この記事では、ビジネス アプリケーションがサーバーレスのピア ネットワークで状態を共有できるようにすることで、ビジネス アプリケーションをピア対応にする方法について詳細に説明します。サンプル アプリケーションについて詳しく説明する前に、ピア ネットワークで状態共有のためによく試されるさまざまな方法について説明し、それらの方法の長所と短所を指摘します。最後に、WeSpend という名前のサンプル アプリケーションを示します。これは、預金口座の取引を追跡するためのツールです。このアプリケーションは、ホーム ネットワーク上で、そのアプリケーション自体のその他すべてのインスタンスとデータを自動的に同期します。
ピア ネットワークを実装する方法は、考えられるアプリケーションのアイデアと同じ数だけあります。どのような実装にも、なんらかの長所と短所があります。そのような長所や短所は、インフラストラクチャに関係していたり、ソリューションの長期的な維持管理の容易さに関係することであったり、そうでなければソリューションの初期開発を容易にすることに関係しています。ここでは、ピア ネットワークの開発に最もよく使用される方法をいくつか見ていきます。
固定状態サーバー
多くの開発チームが、ピア対応アプリケーションの初期概念フェーズを切り抜けて実装の検討を始めたときに最初に思い浮かべる問題の 1 つは、通常、"どうやって状態を共有するのか" ということです。
最も簡単な解決方法の 1 つは、ハイブリッド ピア ネットワークを作成することです。このソリューションでは、すべてのブロードキャストとピア トラフィックはピア メッシュで発生しますが、既知の中央サーバーが存在し、通常は Web サービスまたは別の Windows® Communication Foundation (WCF) サービスを利用して、状態を維持しています。これにより、ネットワーク上のピアはアプリケーション全体に対して単一の状態を認識でき、一方で最適化されたピア メッシュ通信を利用できます。
この種のネットワークの最も一般的な例は、ソーシャル ネットワークや IM アプリケーションです。IM アプリケーションでは、通常、ログインしているユーザーのリスト、ユーザーの所在と対応可能性、および全登録ユーザーの友人リストを管理する中央サーバーが必要です (図 1 を参照)。ネットワーク上のユーザー間の通信は、ピアツーピア方式で行われます。従来のクライアント サーバー機能とピアツーピア機能の間には、非常に便利で強力な機能の分割が見られます。
図 1 ピア ネットワークにおける中央状態サーバー (クリックすると拡大画像が表示されます)
このパターンの長所は、簡単に実装できることです。ピア ネットワーク コンポーネントは複雑さが少なく、しかも近年では Microsoft® .NET Framework と WCF のおかげで中央サーバーとの通信が非常に簡単になっています。
短所は、インフラストラクチャに中央サーバーが必要なことです。ピア アプリケーションは中央サーバーのアドレスを知る必要があり、これは構成とメンテナンスの問題の原因になる可能性があります。また、多くのアプリケーション開発者には、中央状態サーバーをグローバルに展開するためのリソースがなく、中央状態サーバーの要件はアプリケーションの仕様では受け入れられないものであることがよくあります。
一時的選択状態サーバー
共有状態パターンの進化における次のステップは、ピア メッシュ内のノードの 1 つを状態サーバーとして選択することによって、前述のパターンからインフラストラクチャの関与を軽減するというものです。このパターンはネットワークを利用したリアルタイム戦略ゲームによく見られ、ゲームを始めたプレーヤーがゲームの状態の "真なる唯一のコピー" を持ち、他のすべてのプレーヤーは同期化された複製を持ちます。
このパターンは、1 つのノードをメッシュの状態サーバーとして選択するアルゴリズムを使用することで機能します。このノードが状態サーバーになった後は、メッシュ内の他のすべてのノードは固定状態サーバーのシナリオと同じパターンに従い、単一の状態を更新および照会します。
この方法では固定中央サーバーが必要であるという問題はなくなりますが、その代わりに、選択システムの複雑さが問題になります。このピア ネットワークを実装するには、状態サーバーの選択方法を決定する必要があります。非常に簡単な解決方法の 1 つは、最も古いノード (メッシュに最も長く存在するもの) または最も新しいノードを選択することです。この方法では、ノードがメッシュに存在しなくなったときに問題が発生します。選択した状態サーバーがオフラインになると、どうなるでしょう。オフラインになることをメッシュが認識した場合は、ピアは新しい状態サーバーを選択できます。しかし、状態サーバーのパフォーマンスが低下しただけで、オフラインにはなっておらず、メッシュが再選択を決定しない場合は、どのようなことが発生するでしょうか。
このように、この解決方法の欠点は、インフラストラクチャから中央サーバーを不要にするのと引き換えに、プロジェクトの進捗に影響を与えかねないほど複雑な選択システムの作成が必要になることです。この選択システムは、単に最善の状態サーバー候補を選択するだけでなく、状態サーバーのブラックホール化 (メッセージを吸い込むだけで吐き出さない状態であり、単なるオフライン状態とは少し違います) や、状態サーバーのオフライン化に対処できるだけの回復力を備えている必要があります。
選択システムの複雑さを実際にうまく処理できるのであれば、この方法は強力で汎用性の高いシステムになる可能性があります。一方、選択システムがうまく機能しないと、この種のネットワークは悲惨な状態になり、状態共有には問題が山積することになります。
純粋なピア サーバーレス データ ブラスト
最初の 2 つのシナリオは、状態の正式なコピーが 1 つだけ存在するソリューションを提供しようとするものです。状態サーバーの場所が固定されるか選択されるかの違いはあっても、ピアが状態を照会および操作する方法は同じであり、単一のリモート サーバーに接続します。
中央状態サーバーの代わりによく使われるのは、筆者が "データ ブラスト" ネットワークと呼ぶネットワークです。このシナリオでは、メッシュに新しいピアが参加するたび、またはピアからデータ更新の要求が明示的にあった場合に、ピアはメッシュ内の他のすべてのピアに対してデータのコピーをブロードキャストします。各ピアは、状態データのブロードキャストを受信するたびに、ブロードキャストの内容とローカル状態を比較し、必要に応じて更新します。
この方法には、比較的大きな欠点が 2 つあります。1 つ目の問題は、ネットワーク上を送信される冗長なデータがかなり大量になる可能性があることです。ピア ネットワークのコンピュータが増えるほど、無駄に消費される帯域幅も多くなります。事実、このシナリオにおける冗長なデータ送信の量は、メッシュ内のノードの数が増えるにつれて急激に増加します。
この方法のもう 1 つの欠点は、各アプリケーションがそれぞれのデータのコピーをブロードキャストするので、データのブロードキャストを受信したら、同期操作を行って受信したデータとローカル データを比較し、差異を調整する必要があります。ピア ネットワークのサイズが拡大すると、やがては到着するメッセージより状態ブロードキャストの方の処理時間が長くなり、ボトルネックが発生して、アプリケーション自体のパフォーマンスが低下する可能性があります。ピア メッシュ内の各ノードはデータが重複しているかどうかを確認する必要があるので、ブロードキャスト送信データが一意であることを識別するためのなんらかの手段が必要です。
少々無駄なネットワーク リソースの浪費に対するトレードオフは、中央状態サーバーがなく、選択パターンに伴う複雑さに苦しまなくて済むことです。ある種の状況においては、特にノードの数が一定のサイズより増加しないことがわかっている場合は、重複するデータ送信が増えても、実装作業が減ることの方に大きな価値があります。
最近ピア同期
筆者は選択シナリオの実装に関係のあるさまざまな方法を調査しているときに、WCF には特定のメッセージが移動する最大ホップ数を示す属性があることを発見しました。このことがわかった後で、中央サーバーが不要なだけでなく、ノードの機能低下に対応でき、選択の必要もない、ピア ネットワークでの状態共有方法が存在することが明らかになりました。筆者は、この方法を最近ピア同期 (Nearest Peer Synchronization) と呼んでいます。
ピア メッシュ内の 2 つのノード間をメッセージが移動するときに使用するルートは、WCF によって完全に指示されます。これにより、WCF はネットワークでの通信パスを最適化できます。このことと、メッセージが移動する距離を 1 ホップだけに制限できることがわかっていれば、状態共有のための隣接同期パターンを実装できます。
まず、メッシュに新しく接続するノードは、接続した後で、1 ホップ メッセージを送信して、共有状態レコードの一意なレコード識別子のリストを要求します。メッシュ内で 1 ホップの距離にあるすべてのノードが、このメッセージを受信します。これらのノードは、コールバック コントラクトを使用して、メッシュに直接応答します。応答には、そのノードが保持している全データ レコードの一意な ID のリストが含まれます。最初に応答したノードが "勝者" となり、その他すべての応答は無視されます。重複するメッセージを無視するコードを作成する必要があることに注意してください。WCF による自動処理は行われません。
新しく接続したノードは、受信した一意な ID のリストと、自分が持っている一意な ID のリストを比較します (ノードは、オフライン状態をディスクに保持し、共有状態の部分的なビューを持っている可能性があります)。その後、応答で受信した一意な ID のうち、ローカル状態に含まれなかった ID を 1 ホップ メッセージでメッシュに送信します。そのメッセージに応答する最初のノードは (やはり、コールバック コントラクトを使用して)、これらの一意な各 ID の詳細を返送します。通常、この詳細は完全に構成されたシリアル化可能なオブジェクトです。
このパターンが優れているところは、障害のあるノードの消失を自動的に修正できる点です。メッシュ内のノードで障害が発生した場合、そのノードはデータの要求に応答しません。それ以外のすべての 1 ホップ ノードは、そのデータに応答する可能性があります。メッシュの構成により、新しく接続するノードが、障害の発生したノードとしか通信できない場合、WCF は、それまで 2 ホップの距離にあったノードを、1 ホップの距離であると見なすようになります。選択ロジックは必要なく、自動フェールオーバーは WCF のピア チャネル層において直接処理されるので、そのための追加コードを記述する必要はありません。各隣接ノードは各隣接ノードの状態と同期していることが保証されるので、状態はメッシュ内のすべてのピアに自動的に伝達されます。図 2 は、隣接するノードどうしが自動的に同期する純粋なサーバーレス ピア メッシュを示しています。
図 2 状態サーバーを持たないピア メッシュ (クリックすると拡大画像が表示されます)
WeSpend サンプル アプリケーション
このアイデアを実行するため、個人用資産マネージャのシミュレーションを見てみましょう。このアプリケーションを使用すると、ユーザーは、当座預金口座に対して行われた小切手取引やデビット取引などの金融取引を追跡できます。
WeSpend は、ネットワーク上で動作するアプリケーションのコピーどうしが状態を自動的に共有するので、コラボレーション ピア アプリケーションです。ユーザーが認識できるこの効果は、たとえば、下の階にいる家族が WeSpend で取引を入力すると、上の階にいる別の家族がこの新しい取引をすぐに見ることができます。同じ家の中で 3 人目の家族が新しく WeSpend を起動すると、アプリケーションを実行していなかった間に作成された取引が自動的に受信されます。この種のピア認識およびコラボレーション機能を追加することによって、優れた人気のあるアプリケーションと、単に可もなく不可もないアプリケーションとの差が生まれます。
WeSpend は、最近隣接ノード同期共有状態パターンを使用します。このアプリケーションは、いくつかの重要な技術コンポーネントを使用して実現されます。第 1 のコンポーネントは既に説明した PeerHopCount という属性であり、この属性を使用すると、メッセージのコンテンツを一定の回数だけ送信してから削除できます。この属性によって、データ メッセージの特性が IP の有効期限 (TTL) の概念とよく似たものになります。
次に示すのは WeSpend アプリケーションでの WCF メッセージ コントラクトのサンプルであり、ピア ホップ カウントを制御するフィールドを含みます。ピア ホップ カウント属性は NetPeerTcpBinding プロトコル バインドによってのみ使用されることに注意してください。その他すべてのバインドはこの属性を無視します。
[MessageContract]
public class TransactionIdRequest {
[MessageBodyMember]
public string Requester;
[PeerHopCount]
public int Hops;
public TransactionIdRequest() {
Hops = 1;
}
}
このメッセージをメッシュに送信しても、使用可能なすべてのノードにメッセージを送信する通常のフラッド動作は発生しません。代わりに、メッセージは 1 回だけ配信された後、存在しなくなります。これにより、メッシュ内の最も近い隣接ノードに対してのみ、取引 ID のリストを要求できます。
このシナリオを可能にするもう 1 つの重要なコンポーネントは、コールバック コントラクトです。WCF ピアに対してメソッドが別のピアによって呼び出されると、ターゲット コードから、メソッド呼び出しを開始したピア メッシュへのコールバックを実行できます。その結果、単一のノードが隣接ノードの要求に応答でき、すべてのピア ノードで個別に WCF サービスをホストする必要がなくなります。コールバック コントラクトを使用して取引 ID の要求に応答するコードの例を次に示します。
public void RequestTransactionIds(TransactionIdRequest request) {
List<Guid> outboundIds = new List<Guid>();
foreach (Transaction tx in ModelRoot.Current.Transactions) {
outboundIds.Add(tx.TransactionID);
}
CallbackChannel.TransactionIdsReply(new TransactionIdReply() {
ReplyFrom = AppController.Current.Username,
TransactionIDs = outboundIds
});
}
ここでは、コールバック チャネルを使用して、メソッド呼び出し RequestTransactionIds を開始したピアに応答しています。TransactionIdRequest メッセージは 1 ホップのみに制限されていることを思い出してください。最大規模のピア ネットワークであっても、開始ピアに応答が殺到することはありません。
TransactionIdsReply メソッドを呼び出して取引 ID のリストを受信したピア ノードは、再びコールバック コントラクトを使用して、ローカル記憶域にまだ存在しない ID の詳細を要求します (図 3 を参照)。

図 3 取引の詳細の要求
public void TransactionIdsReply(TransactionIdReply reply)
{
// Compare the list of TX IDs with the list of IDs in local storage
// for each item that is "missing" from local storage, put that item
// in the request for details.
List<Guid> output = new List<Guid>();
foreach (Guid id in reply.TransactionIDs)
{
if (!ModelRoot.Current.Transactions.Any(
tx => tx.TransactionID.Equals(id)))
{
output.Add(id);
Console.WriteLine("[LTS] Need to request detail for TX " +
id.ToString());
}
}
CallbackChannel.RequestTransactionDetails(output);
}
同期ペアのもう一方は、RequestTransactionDetails メソッドを呼び出すと、やはりコールバック コントラクトを使用して、メッシュに応答します (図 4 を参照)。

図 4 詳細情報の応答
public void RequestTransactionDetails(List<Guid> transactionIds)
{
List<LedgerTransaction> output = new List<LedgerTransaction>();
foreach (Guid id in transactionIds)
{
Transaction tx = (from Transaction lt in
ModelRoot.Current.Transactions
where lt.TransactionID.Equals(id)
select lt).FirstOrDefault();
if (tx != null)
output.Add(tx.ToLedgerTransaction());
}
CallbackChannel.TransactionDetailsAck(new TransactionDetailReply()
{
ReplyFrom = AppController.Current.Username,
Transactions = output
});
}
前述のコード リストで使用しているプロパティ CallbackChannel は、WCF ピア サービス コントラクトの実装で次のように定義されています。
protected ILedgerTransactionServiceChannel CallbackChannel {
get {
return OperationContext.Current.GetCallbackChannel<
ILedgerTransactionServiceChannel>();
}
}
図 5 は、メッシュに接続したばかりのピアと最も近い隣接ノードの間で、同期操作を開始する呼び出しに応答するメッセージの流れを示しています。
図 5 最近隣接ノード同期の流れ (クリックすると拡大画像が表示されます)
前述のメソッドの実装に欠落していることの 1 つは、再入実行に対する防御策です。つまり、複数のピアが同時に同期操作を開始しようとした場合、実稼働コードではこれを防止する必要があります。また、このコードは同期操作のためにコールバック コントラクトを多用することにも注意してください。ピア ノードは他のピア ノードが同時に同期を実行できないように保証する必要がありますが、これは簡単なセマフォを使用して実現できます。
アプリケーションにピア ネットワーク機能を追加すると、ソフトウェアのユーザー エクスペリエンスが大幅に向上します。しかし、たいていの場合は、そうすることで発生する複雑さが障害になり、ピア ネットワーク機能が製品の設計から除外されてしまうこともあります。実装によってはメンテナンス上の悪夢にたちまち陥り、ピア ネットワークが仕様から削除されることになり、複雑さはまさに手に負えなくなります。
この記事の目的は、ピア ネットワークが構築可能で簡単に実装できるだけでなく、高い効率とフォールト トレラント機能を備えたコーディングしやすいサーバーレス状態共有システムを実装することさえ可能であると示すことです。また、ここで解説したパターンにはほとんど無限のバリエーションがあることも覚えておいてください。
コード サンプルをダウンロードし、試してみることをお勧めします。コンピュータを 2 台用意し、2 つの異なるユーザー名をシミュレートするように app.config を変更するのが最良の方法です。txhost のアプリケーション設定を true (アプリケーションはいくつかのサンプル取引を作成します) に設定したインスタンスを起動した後、txhost を false (取引の自動作成を行いません) に設定したもう 1 つのインスタンスを起動します。1 つ目のインスタンスの取引が 2 つ目のインスタンスに自動的に伝達されることがわかります。ボタンをクリックして新しい取引を作成すると、サンプル アプリケーションの両方の実行インスタンスにその取引が表示されるのがわかります。同期の効果をさらにはっきり確認するには、2 つ目のインスタンスをいったん終了してから再起動してください。2 つ目のインスタンスを再起動すると、すべての取引が表示されます。コンピュータでのピア名解決プロトコル (PNRP) の動作によっては、サンプル アプリケーションが最初の通信を開始するまでに最大 30 秒ほどかかります。