コンテンツにスキップ

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 サーバーが起動するわけではありません。

ClusterConfigEngineOptions と分けて保存・読み込みできます。各ノードに同じメンバー構成を置き、起動時にローカルの 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 が必要です。primaryNodeIdClusterConfig 内に存在する場合、runtime はその config entry から primaryHostprimaryReplPort を補完できます。

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_TARGETSQUORUM は 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 を待たないAckPolicyModeNONE、または 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 が変わっている可能性。