コンテンツにスキップ

Cluster の使い方

このページは、Cluster のメンバー構成を定義し、ノードを起動したい場合に読むページです。実行時の内部構造、ルーティング、通信形式、運用上の境界は Cluster のアーキテクチャ に分けています。

Cluster は peer を自動発見しません。components.clusterEnabled を有効にする前に、ローカルノードを識別し、クラスタ全体を説明する情報を用意します。

項目理由
安定した node idローカルプロセスを 1 つの NodeInfo に対応させるため。
メンバー一覧各ノードの host、data/API port、replication port、capability を定義するため。
配置モードlocal-only、mirror、key owner 分散のどれにするかを決めるため。
起動ロールプロセスを Primary / Replica のどちらで起動するかを明示するため。
Primary 接続先Replica が Primary のレプリケーション待ち受けに接続するため。
ACK ポリシーPrimary の書き込みが Replica の確認をどこまで待つかを決めるため。
通信モードlocal/private network 向けの plain か、secure replication かを選ぶため。

Cluster は、レプリケーション実行層と配置情報です。service discovery、client routing、自動 failover、snapshot orchestration は、server、control plane、deployment tooling 側で持つ前提です。

ReplicationMode は配置方法を決めます。この値は ClusterConfig に保存されます。

モード意味
STANDALONEローカルだけで動作します。レプリケーションは不要です。
MIRROR書き込みをすべてのデータ保持ノードへ送ります。
STRIPEキーごとに、決定的な rendezvous hashing で 1 つのデータ保持ノードを選びます。

STRIPE は、ノード構成を変えても既存データを自動で移動しません。Striped な構成でメンバーを変える場合は、運用上の移行作業として扱います。

目的まず使うものメモ
1 プロセス、またはローカルテストSTANDALONECluster を無効、または local-only に近い形で扱います。
同じ書き込みを複数ノードに持たせるMIRRORPrimary が entry と Blob payload を Replica へ送ります。
キーごとに 1 つのデータ保持ノードを決めるSTRIPE配置は決定的ですが、移行は外部で行います。
自動 scale-out / rebalance外部 control plane現在の Cluster は基本部品であり、自動スケール機構ではありません。

現時点で最も素直な replicated deployment は MIRROR です。STRIPE は、client routing、membership change、data migration を上位層で制御する場合に向いています。

非 standalone モードでは、NodeStartupRole を明示します。

起動ロール実行時ロールメモ
PRIMARYPRIMARYローカルノードが config 内に存在し、COORDINATOR_ELIGIBLE を持つ必要があります。
REPLICAREPLICAPrimary の node id、host、replication port を解決できる必要があります。
AUTOMIRROR / STRIPE では拒否Cluster モードでの自動ロール選択は未実装です。

Primary / Replica は実行時ロールです。配置モードとは別の概念です。たとえば MIRROR でも、書き込みを送る Primary プロセスと、それを受け取る Replica プロセスがあります。

最初の replicated setup では、構成を単純にして明示的に起動します。

手順Node 1Node 2
node.id を作る12
cluster.akcc を置く同じ 2 ノード構成同じ 2 ノード構成
起動ロールPRIMARYREPLICA
Primary id11
レプリケーションポートNode 1 の replPort で待ち受けNode 1 の replPort へ接続
データディレクトリNode 1 専用Node 2 専用

各プロセスには専用の dataDir を持たせます。2 つのノードで同じ WAL、SST、Blob、VersionLog ファイルを共有しないでください。

各エンジンインスタンスは、安定した数値の node id を持ちます。AkkEngine::open() は paths.nodeIdPath から node id を読み込み、ファイルがなければランダムな非ゼロ uint64_t を生成して保存します。

paths.dataDir を設定している場合、既定パスは次の通りです。

<dataDir>/node.id

非 standalone モードでは、この id が ClusterConfig 内の NodeInfo::nodeId と一致している必要があります。実運用では、初回の Cluster 起動前に node.id を作成・配置するか、生成された id を読んで config に追加してから components.clusterEnabled を有効にします。

node.id はローカルプロセスの identity です。メンバー構成を自動生成する仕組みではありません。削除して別の id が作られると、他ノードが期待する NodeInfo と一致しなくなる可能性があります。

Cluster 設定は cluster::ClusterConfig で表します。node id、peer host、data/API port、replication port、node capability、配置モード、ACK ポリシーを保持します。

#include "akk/engine/AkkEngine.hpp"
#include "akk/engine/cluster/ClusterConfig.hpp"
#include <utility>
#include <vector>
namespace engine = akkaradb::engine;
namespace cluster = akkaradb::engine::cluster;
cluster::ClusterConfig makeConfig() {
std::vector<cluster::NodeInfo> nodes{
{
.nodeId = 1,
.host = "127.0.0.1",
.dataPort = 7070,
.replPort = 7170,
.capabilities = cluster::COORDINATOR_ELIGIBLE | cluster::DATA_BEARING,
},
{
.nodeId = 2,
.host = "127.0.0.1",
.dataPort = 7071,
.replPort = 7171,
.capabilities = cluster::DATA_BEARING,
},
};
cluster::AckPolicy ack{};
ack.mode = cluster::AckPolicyMode::ALL_TARGETS;
ack.stage = cluster::AckStage::APPLIED;
return cluster::ClusterConfig{
std::move(nodes),
cluster::ReplicationMode::MIRROR,
ack,
};
}

