Infinispanの分散キャッシュ
August 23, 2025Inifinispanは有償インメモリーデータベースRed Hat Data GridのOSSとして提供されており、OIDCによるSSOの機能を提供するKeyCloakでキャッシュサーバとして利用できる。 Red HatはKeyCloakの開発も支援している。
Infinispanの用途は、InifinispanはRESPプロトコルを実装しているなど、RedisやMemcachedと近い。 公式ドキュメントでRedisやmemcachedを置きかえるユースケースを紹介されている。 Inifinispanは、Javaで実装されており、JavaのクライアントとJSR107のJCache APIで通信できる。 たとえば、クライアントは、CacheManagerでハッシュマップ構造のCacheを取得し、エントリを操作できる。
Infinispanクラスタのノードをクライアントと同じJVMで動かすこともできる。 ただし、JVMを同じくする場合、クライアントを停止してもキャッシュを維持したり、違うノード数でクラスタを構成したりすることは難しいだろう。 KeyCloakの高可用性に関する文書では、KeyCloakと別のサーバーでクラスタを構築することを推奨している。
ノードの実行環境だけでなく、Cacheにも保存方法の選択肢がある。 保存方法はlocal, replicated, distributed, invalidationの4通りあり、クライアントと違うJVMで起動したノードでクラスタを構成したときは、replicatedかdistributedのいずれかを選ぶ。 Invalidationは推奨されていない。
Replicated cacheのエントリは全ノードに保存されるが、distributed cacheのエントリは一部のノードにしか保存されない。 レプリケーションのリクエスト数はノード数に対して線形であり、ノード数が10以上になるときは、処理性能を下げないようにdistributed cacheが推奨されている。 つまり、Infinispanの高可用性と処理性能を上げるには、クライアントと別のプロセスでノードを起動し、distributed cacheを選ぶことになる。
Distributed cacheのエントリを保存するノードは、コンシステントハッシュ法によって決まる。 具体的なアルゴリズムはどのConsistentHashFactoryの具象クラスを採用するかで決まる。 原典のコンシステントハッシュ法は、キーを保存するノードを分散するために、キーとノードを単位区間のハッシュ値に写像し、ハッシュ値の隣接するノードにキーを保存する。 具体的なアルゴリズムを過去に記事にした。
ConsistentHashFactoryのアルゴリズムは原典と違う。
Infinispanに備わるトランザクションやレプリケーションは、原典のコンシステントハッシュ法では解決できない。
Infinispanはエントリを保存する集合から1つのノードをprimary ownerに選ぶ。
残りのノードはbackupと呼ばれる。
Backupへの書き込みリクエストはprimary ownerに転送される。
Primary ownerはエントリを保存した後に、その結果をBackupに転送する。
InfinispanのConsisitentHashFactoryは、エントリを分散して配置するだけでなく、primary ownerとbackupの選出も担う。
特に指定しなければConsistentHashFactoryの実装にSyncConsistentHashFactoryが選ばれる。
以下では、15.2.5-final版の実装の概要について説明する。
createメソッドは、int numOwners, int numSegments,List<Address> members, Map<Address, Float> capacityFactorsを引数にとる。
numOwnersはエントリを保存するノード数、membersはノードを表す。
Primary ownerを先頭とするnumOwners個のノードのリストをセグメントといい、SyncConsistentHashFactoryはnumSegments個のセグメントを生成する。
エントリのキーとセグメントのハッシュ値を比較し、キーに隣接するセグメントのノードにエントリを保存する。
セグメントごとにノードやその順序を変えることで負荷分散、primary ownerの選出、レプリケーションを実現する。
ノードに割り当てるセグメントも両者のハッシュ値の距離によって決まる。
capacityFactorsで、割り当てるセグメント数やセグメント内の順序をノードの性能に合わせて調整できる。
numOwnersは実行中でも可変だがnumSegmentsは変えられない。
ハッシュ空間は[0..2^63-1]であり、long型で表現できる非負の整数の集合に等しい。
セグメントのハッシュ値は、隣接するセグメントのハッシュ値と等間隔(Long.MAX_VALUE/numSegments)になるように決まる。最初のセグメントのハッシュ値はLong.MAX_VALUE/(2*numSegments)からはじまる。
以下のcomputeSegmentHashesでsegmentHashesにハッシュ値を記録する。
private long[] computeSegmentHashes(int numSegments) {
assert segmentSize != 0;
long[] segmentHashes = new long[numSegments];
long currentSegmentHash = segmentSize >> 1;
for (int s = 0; s < numSegments; s++) {
segmentHashes[s] = currentSegmentHash;
currentSegmentHash += segmentSize;
}
return segmentHashes;
}
エントリーのキーを保存するセグメントを決めるためにMurMurHash3を使う。
キーにMurMurHash3を適用して得られたハッシュ値の下位64ビットをセグメント数で割り、セグメントを決める。
実際の実装はgetSegmentForHashにある。
public int getSegmentForHash(int hash) {
// The result must always be positive, so we make sure the dividend is positive first
return (hash & Integer.MAX_VALUE) / segmentSize;
}
ノードのハッシュの計算にもMurMurHash3を使う。
ノードの場合、原典のコンシステントハッシュ法にあるvirtual nodeを使うため、実際にはvirtual nodeを求める。
virtual nodeの数は、Builderのコンストラクタのようにセグメント数-1の対数に近い値になる。
numNodeHashes = 32 - Integer.numberOfLeadingZeros(numSegments);
virtual nodeのハッシュ値は、virtual nodeの配列の番号をシードとしてノードのUUIDにMurMurHash3を適用した値になる。
virtual nodeとsegmentのハッシュ値を求めたら、今度はノードにセグメントを割り当てる。 はじめに、すべてのセグメントのprimary ownerを決め、その後順番にセグメントのbackupの集合を決める。 セグメントとノードの距離は、セグメントのハッシュ値と最も近いvirtual nodeの距離をスケールした値になる。 全ノードで最小のキャパシティをノードのキャパシティで割った値でスケールする。 最大のハッシュ値をもつvirtual nodeと最小のハッシュ値をもつvirtual nodeは互いに隣接するとみなす。 各セグメントのbackupの集合を決めるときは、ハッシュ値を基準した優先度つきキューにすべてのノードを追加した後に、集合の数だけキューから取り出す。