3.3. 分散キャッシュ

分散は、numOwners として設定された、キャッシュ内の任意のエントリーの固定数のコピーを保持しようとします。これにより、キャッシュを線形にスケーリングし、ノードがクラスターに追加されるにつれて、より多くのデータを格納できます。

ノードがクラスターに参加およびクラスターから離脱すると、キーのコピー数が numOwners より多い場合と少ない場合があります。特に、numOwners ノードがすぐに連続して離れると、一部のエントリーが失われるため、分散キャッシュは、numOwners - 1 ノードの障害を許容すると言われます。

コピー数は、パフォーマンスとデータの持続性を示すトレードオフを表します。維持するコピーが増えると、パフォーマンスは低くなりますが、サーバーやネットワークの障害によるデータ損失のリスクも低くなります。維持されるコピーの数に関係なく、分散は直線的にスケーリングされます。これは、Data Grid のスケーラビリティの鍵となります。

キーの所有者は、キーへの書き込みを調整する 1 つのプライマリー所有者と、0 個以上のバックアップ所有者に分割されます。プライマリー所有者とバックアップ所有者の割り当て方法の詳細は、キー所有権 セクションを参照してください。

図3.3 分散モード

読み取り操作はプライマリー所有者からの値を要求しますが、妥当な時間内に応答しない場合、バックアップの所有者からも値を要求します。(infinispan.stagger.delay システムプロパティー) は、リクエスト間の遅延を制御します。) キーがローカルキャッシュに存在する場合、読み取り操作には 0 メッセージが必要になる場合があり、すべての所有者が遅い場合は最大 2 * numOwners メッセージが必要になる場合があります。

また、書き込み操作は、最大 2 * numOwners メッセージで、送信元からプライマリー所有者、numOwners - 1 メッセージ、プライマリーからバックアップへの 1 メッセージ、対応する ACK メッセージまでです。

注記

キャッシュトポロジーの変更により、読み取りと書き込みの両方で再試行と追加メッセージが発生する可能性があります。

レプリケートモードと同様に、分散モードは同期または非同期にすることもできます。レプリケートされたモードでは、更新が失われる可能性があるため、非同期レプリケーションは推奨されません。更新の損失に加えて、非同期の分散キャッシュは、スレッドがキーに書き込むときに古い値を確認し、その後に同じキーをすぐに読み取ることもできます。

トランザクション分散キャッシュは、トランザクション複製キャッシュと同じ種類のメッセージを使用しますが、ロック/準備/コミット/ロック解除メッセージは、クラスター内のすべてのノードにブロードキャストされるのではなく、影響を受けるノード(トランザクションの影響を受ける少なくとも 1 つのキーを所有するすべてのノード) にのみ送信されます。最適化として、トランザクションが単一のキーに書き込み、送信元がキーの主な所有者である場合、ロックメッセージは複製されません。

3.3.1. 読み取りの一貫性

同期レプリケーションを使用しても、分散キャッシュは線形化できません。(トランザクションキャッシュの場合は、シリアル化/スナップショットの分離に対応していないとします。) 1 つのスレッドで 1 つの put を実行できます。

cache.get(k) -> v1
cache.put(k, v2)
cache.get(k) -> v2

ただし、別のスレッドでは、異なる順序で値が表示される場合があります。

cache.get(k) -> v2
cache.get(k) -> v1

その理由は、プライマリー所有者が応答する速度に応じて、クラスター内のすべてのノードが任意の 所有者から値を返す可能性があるためです。書き込みはすべての所有者にわたってアトミックではありません。実際、プライマリーはバックアップから確認を受け取った後にのみ更新をコミットします。プライマリーがバックアップからの確認メッセージを待機している間、バックアップからの読み取りには新しい値が表示されますが、プライマリーからの読み取りには古い値が表示されます。

3.3.2. キーの所有者

分散キャッシュは、エントリーを固定数のセグメントに分割し、各セグメントを所有者ノードの一覧に割り当てます。レプリケートされたキャッシュは同じで、すべてのノードが所有者である場合を除きます。

所有者リストの最初のノードはプライマリー所有者です。一覧のその他のノードはバックアップの所有者です。キャッシュトポロジーが変更するると、ノードがクラスターに参加またはクラスターから離脱するため、セグメント所有権テーブルがすべてのノードにブロードキャストされます。これにより、ノードはマルチキャスト要求を行ったり、各キーのメタデータを維持したりすることなく、キーを見つけることができます。

numSegments プロパティーでは、利用可能なセグメントの数を設定します。ただし、クラスターが再起動しない限り、セグメントの数は変更できません。