NodeInfo::host は他ノードから接続されるアドレスです。replPort はレプリケーション待ち受けポートです。dataPort はそのノードの公開データ/API ポートとして記録される値で、これだけで API サーバーが起動するわけではありません。

ClusterConfig は EngineOptions と分けて保存・読み込みできます。各ノードに同じメンバー構成を置き、起動時にローカルの role だけを選びます。

cluster::ClusterConfig cfg = /* build or load from your own config source */;
cfg.save("data/node-1/cluster.akcc");
cfg.save("data/node-2/cluster.akcc");

各プロセスでは、ローカルの copy を参照します。

EngineOptions opts;
opts.components.clusterEnabled = true;
opts.paths.dataDir = "data/node-2";
opts.paths.clusterConfigPath = "data/node-2/cluster.akcc";

cluster.akcc には、メンバー構成、配置、capability、ACK ポリシーが保存されます。起動 role、secure pin、Primary の上書き、bind host のような実行時専用値は EngineOptions 側に残ります。

AkkEngine::open() の前に Cluster コンポーネントを有効にします。opts.cluster.config が指定されていればそれを使い、指定されていなければ paths.clusterConfigPath を読み込みます。

engine::AkkEngineOptions opts;
opts.paths.dataDir = "data/node-1";
opts.paths.nodeIdPath = "data/node-1/node.id";
opts.components.clusterEnabled = true;
opts.cluster.config = makeConfig();
opts.cluster.runtime.startupRole = cluster::NodeStartupRole::PRIMARY;
opts.cluster.runtime.transportMode = cluster::TransportMode::PLAIN;
opts.cluster.runtime.replBindHost = "127.0.0.1";
auto db = engine::AkkEngine::open(std::move(opts));

Primary 起動は、ローカル node id が config 内に存在しない場合、またはローカルノードが COORDINATOR_ELIGIBLE を持たない場合に失敗します。レプリケーション待ち受けには、ローカルノードに設定された replPort が使われます。

同じ書き込み stream に対して、Primary として起動するプロセスは 1 つだけにします。現在の Cluster runtime は leader election や fencing を持たないため、split-brain の防止は deployment layer の責務です。

Replica 起動には Primary の id が必要です。primaryNodeId が ClusterConfig 内に存在する場合、runtime はその config entry から primaryHost と primaryReplPort を補完できます。

engine::AkkEngineOptions opts;
opts.paths.dataDir = "data/node-2";
opts.paths.nodeIdPath = "data/node-2/node.id";
opts.components.clusterEnabled = true;
opts.cluster.config = makeConfig();
opts.cluster.runtime.startupRole = cluster::NodeStartupRole::REPLICA;
opts.cluster.runtime.primaryNodeId = 1;
opts.cluster.runtime.transportMode = cluster::TransportMode::PLAIN;
auto db = engine::AkkEngine::open(std::move(opts));

Primary の接続先は直接上書きできます。

opts.cluster.runtime.primaryNodeId = 1;
opts.cluster.runtime.primaryHost = "10.0.0.10";
opts.cluster.runtime.primaryReplPort = 7170;

Primary id が未指定、ローカルノード自身を指している、coordinator 候補ではない、または host と replication port を解決できない場合、Replica 起動は失敗します。

Replica クライアントはバックグラウンドで再接続します。接続やハンドシェイクに失敗した場合、エンジンが開いている間は約 200 ms 待ってから再試行します。

Replica が新規の場合、または停止時間が短く、外部 snapshot やメモリ上の catch-up window で差分を補える場合は、次の流れで復帰させます。

  1. Replica に新しい snapshot が必要なら、書き込みを止めるか一貫した source copy を取ります。
  2. Replica 専用の dataDir と安定した node.id を用意します。
  3. Primary と Replica の両方を含む cluster.akcc を置きます。
  4. NodeStartupRole::REPLICA と期待する primaryNodeId を指定して起動します。
  5. Replica が接続し、新しい entry を適用していることを logs / stats で確認します。

Primary が保持するのは短いメモリ上の entry buffer だけです。この範囲を超えて遅れた Replica は、再接続前に外部の data copy で追いつかせる必要があります。

計画的に Replica を外す場合は、次の流れにします。

  1. Replica process を停止します。
  2. 残るノードで使う durable cluster config を更新します。
  3. deployment model に合わせて process を restart / reload します。
  4. Primary の ACK policy を確認します。ALL_TARGETS と QUORUM は live replica 数に依存します。
  5. recovery に不要になったことを確認してから、Replica の data directory を削除または保管します。

現在の membership change は config change として扱います。Cluster は striped data の rebalance や file migration を自動では行いません。

AckPolicy は、Primary の書き込みが Replica の確認をどこまで待つかを制御します。

