ブローカーのサイジングとスケーリング
kanade は単一の NATS + JetStream ブローカーの上で動きます。フリートが大きくなる (数百台 → 数千台) と、スケーリングのボトルネックになるのはエージェントではなくブローカーです。このページでは、エージェント1台あたりのフットプリント、#512 の作業で何が変わったか、単一ノードで注意すべき限界、そしてスケールアップ中に実測値を採取して起動スプレイを入れるかブローカーを増強/クラスタ化するかを判断する方法を扱います。
エージェント1台あたりの consumer フットプリント
稼働中のエージェントはそれぞれ JetStream の consumer をいくつか保持します。KV watch ごとに ordered push consumer が1つ、それに加えてコマンド再生用の durable consumer が1つです:
| Consumer | 由来 |
|---|---|
agent_config watch (キーで絞り込み) | config_supervisor |
agent_groups watch (所属 → 実効 config) | config_supervisor |
agent_groups watch (所属 → 購読) | groups.rs |
schedules watch | local_scheduler |
jobs watch | local_scheduler |
fleet_config の freeze watch (単一キー) | local_scheduler |
EXEC durable replay | command_replay |
つまりエージェント1台あたり約7 consumerです。この数はエージェントごとにほぼ固定で、キーで絞り込んでも減りません。したがってブローカー側の総数はフリートに比例して増えます:
| フリート規模 | ブローカー上の consumer 数 (概算) |
|---|---|
| 15 | 約 105 |
| 500 | 約 3,500 |
| 3,000 | 約 21,000 |
v0.43.96 で変わったこと (と変わらなかったこと)
#512 (v0.43.96 で出荷) が叩いたのは超線形のコストであって、consumer の数ではありません:
#832—agent_configのキー絞り込み watch。 エージェントはバケット全体 (watch_all) ではなく、global、pcs.<self>、自分のgroups.<g>だけを watch します。これで2つの爆発が消えます:- PC 単位の書き込みファンアウト:
pcs.<id>1件への書き込みが全 N エージェントに届かなくなりました (以前は届いていて、N−1 台が分類しては捨てていました)。 - 再接続時の再同期ストーム: 再同期はバケット全体に対する
keys()+ キーごとの走査ではなく、エージェントあたり 3〜5 回の直接getになりました。総計で O(N²) → O(N) です。
- PC 単位の書き込みファンアウト:
#839— 単一キーの freeze watch。fleet_configはKEY_FREEZEしか持たないので、freeze の watcher はwatch_all+ クライアント側フィルタではなくwatch(KEY_FREEZE)を使います。
変わっていないのは、エージェント1台あたり約7 consumer というフットプリントと、schedules / jobs の watch_all です (これらは全エージェントがターゲティング判定のために評価する共有カタログで、無くすにはサーバー側ターゲティングという別の変更が要ります)。したがって 3,000 台では依然として約 21,000 consumer と、同期した再接続による接続/consumer 作成のバーストを見込んでおく必要があります。
単一ノードの限界と再接続の群れ
単一ノードの JetStream でサイジングすべきものは2つです:
-
定常状態のフットプリント — 3,000 台での約 21,000 consumer は JetStream のメタ (Raft) 層に載り、メモリとファイルハンドルを消費します。目標 N に合わせてブローカーホストの RAM と JetStream の
max_file/max_memoryを設定し、メタ層の健全性を監視してください。 -
再接続の群れ (herd) — 多数のエージェントが_同一の瞬間に_再接続すると (ブローカー再起動、朝の電源投入の波、多数の PC を巻き込むネットワーク事象)、接続を張り直して consumer をバースト的に作り直します。キー絞り込みによって各エージェントの再同期はすでに数回の安価な
getまで落ちているので、危険な O(N²) の読み込みストームは消えています。ただし接続 + consumer 作成のバーストは依然として O(N) で、しかも同期しています。なお、ランダムで_同期していない_再接続 (ノート PC 1台の wifi 瞬断) は群れではありません。群れになるのはフリート全体が同期した事象だけです。
ストレージ: 50 GB のファイルストア、リソース単位の上限、保持期間
RAM と consumer はサイジングの話の半分にすぎず、もう半分はディスクです。JetStream のデータはすべて1つのディレクトリ (store_dir: C:/ProgramData/Kanade/nats/jetstream) の下に置かれ、ブローカー全体として max_file_store: 50GB (configs/nats-server.conf) で制限されます。これは全ストリームと全オブジェクトストアで共有されるソフト上限であって、ストリーム単位のものではありません。埋まっていくと何が起きるか:
- 自前の保持設定を持つリソースは古いものから退避し (
DiscardPolicy::Old)、健全なままです。 - 上限を持たないリソースは退避できません。そのためファイルストアが一杯になると、フリート全体で_あらゆる_ JetStream publish が「insufficient storage resources available」(エラー 10077) で失敗し始めます — エージェントの結果アップロード、collect バンドルのアップロード、publish、KV put。読み取りは動き続け、死ぬのは書き込み側です。上限の無いリソースは、上限のあるリソースの取り分も押しのけます。
したがって全リソースが上限を持たねばならず、実際に持っています:
-
ストリーム (
RESULTS/INVENTORY/AUDIT/OBS_EVENTS/NOTIFICATIONS/EXEC/EVENTS) はそれぞれmax_ageとmax_bytesを持ちます (bootstrap.rs、合計で約 5.3 GiB を確保)。これらは転送 + 再生のバッファであり、永続的な記録はバックエンドの SQLite です。運用者が見たいと思う履歴より上限を短く取れるのはそのためです。 -
オブジェクトストアはバケットごとに
max_bytesで制限され、SPA から再起動なしで変更できます (設定 → server → オブジェクトストアのディスク上限、#1247)。空欄にすると以下の組み込み既定値に戻ります:バケット 既定の上限 格納するもの result_output1,024 MiB 大きすぎる stdout/stderr の blob (数秒で SQLite に投影されます) agent_releases2,048 MiB エージェントの実行ファイル (約20バージョン) app_packages5,120 MiB 運用者が用意したインストーラ scripts256 MiB マニフェストのスクリプト本体 collections5,120 MiB collect ジョブのバンドル ( max_ageもあり、収集バンドルの保持期間 で変更できます)バックエンドは設定値を裏側の
OBJ_*ストリームへ起動のたびと保存のたびに反映します。上限が導入される前に作られたバケットにも上限が行き渡るのはこの仕組みによります。既定の合計確保量は約 13.5 GiB で、ストリームと合わせて 50 GB に十分収まるよう設計されています。 -
Recovery. If a stream or bucket has drifted from its expected config or is corrupted, repair it with the
natsCLI and an administrative credential, not withkanade. The backend recreates anything missing the next time it starts. This works even when the backend cannot start (a drifted stream config makes its startup bootstrap fail), because the scripts talk to the broker directly. Stopkanade-backendfirst (its projectors hold durable consumers) and start it again afterwards:# delete one stream / KV bucket / object store (asks you to type the name) ./scripts/ops/jetstream-delete.ps1 -Kind stream -Name RESULTS -Server nats://127.0.0.1:4222 -Creds ./admin.creds # wipe everything kanade uses: dry run first, then add -Yes ./scripts/ops/jetstream-reset.ps1 -Server nats://127.0.0.1:4222 -Creds ./admin.creds ./scripts/ops/jetstream-reset.ps1 -Server nats://127.0.0.1:4222 -Creds ./admin.creds -YesBoth need the
natsCLI onPATHand also readNATS_URL,NATS_CREDS,NATS_USERandNATS_PASSWORD. Deleting a resource deletes its data. -
SQLite (投影) も_無制限ではありません_。バックエンドのクリーンアップタスクが5分ごとに、区切られたバッチで刈り取ります —
execution_results/executions/obs_events/inventory_historyが 90 日、audit_logが 365 日、host_perf_samplesが 30 日、process_perf_samplesが 7 日です。どの DB の窓も、対応するストリームの窓より意図的に長く取ってあります。ストリームが再生できるものは必ず SQLite に既にある、ということです。残りのテーブルは upsert/replace 型 (現在の状態ぶんの大きさ) で、死んだエージェントはagent_prune_days(これも ServerSettings のノブ) で刈られます。
リソースごとの実使用量 (使用バイト数と上限) は SPA の JetStream ページにあります。ブローカーの現在の設定を読むので、上限を変えると次回の読み込みで反映されます。
打ち手 (優先順)
- まずブローカーのサイジング。 目標 N での定常 consumer 数を賄えるだけの RAM とファイル上限を、この単一ノードに与えてください。これが主たる打ち手で、他はすべて副次的です。
- 起動スプレイ — 実測したときだけ。 再接続の再同期の前に PC ごとの決定的な遅延 (
hash(pc_id)) を入れれば、consumer 作成と接続のバーストを一定の窓に均せます。これは設計判断としてまだ製品に入っていません。PR1 ですでに二次の項は消えており、async-nats の再接続バックオフとnats_retryの ±25% ジッタがある程度はバーストを散らしているからです。またスプレイは、群れではない単発の瞬断も含めてすべての再接続に遅延を足すので、実際に群れがブローカーを苦しめていないかぎり差し引きで損になります。データで判断してください (次節)。同期した事象で consumer 作成の遅延、接続の滞留、JetStream API のエラーが見えたなら、スプレイを入れます (Disconnected → Connected後の最初の同期だけに限定し、数秒で頭打ちにします)。 - エージェントあたりの consumer を減らす。 可能なら watch をまとめ (freeze watch はすでに単一キー、2本の
agent_groupswatch は統合の候補)、KV の履歴は浅く保ちます (agent_config/agent_groupsはhistory: 1)。 - JetStream をクラスタ化する。 1ノードで抱えられる量を超えたら、JetStream クラスタへ移ります。これが最後の手段であり、最も大きな変更です。
規模での実測 (事後分析)
本番のブローカーを立ち上がりの最中にライブで眺めるのは、たいてい無理です。代わりに同梱の collect: ジョブで数値を採取し、あとからバンドルを見てください。
バックエンド / NATS のホスト上で、できれば同期した再接続事象の最中か直後に実行します (スプレイの是非を決めるのは群れの瞬間です):
kanade exec collect-broker-health --pcs <backend-host-id>
このジョブ (configs/jobs/collect-broker-health.yaml) はブローカーを約3分にわたってサンプリングし、バンドルを OBJECT_COLLECTIONS にアップロードします。SPA の 収集 ページからダウンロードしてください (zip をそのままレビュー担当に渡しても構いません)。読み取り専用で、事前準備は一切不要です。NATS の認証なしの HTTP monitoring ポート (既定 8222 の /jsz エンドポイント) を読むだけなので、SYSTEM の PATH に nats CLI を置く必要も、トークンも要りません。必要なら対象側で KANADE_BH_SAMPLES / KANADE_BH_INTERVAL_SEC (ブローカーの http_port が違う場合は KANADE_BH_MON_PORT も) で窓を調整できます。
バンドルの内容:
- 時系列 (
connz-*.json、jsz-*.json) — サンプルごとの接続数 (/connz) と JetStream の consumer 数 (/jsz)。群れの瞬間にここが跳ねていれば、それがスプレイの signal です。 - consumer フットプリント (
jsz-full.json) —/jsz?consumers=true&streams=true。consumer の全リストで、合計が約 7N であることと、どのストリームが抱えているかを確認できます。 - サーバーの健全性/リソース (
varz.json、healthz.json) —/varz(メモリ、CPU、接続数、slow consumer) と/healthzの状態。 - ログの末尾 (秘匿情報は伏せ済み) — backend と nats-server。
何を見るか
- サンプルを通して consumer/接続数がなめらかで、
/healthzが健全、/varzのメモリ/CPU に余裕がある → ブローカーはその事象を吸収できています。スプレイは不要で、N に先回りしてサイジングを続けるだけです。 - 事象の時点で接続の滞留や consumer 作成の遅延がとがっている、JetStream API がエラーを返す、メモリ/CPU が張り付く → 群れは実在します → 起動スプレイ (打ち手2) を入れる、あるいはブローカーを増強します。
#828のダウングレード・フラッピングの回帰は、このジョブではなくOBS_EVENTSのagent_updateタイムラインから (バックエンド API 経由で) 別途確認します。そのデータはすでにフリート全体に対して問い合わせ可能です。