同様に、キーからセグメントのマッピングは変更できません。鍵は、クラスタートポロジーの変更に関係なく、常に同じセグメントにマップする必要があります。キーからセグメントのマッピングは、クラスタートポロジーの変更時に移動する必要のあるセグメント数を最小限に抑える一方で、各ノードに割り当てられたセグメント数を均等に分散することが重要になります。

KeyPartitioner を設定するか、Grouping API を使用して key-to-segment マッピングをカスタマイズできます。

ただし、Data Grid は以下の実装を提供します。

SyncConsistentHashFactory

一貫性のあるハッシュ に基づくアルゴリズムを使用します。サーバーヒントを無効にした場合は、デフォルトで選択されています。

この実装では、クラスターが対称である限り、すべてのキャッシュの同じノードに常にキーが割り当てられます。つまり、すべてのキャッシュがすべてのノードで実行します。この実装には、負荷の分散が若干不均等であるため、負のポイントが若干異なります。また、参加または脱退時に厳密に必要な数よりも多くのセグメントを移動します。

TopologyAwareSyncConsistentHashFactory
SyncConsistentHashFactory に似ていますが、サーバーヒントに適合しています。サーバーヒントが有効な場合にデフォルトで選択されます。
DefaultConsistentHashFactory

SyncConsistentHashFactory よりも均等に分散を行いますが、1 つの欠点があります。ノードがクラスターに参加する順序によって、どのノードがどのセグメントを所有するかが決まります。その結果、キーは異なるキャッシュ内の異なるノードに割り当てられる可能性があります。

サーバーヒントを無効にした状態で、バージョン 5.2 からバージョン 8.1 のデフォルトでした。

TopologyAwareConsistentHashFactory

DefaultConsistentHashFactoryに似ていますが、サーバーヒント に適合しています。

サーバーヒントが有効なバージョン 5.2 から 8.1 へのデフォルトでした。

ReplicatedConsistentHashFactory
レプリケートされたキャッシュの実装に内部で使用されます。このアルゴリズムは分散キャッシュで明示的に選択しないでください。

3.3.2.1. 容量ファクト

設備利用率は、ノードで使用可能なリソースに基づいてセグメントからノードへのマッピングを割り当てます。

容量係数を設定するには、負でない数を指定し、Data Grid ハッシュアルゴリズムにより、各ノードに容量係数で重み付けされた負荷が割り当てられます (プライマリー所有者とバックアップ所有者の両方として)。

たとえば、nodeA には、同じ Data Grid クラスター内の nodeB の 2 倍のメモリーがあります。この場合、capacityFactor を値 2 に設定すると、nodeA に 2 倍の数のセグメントを割り当てるように Data Grid が設定されます。

容量係数を 0 に設定することは可能ですが、ノードが有用なデータ所有者として十分な長さでクラスターに参加していない場合にのみ推奨されます。

3.3.3. ゼロ容量ノード

各キャッシュ、ユーザー定義キャッシュ、および内部キャッシュに対して容量係数が 0 であるノード全体の設定が必要になる場合があります。ゼロの容量ノードを定義する場合、ノードはデータを保持しません。これは、ゼロ容量ノードを宣言します。

<cache-container zero-capacity-node="true" />
new GlobalConfigurationBuilder().zeroCapacityNode(true);

3.3.4. ハッシュ設定

これは、XML を使用して、ハッシュを宣言的に設定する方法です。

<distributed-cache name="distributedCache" owners="2" segments="100" capacity-factor="2" />

Java では、プログラムを用いてこの方法で設定できます。

Configuration c = new ConfigurationBuilder()
   .clustering()
      .cacheMode(CacheMode.DIST_SYNC)
      .hash()
         .numOwners(2)
         .numSegments(100)
         .capacityFactor(2)
   .build();

3.3.5. 初期クラスターサイズ

トポロジーの変更 (つまり、実行時にノードが追加/削除される) の処理における Data Grid の非常に動的な性質は、通常、ノードが開始する前に他のノードの存在を待たないことを意味します。これは非常に柔軟性がありますが、キャッシュの開始前に、特定の数のノードがクラスターに参加する必要があるアプリケーションには適切ではない場合があります。このため、キャッシュの初期化に進む前に、クラスターに参加するノードの数を指定できます。これには、initialClusterSize および initialClusterTimeout トランスポートプロパティーを使用します。宣言型 XML 設定:

<transport initial-cluster-size="4" initial-cluster-timeout="30000" />

プログラムによる Java 設定:

GlobalConfiguration global = new GlobalConfigurationBuilder()
   .transport()
   .initialClusterSize(4)
   .initialClusterTimeout(30000, TimeUnit.MILLISECONDS)
   .build();