ポリシー意味
NONEReplica からの ACK を待ちません。
ALL_TARGETS現在 live なすべての Replica が対象 sequence を ACK するまで待ちます。
QUORUM少なくとも quorum 個の live Replica が対象 sequence を ACK するまで待ちます。

AckStage は、ACK が何を意味するかを決めます。

StageReplica 側の動作
RECEIVEDentry bytes を受信し decode した後に ACK します。
APPLIEDローカルエンジンへ適用した後に ACK します。
DURABLEローカルの durability sync を強制してから ACK します。

強い stage ほど確認内容は強くなりますが、write latency は増えます。Blob frame は Replica へ送られますが、entry ACK ポリシーの待機対象にはなりません。

cluster::AckPolicy fireAndForget;
fireAndForget.mode = cluster::AckPolicyMode::NONE;
cluster::AckPolicy appliedOnAll;
appliedOnAll.mode = cluster::AckPolicyMode::ALL_TARGETS;
appliedOnAll.stage = cluster::AckStage::APPLIED;
cluster::AckPolicy oneDurableReplica;
oneDurableReplica.mode = cluster::AckPolicyMode::QUORUM;
oneDurableReplica.quorum = 1;
oneDurableReplica.stage = cluster::AckStage::DURABLE;

DURABLE は、Replica が local durability path を強制してから ACK することを求めます。組み込み stage の中では最も強い確認ですが、その分コストも高くなります。復旧時の意味を latency より優先する経路で使います。

TransportMode::SECURE では、レプリケーション frame の前に native secure channel を開きます。secure.identitySeedPath が空で、paths.dataDir が設定されている場合、runtime は次の path を使います。

<dataDir>/cluster.identity
opts.cluster.runtime.transportMode = cluster::TransportMode::SECURE;
opts.cluster.runtime.secure.identitySeedPath = "data/node-1/cluster.identity";
opts.cluster.runtime.secure.expectedPrimaryNodeId = 1;

TransportMode::PLAIN は、すべての advertised node host が loopback、link-local、unique-local IPv6、または private IPv4 の場合だけ許可されます。public/WAN host は拒否されるため、SECURE を使ってください。

secure replication では、Replica 側で接続先 Primary の node id を固定できます。

opts.cluster.runtime.primaryNodeId = 1;
opts.cluster.runtime.secure.expectedPrimaryNodeId = 1;

より厳密に検証する場合は、pinnedPeers に cluster node id ごとの期待 public key を入れます。この key は各ノードの cluster.identity seed から導出されます。identity seed は durable に保持し、backup 対象にしてください。置き換えると、そのノードの secure identity が変わります。

paths.dataDir が設定されている場合、Cluster 関連の既定パスは次の通りです。

Path既定値
paths.clusterConfigPath<dataDir>/cluster.akcc
paths.nodeIdPath<dataDir>/node.id
Cluster manifest<dataDir>/cluster.akmf
Secure identity seedsecure mode が既定 path を必要とする場合、<dataDir>/cluster.identity

cluster.akcc はメンバー構成とポリシーを保存します。replBindHost、Primary の上書き、secure pin、transport mode のような実行時専用値は、Cluster config には保存されません。

Replica を停止し、data directory は保持したまま、同じ node.id と config で起動します。停止時間が短ければ、handshake 時に Primary が buffered entry を replay できます。長時間停止していた場合は、再接続前に snapshot で Replica を更新します。

Primary を再起動する前に書き込みを止めます。同じ node.id、config、PRIMARY role で起動します。Replica はバックグラウンドで再接続し、利用可能な catch-up window の範囲で local sequence 以降の entry を受け取ります。

別ノードを Primary に昇格する処理は、runtime の自動機能ではなく運用手順です。対象ノードを選び、その data が用途に対して十分に新しいことを確認し、config と client routing を更新してから、Primary として起動する process を 1 つだけにします。

症状確認すること
selfNodeId not foundバイナリの node.id の値が NodeInfo::nodeId のどれとも一致していない。
Primary 起動が拒否されるローカルノードに COORDINATOR_ELIGIBLE が付いていない。
Replica 起動が拒否されるprimaryNodeId がない、ローカル node id と同じ、または coordinator 候補ではない。
config load が失敗するcluster.akcc がない、壊れている、version が違う、CRC が合わない。
plain transport が拒否されるNodeInfo::host に public/WAN address が含まれている。
Cluster backend が見つからないcluster runtime backend がリンクされていない、または runtimeBackendPath から load できない。
症状確認すること
Replica が再接続を繰り返すPrimary host、replPort、transport mode、secure identity の期待値。
書き込みが Replica を待たないAckPolicyMode が NONE、または policy を満たす live replica がない可能性。
QUORUM が完了しないquorum が、選択した stage を ACK できる connected replica 数より大きい可能性。
reconnect 後に Blob data が足りないBlob frame は Primary の catch-up buffer に残らないため、長い gap では snapshot/copy が必要。
STRIPE の read が membership change 後に外れる外部 migration なしで key ownership が変わっている可能性。