Jira DC キャッシュ レプリケーション - 開発のヒントとコツ
このページでは、Jira DC のリモート キャッシュ実装に関する特定のプロパティに焦点を当てます。
TLDR
ほとんどの場合は、DB にバッキングされたローダーを使用して無効化し、キャッシュをレプリケートすることをお勧めします。
非同期および同期レプリケーションの比較
Jira DC では、リモート キャッシュは常に非同期でレプリケートされたキャッシュです。
//Note: those cache settings are ignored in Jira DC
//Jira DC supports only async cache replication
new CacheSettingsBuilder().replicateAsynchronously();
new CacheSettingsBuilder().replicateSynchronously();
保証されているのは、特定のスレッド上でレプリケートされたキャッシュに対する操作が、同じ順序で他のノードにレプリケートされることのみです。
Jira Data Center キャッシュ レプリケーションの仕組みに関する詳細をご確認ください。
ReplicateViaInvalidation (無効化によるレプリケート) と ReplicateViaCopy (コピーによるレプリケート) の比較
レプリケーションには 2 種類あります。1 つ目は適切に動作する、シンプルかつ推奨されるタイプです。これはほとんどの Jira キャッシュが使用している無効化によるレプリケーションです。こうしたキャッシュには常にローダーが必要であるため、これらのキャッシュに対して実行される操作は removeByKey または removeAll のみとなります。これによって、他のすべてのノードに対して非同期の remove (key) または removeAll メッセージがトリガーされます。
次は、このようなキャッシュ定義の例です。
cacheManager.getCache(
getClass().getName() + ".cache", //cache name
this::loadValueForKey, //cache loader
new CacheSettingsBuilder().remote().replicateViaInvalidation().unflushable().build() // replicated via invalidation
);
警告
注: キャッシュ設定を明示的に指定しない場合は、デフォルトで次のようになります。
キャッシュはデフォルトで
remote
となります。- キャッシュはデフォルトで
replicateViaInvalidation
となります。 キャッシュ設定を常に明示的に指定することをお勧めします。
ローダーがないと、このようなキャッシュはおそらく役に立ちません。「役に立たないキャッシュ」をご参照ください。
通常、このようなキャッシュを使用したシーケンスは次のようになります。
value = ...;
updateDB(key1,value); // store the new value of key in the store (which the cache loader is using)
cache.remove(key1); // this invalidates the key in the local cache synchronously and asynchronously on other nodes by sending a remove(key1) message to all nodes
....
cache.get(key1); //triggers the loader to get the value for key from the shared store
もう 1 つのタイプのレプリケーションは、値によるレプリケーションです。このようなキャッシュは、ローダーによってバッキングされません。このキャッシュに対するすべての操作 (put、remove) を実行すると、put は put として、remove は remove としてレプリケートされます。このタイプは扱いにくく、すぐに使えるユース ケースはほとんどありません。
値は非同期的にレプリケートされます。これはレプリケートされたキャッシュで同期された共有ストレージではなく、すべての同期キャッシュの操作はローカルであることにご留意ください (つまり、変更がメモリ内のキャッシュに適用されて、レプリケートされたメッセージがローカル ストア (localq) に保持されます)。
ここでの問題は、非同期の put (これは put と remove のみの主な違いです) は、すべてのノードでキャッシュの状態が未定義になることです。異なる put (K1) 間の競合は、単一ノード上の異なる 2 つのスレッド間、または任意のノード間で発生します。クラスター ロックを使用してこの問題を解決する案もありますが、クラスター ロック サービスは DB に基づいている (また、動的キーを十分にサポートしていない) ため、無効化によってレプリケートされたキャッシュに切り替えることをお勧めします。
ただし、キャッシュに保存するデータがノード (ノード ID) によってキー設定されている、つまりクラスター内に特定のキーを更新しているノードが 1 つしかない場合は、これが問題にならないユース ケースがあります。こうした場合に保証する必要があることは、特定のノード上のキャッシュが単一のスレッドによってのみ更新されることのみです。
例: 各ノードが最後に作成された課題のタイムスタンプを格納するキャッシュを想定してみましょう。
cacheTimestampByNode = cacheManager.getCache(
getClass().getName() + ".cache",
null, //no loader
new CacheSettingsBuilder().remote().replicateViaCopy().unflushable().build() //replicated via copy
)
シーケンスは次のようになります。
onIssueCreate(issue) {
currentNode = getCurrentNodeId();
timestamp = issue.created();
runInCacheUpdateThread(
cache.put(currentNode, timesamp); //this updates the local cache synchronously and sends an async put(currentNode, timestamp) to all nodes
)
}
キャッシュを値でレプリケートしていずれのノードでも値を更新できるようにする場合は、方針変更が必要です。この場合は、同期するために高価で問題の多い別のレイヤーが必要となります (クラスター ロック、クラスター メッセージ)。さらには、ノードのライフサイクル (ノードのキャッシュのアップ/ダウン/クリア) にも対応する必要があります。
警告
クラスター同期メカニズムによって値でキャッシュをレプリケートするというパターンの使用は、可能であれば避けてください。
デリバリーの保証
Jira 8.12.0 以降では、異なるデリバリー保証を持つレプリケートされたキャッシュのタイプが 2 種類あります。
無効化によってレプリケートされたキャッシュでトリガーされたキャッシュ レプリケート操作 (remove/removeAll) は、デリバリーが保証されます。
値によってレプリケートされたキャッシュでトリガーされたキャッシュ レプリケート操作 (put/remove/removeAll) は、デリバリーが保証されません。
キャッシュ ライフサイクル
リモート キャッシュは動的に作成できませんが、アプリ/Jira ライフサイクルの一部として作成する必要があります。リモート キャッシュは、遅延のないシングルトン サービス コンストラクターで定義する必要があります。
リモート キャッシュが作成されると、このキャッシュを表す RMI キャッシュ ピアが作成されます。Jira/プラグインが起動している際にキャッシュが作成されない場合、他のノードで発生しているこのキャッシュに対する操作は、そのノードにレプリケートできません。
WARN [LOCALQ] [VIA-INVALIDATION] Abandoning sending because cache does not exist on destination node: [cache-name]
...
java.rmi.NotBoundException
...
他のノードが起動中の場合でも (平等に) 以上の状況が発生する可能性があります (Jira が RMI ポートを開いた状態で起動しているがプラグインがまだ起動中である、または Jira は起動しているもののこのキャッシュを持つプラグインが再起動中である場合。さらには、このようなキャッシュを持つプラグインが ZDU に互換性がなく、ZDU 中にこのキャッシュを使用している、またはこのキャッシュをまだ持っていない他のノードが存在する場合は、ZDU 中にこの問題が発生する可能性があります)。
役に立たないキャッシュ
上述のように、DC では通常必要とされる 1 種類のキャッシュがあります。
→ 永続データ ストア (DB) でバッキングされたローダーによる無効化によってレプリケートされたキャッシュ
もう 1 つのタイプのキャッシュは、データがノードによってキー設定される少数のユース ケースでのみ活用できます。
→ 指定されたキー値が設計上、1 つのノードでしか更新できない状況において、ローダーなしで値によってレプリケートされたキャッシュ
注意 - キャッシュ API では、Jira DC では完全に無用な他のキャッシュを作成できます。
例: ローダーなしで無効化によってレプリケートされたキャッシュ
(put と remove は remove としてレプリケートされるため、空でありながら非常にビジーであるキャッシュになります)
// node1
cache.put(k1, v); //replicated as remove(k1)
// node2
cache.put(k1, v); //replicated as remove(k1)
// node1
cache.get(k1) == null
// node2
cache.get(k1) == null
java.rmi.NotBoundException
java.rmi.NotBoundException の例外は、ノードがキャッシュ レプリケーション メッセージ (REMOVE、PUT) をレプリケートしようとしており宛先ノードが稼働しているものの、このキャッシュが他のノードで利用できない場合にスローされます。これは、一時的な状態 (後日説明します) または永続的な状態 (この場合は何かしらの問題があります。これについても後日説明します) のいずれかの場合があります。
Jira は、プラグインの起動時にリモート キャッシュが作成される (作成されるべきである) ことを想定しています。
そのため、通常、コードは次のようになります。
@Component
public class MyComponent {
private final Cache<Serializable, Serializable> myRemoteCache;
public MyComponent(@ComponentImport final CacheManager cacheManager) {
this.myRemoteCache = cacheManager.getCache(
"my.remote.cache.name",
null,
new CacheSettingsBuilder()
.replicateViaInvalidation()
.build());
}
}
ここでは、2 つのノード クラスターとキャッシュ レプリケーション イベントをノード 1 に作成します。この例外がいつ発生するのか確認してみましょう。
- 両方のノードが稼働して両方のノードでプラグインが初期化され、キャッシュが (ノードの起動時に) 作成される: java.rmi.NotBoundException - 発生しない
- ノード 1 が稼働していてノード 2 がダウンしている (ダウンとは、ノード 1 がノード 2 の RMI ポートに接続できないことを意味します): java.rmi.NotBoundException - 発生しない
- ノード 1 が稼働していてノード 2 が起動中 - すべてのプラグインが起動する前 (つまり、MyComponent がノード 2 に作成される前) に RMI ポートが開いている。java.rmi.NotBoundException - 発生する (ただし一時的な状況です)。ノード 1 はこのキャッシュ レプリケーション メッセージを配信するために、n 回 (ログ内の情報) 再試行します。また、ノード 2 が起動中でこのプラグインが完全に稼働すると、キャッシュがすぐに利用可能になると想定します。
- 両方のノードが稼働しているものの、ノード 2 上ではキャッシュが作成されなかった: java.rmi.NotBoundException - 発生する
考えられる説明:- Jira の異なるバージョンが両方のノードで実行されていて、プラグインに 1 つのバージョンが不足している - これはゼロ ダウン タイムのアップグレードを実行している場合に発生する可能性がありますが、通常は問題ありません (無効化によってレプリケートされた推奨のリモート キャッシュを使用している場合)
- リモート キャッシュがノードの起動時ではなく、ノードの存続期間中に作成された - これはコード内のエラーである場合と、すべてのノードでキャッシュの作成を同期させるために別のメカニズム (クラスター ロック) が必要である場合が考えられます。いずれにせよ推奨されません。
リンク
Jira Data Center のキャッシュ レプリケーション