上記の設定は、初期化前に 4 つのノードがクラスターに参加するのを待機します。指定されたタイムアウト内に最初のノードが表示されない場合は、キャッシュマネージャーが起動に失敗します。

3.3.6. L1 キャッシュ

L1 が有効になっている場合、ノードはリモート読み取りの結果を短期間 (設定可能、デフォルトでは 10 分) ローカルに保持し、ルックアップを繰り返すと、所有者に再度尋ねるのではなく、ローカルの L1 値が返されます。

図3.4 L1 キャッシュ

L1 キャッシュは無料ではありません。有効にするとコストがかかり、このコストは、すべてのエントリー更新でインバリデーションメッセージをすべてのノードにブロードキャストする必要があることです。L1 エントリーは、キャッシュが最大サイズで設定されている場合に他のエントリーと同様にエビクトできます。L1 を有効にすると、非ローカルキーの繰り返される読み取りのパフォーマンスが向上しますが、書き込みが遅くなり、メモリー消費量がある程度増加します。

L1 キャッシュが正しいか。正しいアプローチとして、L1 を有効にしない状態でアプリケーションをベンチマークし、アクセスパターンに最も適した動作を確認できます。

3.3.7. サーバーヒント

以下のトポロジーヒントを指定できます。

マシン
これは、複数の JVM インスタンスが同じノードで実行されている場合、または複数の仮想マシンが同じ物理マシンで実行している場合でも、おそらく最も便利です。
ラック
大規模なクラスターでは、同じラックにあるノードでハードウェアまたはネットワークの障害が同時に発生する可能性が高くなります。
サイト
一部のクラスターでは、復元力を高めるために、複数の物理的な場所にノードが存在する場合があります。2 つ以上のデータセンターにまたがる必要のあるクラスターには、クロスサイトレプリケーションも別の方法であることに注意してください。

上記のすべては任意です。指定した場合、ディストリビューションアルゴリズムは、できるだけ多くのサイト、ラック、およびマシン全体に各セグメントの所有権を分散しようとします。

3.3.7.1. 設定

ヒントは、トランスポートレベルで設定されます。

<transport
    cluster="MyCluster"
    machine="LinuxServer01"
    rack="Rack01"
    site="US-WestCoast" />

3.3.8. キーアフィニティーサービス

分散キャッシュでは、不透明なアルゴリズムを使用してノードのリストにキーが割り当てられます。計算を逆にし、特定のノードにマップする鍵を生成する簡単な方法はありません。ただし、一連の (疑似) ランダムキーを生成し、それらのプライマリー所有者が何であるかを確認し、特定のノードへのキーマッピングが必要なときにアプリケーションに渡すことができます。

3.3.8.1. API

以下のコードスニペットは、このサービスへの参照を取得し、使用する方法を示しています。

// 1. Obtain a reference to a cache
Cache cache = ...
Address address = cache.getCacheManager().getAddress();

// 2. Create the affinity service
KeyAffinityService keyAffinityService = KeyAffinityServiceFactory.newLocalKeyAffinityService(
      cache,
      new RndKeyGenerator(),
      Executors.newSingleThreadExecutor(),
      100);

// 3. Obtain a key for which the local node is the primary owner
Object localKey = keyAffinityService.getKeyForAddress(address);

// 4. Insert the key in the cache
cache.put(localKey, "yourValue");

サービスはステップ 2 で開始します。この時点以降、サービスは提供されたエグゼキューターを使用してキーを生成してキューに入れます。ステップ 3 では、サービスから鍵を取得し、手順 4 ではそれを使用します。

3.3.8.2. ライフサイクル

KeyAffinityServiceライフサイクル を拡張し、停止と (再) 起動を可能にします。

public interface Lifecycle {
   void start();
   void stop();
}

サービスは KeyAffinityServiceFactory でインスタンス化されます。ファクトリーメソッドはすべて Executor パラメーターを持ち、これは非同期キー生成に使用されます (呼び出し元のスレッドでは処理されません)。ユーザーは、この Executor のシャットダウンを処理します。

KeyAffinityService が起動したら、明示的に停止する必要があります。これにより、バックグラウンドキーの生成が停止し、保持されている他のリソースが解放されます。

KeyAffinityService がそれ自体で停止する唯一の状況は、登録済みのキャッシュマネージャーがシャットダウンした時です。

3.3.8.3. トポロジーの変更

キャッシュトポロジーが変更すると (つまり、ノードがクラスターに参加またはクラスターから離脱する)、KeyAffinityService によって生成されたキーの所有権が変更される可能性があります。主なアフィニティーサービスはこれらのトポロジーの変更を追跡し、現在別のノードにマップされるキーを返しませんが、先に生成したキーに関しては何も実行しません。

そのため、アプリケーションは KeyAffinityService を純粋に最適化として処理し、正確性のために生成されたキーの場所に依存しないようにしてください。

特に、アプリケーションは、同じアドレスが常に一緒に配置されるように、KeyAffinityService によって生成されたキーに依存するべきではありません。キーのコロケーションは、Grouping API によってのみ提供されます。

3.3.8.4. Grouping API

キーアフィニティーサービス を補完する Grouping API を使用すると、実際のノードを選択することなく、同じノードにエントリーのグループを同じ場所にコロケートできます。

3.3.8.5. 仕組み

デフォルトでは、キーのセグメントはキーの hashCode() を使用して計算されます。Grouping API を使用する場合、Data Grid はグループのセグメントを計算し、それをキーのセグメントとして使用します。セグメントがノードにマップされる方法についての詳細は、キーの所有権 セクションを参照してください。

Grouping API を使用している場合は、他のノードに問い合わせることなく、すべてのノードが全キーの所有者を計算できることが重要です。このため、グループは手動で指定できません。グループは、エントリーに固有 (キークラスによって生成される) または外部 (外部関数によって生成される) のいずれかです。

3.3.8.6. Grouping API を使用する方法

まず、グループを有効にする必要があります。プログラムを用いて Data Grid を設定している場合は、以下を呼び出します。

Configuration c = new ConfigurationBuilder()
   .clustering().hash().groups().enabled()
   .build();

または、XML を使用している場合は、以下を行います。

<distributed-cache>
   <groups enabled="true"/>
</distributed-cache>

キークラスを制御できる場合 (クラス定義を変更できるが、変更不可能なライブラリーの一部ではない)、組み込みグループを使用することをお勧めします。intrinsic グループは、@Group アノテーションをメソッドに追加して指定します。以下に例を示します。

class User {
   ...
   String office;
   ...

   public int hashCode() {
      // Defines the hash for the key, normally used to determine location
      ...
   }

   // Override the location by specifying a group
   // All keys in the same group end up with the same owners
   @Group
   public String getOffice() {
      return office;
   }
   }
}
注記

group メソッドは String を返す必要があります。

キークラスを制御できない場合、またはグループの決定がキークラスと直交する懸念事項である場合は、外部グループを使用することをお勧めします。外部グループは、Grouper インターフェイスを実装することによって指定されます。

public interface Grouper<T> {
    String computeGroup(T key, String group);

    Class<T> getKeyType();
}

同じキータイプに対して複数の Grouper クラスが設定されている場合は、それらすべてが呼び出され、前のクラスで計算された値を受け取ります。キークラスにも @Group アノテーションがある場合、最初の Grouper はアノテーション付きのメソッドによって計算されたグループを受信します。これにより、組み込みグループを使用するときに、グループをさらに細かく制御できます。Grouper 実装の例を見てみましょう。

public class KXGrouper implements Grouper<String> {

   // The pattern requires a String key, of length 2, where the first character is
   // "k" and the second character is a digit. We take that digit, and perform
   // modular arithmetic on it to assign it to group "0" or group "1".
   private static Pattern kPattern = Pattern.compile("(^k)(<a>\\d</a>)$");

   public String computeGroup(String key, String group) {
      Matcher matcher = kPattern.matcher(key);
      if (matcher.matches()) {
         String g = Integer.parseInt(matcher.group(2)) % 2 + "";
         return g;
      } else {
         return null;
      }
   }

   public Class<String> getKeyType() {
      return String.class;
   }
}

Grouper 実装は、キャッシュ設定で明示的に登録する必要があります。プログラムを用いて Data Grid を設定している場合は、以下を行います。

Configuration c = new ConfigurationBuilder()
   .clustering().hash().groups().enabled().addGrouper(new KXGrouper())
   .build();

または、XML を使用している場合は、以下を行います。

<distributed-cache>
   <groups enabled="true">
      <grouper class="com.acme.KXGrouper" />
   </groups>
</distributed-cache>

3.3.8.7. 高度なインターフェイス

AdvancedCache には、グループ固有のメソッドが 2 つあります。

getGroup(groupName)
グループに属するキャッシュ内のすべてのキーを取得します。
removeGroup(groupName)
グループに属するキャッシュ内のすべてのキーを削除します。

どちらのメソッドもデータコンテナー全体とストア (存在する場合) を繰り返し処理するため、キャッシュに多くの小規模なグループが含まれる場合に処理が遅くなる可能性があります。