クラスターとグリッド

複数の SENCA のノードを 1 つのクラスターにまとめる方法です。設定を Raft で全ノードに複製し、入力・出力・パイプライン・ルームをグリッドでノードと GPU に配置します。障害が起きたときの移動、デグレード、ノード間の中継も説明します。

複数台で動かす

このページの見出し
  1. 全体の構成
  2. クラスターの構築
  3. ストリームの所有権と、どのノードでも視聴できる仕組み
  4. グリッド
  5. ワークロードごとの見積もり
  6. デグレード・追い出し・復帰
  7. 運用の操作
  8. 例
  9. 障害時の動作と所要時間
  10. 関連するページ

SENCA のノード(senca-node を動かすサーバー)を複数台まとめてクラスターにすると、ノードや GPU が 1 台止まっても、配信や加工の処理を別の場所で動かし続けられます。

このページの要点:

  • 設定(コントロールプレーンのデータ)は、Raft(複数台のサーバーで同じ記録を同じ順序で持ち合う合意の方式)で全ノードに複製されます。投票するノードは 1〜7 台です。
  • 配信を受け取ったノードが、そのストリームの「所有者」になります。所有者はフェンシングトークン(所有者が替わるたびに増える番号。古い所有者が誤って動き続けるのを防ぎます)で管理されるため、同じストリームが 2 か所で受信されることはありません。
  • 所有者でないノードで視聴されたときは、所有者からノード間の中継で 1 本だけ受け取り、手元のミラー(複製)から配信します。
  • グリッドは、ノードの CPU・メモリ・帯域・GPU・キャプチャデバイスを集計して、入力・出力・パイプライン・ルーム・メディアパイプラインを配置します。ノードやデバイスが止まると、別のノードへ移すか、一部の処理を外した状態で動かし続けます(デグレード)。復旧すると元に戻します。
  • メンテナンスの前には、ドレイン(新しい割り当てを止め、動いている処理を他のノードへ移すこと)を使います。

以下では、グリッドが配置する処理の単位(入力・出力・パイプライン・ルーム)をワークロードと呼びます。

グリッドは単一ノードでも同じように動いています。そのノード自身がリーダーで、配置先もそのノードだけ、という形です。 そのため配置の指定(placement)、デグレード、GET /api/v1/grid は、単一ノードでも使えます。

全体の構成

構成要素役割
Raftコントロールプレーンへのすべての変更を全ノードに複製します。投票するノード(投票者)は 1〜7 台です
所有権配信を受信したノードが、そのストリームの所有者になります(フェンシングトークン付き)
中継所有者でないノードで視聴するとき、所有者からエンコード済みのフレーム(AU。アクセスユニット)を転送し、そのノード上のミラーから配信します
グリッドリーダーが、定期的な生存通知(ハートビート)からノードの停止を検出します。毎秒ワークロードの配置を決めて、合意ログに記録します。各ノードは自分に割り当てられた分だけを動かします

複製されるのはコントロールプレーンのデータだけです。メディア(HLS のセグメントや録画のファイル)は、それを作ったノードのディスクにあり、 複製されません(録画)。

クラスターの構築

設定ファイル

クラスターの設定は JSON で書き、--cluster-config FILE で渡します(または --config の JSON の cluster に同じ内容を書きます)。 未知のフィールドがあると、起動時にエラーになります。

{
  "node_id": 1,
  "listen": "10.0.0.1:7000",
  "relay_listen": "10.0.0.1:7001",
  "secret_file": "/etc/senca/cluster-secret",
  "tls_ca": "/etc/senca/cluster-ca.pem",
  "tls_cert": "/etc/senca/n1.pem",
  "tls_key": "/etc/senca/n1.key",
  "peers": [
    {"id": 1, "addr": "10.0.0.1:7000", "relay": "10.0.0.1:7001", "api": "https://n1.example.com"},
    {"id": 2, "addr": "10.0.0.2:7000", "relay": "10.0.0.2:7001", "api": "https://n2.example.com"},
    {"id": 3, "addr": "10.0.0.3:7000", "relay": "10.0.0.3:7001", "api": "https://n3.example.com"}
  ]
}
フィールド必須意味
node_idはいこのノードの Raft の番号。1 以上で、peers に同じ id のエントリが必要です
listenはいノード間の通信(Raft、書き込みのリーダーへの転送、ハートビート)の待ち受けアドレス
peersはい投票するノードの一覧(自分を含む)。1〜7 台
peers[].idはいそのノードの Raft の番号
peers[].addrはいそのノードの listen
peers[].apiいいえそのノードの管理 API の URL。307 リダイレクト、ルームの redirect、ルームの REST API の転送に使います(下記)
peers[].relayいいえそのノードの中継の待ち受けアドレス。既定は addr のポート + 1(下の検査を参照)
secret_fileはいクラスターシークレットのファイル。全ノードで同じ内容にします。前後の空白を除いて 16 文字以上必要です
tls_ca・tls_cert・tls_keyいいえノード間の mTLS(双方が証明書を確かめ合う TLS)。3 つすべてを指定するか、すべて省略します(一部だけ指定するとエラーになります)
relay_listenいいえこのノードの中継の待ち受けアドレス。既定は listen のポート + 1(下の検査を参照)

次の場合、ノードは起動しません(終了コード 2 または 1)。

  • node_id が 0 か、peers に自分が含まれていない。または、peers[].id が 0 か、重複している。
  • peers が 0 台か、8 台以上。
  • クラスターシークレットが 16 文字未満。
  • tls_* の一部だけを指定している。
  • アドレスが重複している: いずれかのノードの中継アドレス(peers[].relay、省略時は addr + 1)が、別のノードの Raft のアドレス(peers[].addr)か中継アドレスと同じ。たとえば同じホストで Raft のポートを連番(7000・7001・7002)にすると、 既定の中継ポート(+ 1)が隣のノードの Raft のポートと重なります。その場合は relay_listen と peers[].relay を明示してください。
  • listen または relay_listen で待ち受けできない(ポートが使用中など)。

構成を決める前に、次の制約を確認してください。

すべてのノードが投票者になります。投票しないメディア処理専用のノードは今のところ作れないので、クラスターは最大 7 台です。

投票者の構成は設定ファイルで固定され、稼働中に投票者を追加・削除する API はありません。 新しいクラスターができるのは、すべての投票者が同じ peers で初めて起動したとき(データディレクトリの control/raft/ が空のとき)です。

peers[].api には、利用者の端末からも他のノードからも届く URL を書いてください。この値は初回の起動でノードを登録するときに記録され、 後で設定ファイルを書き換えても変わりません。書かなかった場合、307 リダイレクトも、ルームの別ノードへの案内もできません。

単一ノードで使っていたデータディレクトリをクラスターに流用しても、コントロールプレーンのデータは引き継がれません。 クラスターでは Raft のログだけを使い、単一ノードのときのログ(control/log)は読み込まないためです。

ポート

ポート接続元用途
listen他のノードのみRaft、書き込みの転送、グリッドのハートビート
relay_listen(既定 listen + 1)他のノードのみ中継(エンコード済みのフレームの転送)
--http・--https利用者と他のノード管理 API、HLS、シグナリング。peers[].api の宛先です
--rtmp・--srt・--rtsp・--webrtc-udp など利用者配信と視聴(取り込みと再生)

クラスターシークレットと mTLS

ノード間の接続(Raft と中継)では、接続の直後にクラスターシークレットを使った相互のチャレンジレスポンス認証を行います。 双方が新しい乱数を送り合い、シークレットから作った鍵で HMAC(双方の乱数とノード番号を含む)を計算して、シークレットを知っていることを示します。 シークレットそのものも、毎回同じになる値も送らないので、通信を盗聴して再送しても接続できません。 相手のノード番号が接続先と違うとき、HMAC が一致しないとき、5 秒以内に認証が終わらないときは接続を閉じ、 ログに cluster: rejected connection from ... か relay: connection from ... refused と記録します。

tls_* を設定すると、さらに mTLS を使います。双方が、クラスターの認証局(tls_ca)が発行した証明書かどうかを検証します。 証明書の SAN には senca-cluster(DNS 名)が必要です。チャレンジレスポンス認証は TLS の中でも行い、中継の接続も同じ証明書を使います。 tls_* を設定しなければ平文の TCP になり、複製されるコントロールプレーンのデータも中継のメディアも暗号化されません(セキュリティ)。

バージョンの混在について: チャレンジレスポンス認証を使わない古いバージョンのノード(固定のハッシュを送る方式)とは接続できません。 クラスターのノードは、まとめて更新してください。

証明書とシークレットは、たとえば次のように作ります(P-256。各ノードの証明書には serverAuth と clientAuth の両方を付けます)。

openssl req -x509 -newkey ec -pkeyopt ec_paramgen_curve:P-256 -nodes \
  -keyout ca.key -out ca.pem -days 365 -subj "/CN=senca-cluster-ca"
for n in n1 n2 n3; do
  openssl req -newkey ec -pkeyopt ec_paramgen_curve:P-256 -nodes \
    -keyout $n.key -out $n.csr -subj "/CN=$n"
  printf "subjectAltName=DNS:senca-cluster\nbasicConstraints=CA:FALSE\nextendedKeyUsage=serverAuth,clientAuth\n" > $n.ext
  openssl x509 -req -in $n.csr -CA ca.pem -CAkey ca.key -CAcreateserial \
    -out $n.pem -days 365 -extfile $n.ext
done
openssl rand -hex 32 > cluster-secret

別の認証局の証明書を持つノードは、クラスターシークレットを知っていても接続を拒否されます(ログに mTLS from ... refused)。

起動

senca-node --data-dir /var/lib/senca --node-name n1 \
  --http 0.0.0.0:8080 --public-http-url https://n1.example.com \
  --cluster-config /etc/senca/cluster-n1.json \
  --label zone=a --net-mbps 10000

--node-name で付けた名前は、配置の指定や API でノードを指すときに使えます。ノードの ID(data_dir/node-id に保存される 128 ビットの ID)とは別物です。

起動してリーダーが決まると、各ノードが自分をクラスターに登録します。 再生 URL の署名に使う鍵と最初の管理者トークンを作るのは、Raft の番号が最小のノード(この例では id: 1)だけです。 トークンはそのノードの data_dir/control/bootstrap-admin-token(パーミッション 0600)に書き出されますが、情報は複製されるので、どのノードに対しても使えます。

初期化が済むまで /readyz は 200 になりません。その後も、過半数(またはリーダー)との通信が 4 秒を超えて途絶えている間は 503 を返します(運用)。

シークレットを暗号化するためのクラスター共通の鍵がまだ無いときは、各ノードが自分の鍵を暗号化してクラスターに書き込もうとし、最初に適用されたものが使われます (セキュリティ)。

書き込みと読み込み

API はどのノードに送ってもかまいません。書き込みはリーダーへ転送され、合意の後に全ノードで同じ順序で適用されます。 受け付けたノードは合意の前に手元の状態で試しに適用してみるので、明らかな誤り(400・404・409 など)はすぐに返ります。最終的な結果は、合意後の適用で決まります。

リーダーが決まっていないとき、過半数と通信できないとき、5 秒以内に適用されないときは、503 CAPACITY_UNAVAILABLE(retryable: true) が返ります。 つまり、過半数を失った側では書き込めません。

一方、読み込みはそのノードの手元の状態から返し、リーダーには問い合わせません。過半数を失ったノードでも読めますが、 書き込んだ直後の数 ms は古い値が返ることがあります。

Raft の時間の刻み(tick)は 100 ms、リーダー選挙のタイムアウトは 1〜2 秒(10〜20 tick の乱数)で、1000 件ごとにスナップショットを取ります。 ログは data_dir/control/raft/ に書き込み、ディスクへの書き出し(fsync)が終わってから送信・適用します。書き込めなければ、ノードは終了コード 70 で終了します。

複製されるもの

コントロールプレーンのデータはすべて複製されます。具体的には次のものです。

  • ストリーム、API トークン(ハッシュ)、録画の記録(メタデータ)、予約、Idempotency-Key の記録
  • ノードの登録情報(GET /nodes)、入力・出力
  • シークレット(暗号化した値)と、シークレットの鍵(クラスターシークレットから作った鍵で暗号化したもの)
  • ストリームの所有者、再生 URL の鍵
  • グリッドの状態(ノードの登録、ワークロード、割り当て、配置できない理由)、ルーム、メディアパイプライン

次のものは複製されません。

  • メディアのデータ(ライブのセグメント、録画のファイル)
  • グリッドのハートビート
  • ルームの参加者とイベント(ルームを配置したノードの手元にだけあります)

ストリームの所有権と、どのノードでも視聴できる仕組み

配信者(RTMP・SRT・WHIP など、または pull の入力)はどのノードに接続してもかまいません。 接続を受けたノードは、そのストリームの所有権の取得をクラスター(Raft)に提案します。提案が適用されると新しいフェンシングトークンが発行され、そのノードが所有者になります(SSE の stream.owner)。

他のノードはこの変更を見て、同じストリームの配信者を手元で受けていれば切断します(ログに publisher evicted)。 後から接続した方が優先されるので、同じストリームが 2 つのノードで同時に公開されることはありません。別のノードで配信を受け直せば、所有者はそのノードに移ります。

所有権は、入力が終わると解放されます。配信者が切断して再接続の猶予(--reconnect-grace)が過ぎると、所有者のノードが所有権の解放をクラスターに提案します。 フェンシングトークンが現在のものと同じ場合だけ解放され、ログに released ownership of stream ... と出ます。 それ以降、他のノードの視聴者は止まった所有者へリダイレクトされず、404 になります。

停止と判定されたノードの所有権も解放されます。ノードの停止が記録されると、そのノードが持っていたストリームの所有者の記録が消えます。 ノードが復帰したときにまだ配信を受け続けていれば、そのノードが所有権を取り直します(ログに re-claimed stream ...)。

視聴: 中継によるミラー

所有者でないノードに視聴のリクエストが来ると(署名付きの HLS、RTMP・SRT・RTSP の再生、WHEP、パイプラインの入力)、そのノードは 所有者から中継でエンコード済みのフレームを受信し、手元にストリームのミラーを作って配信します。

同じストリームを何人が視聴しても、ノード間で転送するのはエンコード済みのフレーム 1 本だけで、途中で変換はしません。 ミラーでも、HLS への変換や途中から見始めるための GOP の保持は所有者と同じように動きます。 ただし録画・字幕の作成・所有権の取得は所有者のノードが行い、ミラーは行いません。

作ったばかりのミラーは、最初の再生リストができるまで最大 6 秒待ちます。使われなくなって 30 秒たったミラーは削除されます。 所有権がこのノードへ移ってきたときは、ミラーを止めて実際の配信者に切り替えます。 所有者が別のノードへ移ると、ミラーは新しい所有者から受け直します。このとき HLS の再生リストには不連続の印(discontinuity)が入ります。

307 リダイレクト(予備の経路)

ライブでは、ミラーを作れないか、6 秒以内に再生リストができず手元にも無いとき、所有者のノードの peers[].api の同じパス(/s/...)へ 307 でリダイレクトします。 録画(VOD)のファイルは録画したノードにしか無いので、手元に無ければそのノードへ 307 でリダイレクトします。

リダイレクト先は、登録時に記録した peers[].api です。書いていなければリダイレクトできず、404 などになります。

中継の状態は、GET /api/v1/nodes の自ノード("self": true)の relay で確認できます。 peers[] には相手のノードごとの値(node・rtt_ms・tx_bps・rx_bps・tx_bytes・rx_bytes・tx_aus・rx_aus・overhead_per_au・ upstreams・downstreams・going_away)が、mirrors にはミラーしているストリームの ID の配列が入ります。

グリッド

ノードの登録と容量

各ノードは起動時に、ワークロードに割り当てられる資源をグリッドに登録します。その後も 30 ハートビートごとに見直し、変化があれば登録し直します。

資源既定変更方法
CPU(ミリコア)論理 CPU 数 × 1000 × 90%--capacity-cpu-millis N
メモリ(MB)/proc/meminfo の MemTotal の 85%(読めなければ 4096 MB を基準)--capacity-memory-mb N
帯域(Mbit/s、送信・受信それぞれ)10000--net-mbps N
GPUコーデックワーカー(エンコード・デコードを担当する別のプロセス)が検出したハードウェアのエンコーダー・デコーダー。デバイスごとに 1 つ登録します(ID は nvidia:0・intel-vpl:0・amd-amf:0・vaapi:0 の形式)。同時に使えるセッション数は、取得できればその値、取得できなければエンコード 8・デコード 16 です—
キャプチャデバイスキャプチャワーカー(キャプチャデバイスを扱う別のプロセス)が検出したデバイス—
ラベル--label KEY=VALUE(複数回指定可)。senca.io/name には --node-name の値が自動で入ります--label
  • --label のキーと値は 1〜128 文字です。senca.io/ で始まるキーは予約されています(コマンドラインでは拒否されます)。
  • 設定ファイル(--config)では、net_mbps・labels・capacity_cpu_millis・capacity_memory_mb に同じ内容を書けます。 設定ファイルの labels も --label と同じ規則で検査します。キーは 1〜128 文字、値は 128 文字までで、制御文字は使えません。 senca.io/ で始まるキーと raft_id は予約されています。ラベルは 64 個までです。規則に合わない場合、ノードは起動しません。
  • ノードの役割には media(ワークロードを動かす)と control(投票者)があります。今のところ、すべてのノードが両方の役割を持ちます。
  • 配置は予約量で計算します。 予約量はワークロードごとの見積もり(「ワークロードごとの見積もり」)の合計で、実際の使用量ではありません。 実測の使用量もハートビートで送られ、リーダー(配置を決めるスケジューラー)の GET /api/v1/grid の usage で見られますが、表示用で配置には使いません。 CPU は /proc/stat の差分から求めたミリコア、メモリは /proc/meminfo の MemTotal − MemAvailable、帯域はループバックを除く /proc/net/dev の差分から求めた Mbit/s です。

ハートビートと停止の検出

各ノードは 1 秒ごとにハートビートをリーダーへ送ります。クラスターの接続を使いますが、合意ログには記録しません。 ハートビートには、手元で受信しているストリーム、動かしているワークロード、デバイスの状態が入ります。 リーダーが受け付けるのは、送信元の Raft の番号と登録済みの raft_id が一致するハートビートだけです。

リーダーは 1 秒ごとに確認し、最後のハートビートから 5 秒を超えたノードを停止と判定して合意ログに記録します。復帰したときも記録します(SSE の node.down・node.up)。 停止を記録した次の周期で、配置を計画し直します。ワークロードの追加・削除・変更があったときは、周期を待たずに計画します。 リーダーが交代したときは検出の状態を作り直し、その後 5 秒間は、まだハートビートが届いていないノードも稼働中とみなします。

デバイスの状態は、各ノードが 10 秒ごとに検出し直します。コーデックワーカーを 1 つ新しく起動して対応する機能を読み取り、キャプチャワーカーにはデバイスの一覧を問い合わせます。 登録済みのデバイスが見つからなくなると、ハートビートで異常を伝え、リーダーがデバイスの停止(SSE の device.down)を記録します。再び見つかれば復帰(device.up)です。 異常なデバイス上のワークロードは、別のデバイスへ移るかデグレードします。見つからなくなったデバイスも登録からは消さず、異常の状態のまま残します。 新しく見つかったデバイスは、登録の見直し(30 ハートビートごと)で追加されます。

この自動の検出は、手動の無効化(「運用の操作」の disable)とは別の仕組みです。検出の結果で手動の無効化が解除されることはありません。

ワークロード

グリッドで配置するワークロードは、リソースから自動で作られます(利用者が直接作るものではありません)。ワークロードの ID はリソースの種類と ID からなり、 input:<id>・output:<id>・pipeline:<id>・room:<id> の形式です。

リソースワークロードになる条件種類
入力(pull)enabled: trueingest(protocol: "device" は capture)
出力(push)enabled: trueegress
パイプラインdesired_state: "running"pipeline
ルーム常にroom

配信者が接続してくる push の配信(接続したノードで受信します)と、録画(所有者のノードで録画します)はワークロードになりません。 ルームのメディアパイプラインのエクスポートはルームの見積もりに加えられ、合成はルームと同じノードに固定したパイプラインになります (メディアパイプライン)。

各ノードは、割り当てのうち自分の分だけを動かします。割り当てが別のノードへ移ると割り当ての番号(token)が変わり、元のノードは その変更が適用された時点でワークロードを止めます。

配置の指定(placement)

入力・出力・パイプライン・ルームの作成時と PATCH 時に、placement を指定できます。結果は各リソースの placement_status に表示されます。

{
  "node": "n2",
  "device": "nvidia:0",
  "labels": [
    {"op": "eq", "key": "zone", "value": "a"},
    {"op": "in", "key": "rack", "values": ["r1", "r2"]},
    {"op": "not_in", "key": "tier", "values": ["spot"]}
  ],
  "anti_affinity": "camera-1",
  "pin_policy": "prefer",
  "priority": "high",
  "strategy": "spread",
  "degrade": [
    {"label": "no_av1", "disable": ["e3"]},
    {"label": "single", "disable": ["sc"]}
  ]
}
フィールド既定意味
nodeなし配置先のノードを手動で決めます。ノードの ID か名前(--node-name)を指定し、保存時に ID に変換します。存在しないノードを指定すると 400 になります
deviceなし配置先の GPU を手動で決めます(GPU の ID)。node が無ければ、その GPU を持ついずれかのノードに配置します
labelsなしラベルの条件。すべてを満たすノードだけが配置先になります。最大 16 個
anti_affinityなしグループ名。同じグループのワークロードは、同じノードに配置しません
pin_policypreferピン留め先が停止したときの扱い。prefer は別のノードへ移し、ピン留め先が復帰してクールダウンが過ぎたら戻します。strict は停止したままにします
prioritynormal優先度。low < normal < high < critical の順に高くなります
strategyspreadspread は配置後の空きが多いノードを、binpack は空きが少ないノードを選んで詰めます
degradeなしデグレードレベル(パイプライン専用。他のリソースに指定すると 400)。最大 8 レベル

ラベルの条件は次の形式で書きます。

op形式一致するノード
eq{"op":"eq","key":K,"value":V}ラベル K の値が V
in{"op":"in","key":K,"values":[...]}ラベル K の値が配列のいずれか(K が無いノードは一致しません)
not_in{"op":"not_in","key":K,"values":[...]}ラベル K の値が配列のどれでもない(K が無いノードも一致します)

node・device・anti_affinity・ラベルのキーと値は 1〜128 文字で、制御文字を含めることはできません。 in・not_in の values は 1〜32 個です。違反すると 400 INVALID_CONFIG になります。未知のフィールドも 400 です。

degrade のレベルには、次の決まりがあります。

  • label は 1〜128 文字です。full(レベル 0 の名前)は使えず、重複もできません。
  • disable は空にできません。パイプラインのグラフにある処理ノードの ID だけを指定できます。
  • レベルは積み重なります(レベル 2 では、レベル 1 で外した処理ノードも外します)。外した処理ノードより後ろの処理ノードは、入力がすべて外れれば一緒に外れます。 入力の一部だけが外れる処理ノード(複数の入力を持つ出力先・合成・音声のミックス)は、外れた入力を除いて残ります。 詳しくは デグレード を参照してください。

ピン留め(node・device で置き場所を固定すること)では、ピン留め先(ノード・GPU)が稼働している間、strict でも prefer でもそこにだけ配置します(ドレイン中なら新しくは配置しません)。 違いが出るのは割り当て先が停止したときです。strict は配置できないまま(理由 pin_target_down)になり、prefer は別のノードへ移ります。 prefer で移ったワークロードは、割り当て先が復帰し、移ってから 30 秒(クールダウン)が過ぎると戻ってきます(理由 pin_returned)。

スコア

空いている資源に配置するときは、候補のノードのうちスコアが高いものを選びます(同点の場合は ID の小さいノードです)。スコアは次の合計です。

  • 空きの点数: spread では、配置後の空きの割合(千分率。CPU・メモリ・帯域・GPU のセッションのうち最も小さいもの)です。binpack では、1000 からこの値を引いたものです。
  • データの近さの加点: ワークロードが使うストリーム(出力ではそのストリーム、パイプラインでは source のストリーム)を受信しているノードに、10000 を加えます。

ワークロードごとの見積もり

予約量は、ワークロードの種類ごとの固定の見積もりで計算します。CPU はミリコア、帯域は Mbit/s です。

ワークロードCPUメモリ受信送信その他
入力(pull)15064 MB100—
入力(protocol: "device")50064 MB00キャプチャデバイスを専有します。options.codec が H.264/H.265 なら GPU のエンコードセッションを 1 つ使います
出力10064 MB010元のストリームを受信しているノードを優先します
ルーム(参加者の上限 N)200 + 20N64 + 2N MB2N2N × min(N, 8)動作中のメディアパイプラインごとに CPU + 20N、メモリ + 8N MB
パイプライン下の表の合計下の表の合計1010GPU のセッションはすべて同じデバイスに配置します

ルームの N は settings.max_participants です(指定が無ければ 50。1〜10000 の範囲に収めます)。

パイプラインの見積もりは、基本の CPU 200 ミリコア・メモリ 128 MB に、処理ノードごとの値を加えて求めます。

処理ノードCPUメモリGPU
encode(H.264・H.265)+100+128 MBエンコード +1
encode(Opus)+50+128 MB—
encode(AV1・VP8・VP9 などソフトウェアの映像)+1500+128 MB—
decode(入力の source のコーデックが H.264・H.265)+100+128 MBデコード +1
decode(その他)+400+128 MB—
compose・board・scale・crop+300+64 MB—
audio_mix+50——

たとえば、VP9 をデコードし、AV1 を 2 本エンコードし、そのうち 1 本は scale を通すグラフは、200 + 400 + 1500 × 2 + 300 = 3900 ミリコアです。 scale を外すデグレードレベル(その後ろのエンコードも外れる)では 2100 ミリコアになります。

H.264・H.265 にはソフトウェアの実装がありません(コーデックと特許)。そのため、これらを使う パイプラインは GPU の無いノードには配置できません(理由 no_device)。

デグレード・追い出し・復帰

スケジューラー(配置を決める仕組み)はリーダーで毎秒動きます。同じ状況なら、常に同じ配置を決めます。各周期の処理は次のとおりです。

  1. 現在の割り当ての検査: ノードの停止、デバイスの故障、条件の変更、アンチアフィニティ(同じグループを同じノードに置かない指定)によって条件に合わなくなったものを、再配置の対象にします。条件に合うものは動かしません。

  2. 容量超過の解消: ノードの容量が減って予約量が容量を超えた場合は、優先度の低い順にデグレードレベルを下げます。それでも足りなければ、ワークロードを追い出します(理由 overcommitted)。

  3. 優先度の高い順(critical → high → normal → low)に、再配置の対象と新しいワークロードを配置します。デグレードレベルごとに(デグレードなしの full から順に)、次の順で試します。

    1. 空いている資源に配置する。
    2. より低い優先度のワークロードのデグレードレベルを下げる。
    3. より低い優先度のワークロードを追い出す。
    4. どれもできなければ、自分の次のレベル(デグレード)で同じことを試す。

    デグレード・追い出しの案が複数あるときは、追い出す数、追い出すワークロードの優先度の合計、下げるレベルの数の順に比べて、小さいものを選びます。 同じか高い優先度のワークロードを押しのけることはありません(配置できなければ unschedulable、理由 contended)。

  4. デグレードしているワークロードは、レベルが変わってから 30 秒(クールダウン) が過ぎたら元のレベルへ戻します(理由 capacity_returned)。 試す順は、同じノードの空きを使う、別のノードの空きへ移す、同じノードで低い優先度のワークロードをデグレードさせるか追い出す、の順です。

  5. 必須でない移動: ドレイン中のノードのワークロードと、prefer の割り当て先が復帰したワークロードを移します(1 回の計画で 64 件まで)。

スケジューラーの次の値は固定で、設定では変更できません。復帰のクールダウンは 30 秒、復帰後に求める余裕は 0(余裕を求めない)、 1 回の計画での必須でない移動は 64 件まで、データの近さの加点は 10000 です。

レベルを下げたり追い出したりするのは、高い優先度のワークロードに場所を空けるときと、ノードやデバイスの故障・容量の減少のときだけです。 逆にレベルを上げるのは、クールダウンが過ぎてからです。状態が行ったり来たりするのを防ぐためです。

デグレードしたパイプラインは、そのレベルで外す処理ノードをグラフから除いて起動し直します(ログに pipeline <id>: running degraded (level N, without [...]))。デグレードレベルを持たないワークロード(入力・出力・ルーム)は、 資源が足りなければ配置できないか、追い出されるだけです。

placement_status

{"state": "degraded", "node": "<node-id>", "node_name": "n2", "device": null,
 "level": 1, "level_label": "single", "since": "2026-11-03T10:00:00Z",
 "reason": {"code": "preempted", "by": "pipeline:..."}}
state意味その他のフィールド
runningデグレードなし(full)で動作中node・node_name・device・level・level_label・since・reason
degradedデグレードしたレベルで動作中同上
pending配置を待っているreasons(null)
unschedulableどのノードにも配置できないreasons
not_scheduledワークロードではない(無効な入力・出力、停止したパイプライン)—

reason.code には、placed・no_capacity・node_down(node 付き)・device_down(device 付き)・draining・ constraint_changed・preempted(by 付き)・overcommitted・capacity_returned・pin_returned・pin_target_down があります。

reasons には、配置できない理由ごとに、該当するノードの数と例(最大 8 件)が入ります。形式は [{"reason": "cpu", "nodes": 2, "examples": ["<node-id>", ...]}] です。 理由には、node_down・not_media・draining・pinned_elsewhere・pin_target_down・labels・anti_affinity・ no_device・capture・cpu・memory・net_in・net_out・gpu_encode・gpu_decode・gpu_memory・ contended(単独なら収まるが、同じか高い優先度のワークロードで埋まっている)があります。

運用の操作

すべて node:admin の権限が必要です(tenant:admin には含まれません)。

操作API応答
ノードの一覧・取得GET /api/v1/nodes・GET /api/v1/nodes/{id}200
ドレインPOST /api/v1/nodes/{id}/drain202
ドレインの解除POST /api/v1/nodes/{id}/undrain202
デバイスを手動で無効化POST /api/v1/nodes/{node}/devices/{device}/disable202
デバイスを有効に戻すPOST /api/v1/nodes/{node}/devices/{device}/enable202
グリッドの状態GET /api/v1/grid200

ドレイン

保守の前にノードを空けるときに使います。ドレインしたノードには新しいワークロードを配置せず、配置済みのワークロードを空きのある別のノードへ移します (理由 draining。1 回の計画で 64 件まで)。ただし、そのノードに手動で割り当てたワークロードは移しません。{id} にはノードの ID を指定し、名前は使えません。 ドレインを解除しても、移したワークロードは自動では戻りません(prefer のピン留めだけは戻ります)。

ドレイン中のノードは新しい配信者を拒否します。RTMP は NetStream.Publish.Rejected(説明 this node is draining)、SRT はハンドシェイクの段階(理由 1503 = unavailable)で拒否します。 WHIP の POST /whip/{stream} は、ドレイン中でない別のノードの同じパスへ 307 でリダイレクトし、そうしたノードが無ければ 503 と Retry-After を返します。 すでに接続している配信者と視聴者は、そのまま続けられます。

デバイスの無効化

GPU やキャプチャデバイスを、検出の結果とは関係なく手動で無効にします。無効にしたデバイスのワークロードは、別のデバイスへ移るかデグレードします。 ハートビートや登録の見直しで勝手に有効に戻ることはなく、手動で戻すまで無効のままです。 {node} には ID と名前のどちらも使え、存在しないデバイスを指定すると 404 です。応答は {"node": "<id>", "device": "nvidia:0", "enabled": false} です。

なお、ノードをグリッドから削除する API はありません。停止したままのノードは up: false の状態で残ります。

GET /api/v1/grid

{
  "scheduler": "<node-id> または null",
  "nodes": [{
    "id": "<node-id>", "name": "n1", "up": true, "draining": false,
    "roles": ["control", "media"], "labels": {"senca.io/name": "n1", "zone": "a"},
    "capacity": {"cpu_millis": 7200, "memory_mb": 13000, "net_in_mbps": 10000, "net_out_mbps": 10000},
    "reserved": {"cpu_millis": 350, "memory_mb": 192, "net_in_mbps": 20, "net_out_mbps": 10},
    "gpus": [{"id": "nvidia:0", "vendor": "nvidia", "healthy": true, "enabled": true,
              "encode_sessions": 8, "decode_sessions": 16, "encode_reserved": 1, "decode_reserved": 0}],
    "capture": [{"id": "...", "kind": "...", "healthy": true}],
    "usage": {"cpu_millis": 850, "memory_mb": 2300, "net_in_mbps": 12, "net_out_mbps": 40},
    "last_heartbeat": "2026-11-03T10:00:00Z",
    "workloads": 3
  }],
  "workloads": [{
    "id": "input:...", "kind": "ingest", "priority": "high", "pinned": true,
    "state": "running", "node": "<node-id>", "device": null, "level": 0, "level_label": "full",
    "reason": {"code": "placed"}, "unschedulable": null
  }]
}
  • scheduler は、問い合わせたノードがリーダー(スケジューラー)なら自分の ID、そうでなければ null です。
  • last_heartbeat と usage(最後のハートビートに含まれる実測の使用量)は、リーダーだけが持っています。リーダー以外に問い合わせると null になります。
  • reserved は予約量(割り当てたワークロードの見積もりの合計)です。実際の使用量ではありません。
  • workloads[].state は running・degraded・unschedulable・pending のいずれかです。
  • capture[] の disabled は、手動で無効にしたときだけ表示されます。

SSE のイベント

次のイベントが GET /api/v1/events(API と SDK)に流れます。グリッドとノードのイベントはテナントに属さないので、 見えるのは node:admin を持つトークンだけです。

イベントresource_iddata
node.updatedノードの ID{"draining": bool}(登録やドレインのとき)
node.down・node.upノードの ID{}
device.down・device.upノードの ID{"device": ...}(検出した状態が変わったとき)
device.disabled・device.enabledノードの ID{"device": ...}(手動で操作したとき)
grid.decisions空{"decisions": [...]}(グリッドが配置を決めたとき)
stream.ownerストリームの ID{"node": "<node-id>"}

grid.decisions の各要素は、type で種類を区別します。

type意味フィールド
assign割り当てたworkload・node・device・level・token・reason
move別のノードへ移したassign と同じものに加えて from
degrade・restoreデグレードさせた・元に戻したworkload・level・device・reason
evict追い出したworkload・reason
unschedulable配置できなかったworkload・reasons

例

pull の入力を n3 に配置し、n3 が停止したら別のノードへ移す例です。

curl -X POST -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
  https://n1.example.com/api/v1/streams/$SID/inputs \
  -d '{"protocol":"rtmp_pull","url":"rtmp://src.example.com/live/key",
       "placement":{"node":"n3","pin_policy":"prefer","priority":"high"}}'
curl -H "Authorization: Bearer $TOKEN" https://n1.example.com/api/v1/inputs/$IID | jq .placement_status

zone が a か b のノードに、2 台のカメラの入力を別々のノードへ配置する例です。

{"protocol":"srt_pull","url":"srt://cam1.example.com:9000",
 "placement":{"labels":[{"op":"in","key":"zone","values":["a","b"]}],"anti_affinity":"cameras"}}

資源が足りなければデグレードするパイプラインの例です(placement の部分)。

{"priority":"normal","degrade":[{"label":"single","disable":["sc"]}]}

ノードをメンテナンスに入れる(ドレインし、終わったら解除する)例です。

NID=$(curl -s -H "Authorization: Bearer $TOKEN" https://n1.example.com/api/v1/grid \
  | jq -r '.nodes[] | select(.name=="n2") | .id')
curl -X POST -H "Authorization: Bearer $TOKEN" https://n1.example.com/api/v1/nodes/$NID/drain
curl -X POST -H "Authorization: Bearer $TOKEN" https://n1.example.com/api/v1/nodes/$NID/undrain

GPU を手動で無効にし、有効に戻す例です。

curl -X POST -H "Authorization: Bearer $TOKEN" https://n1.example.com/api/v1/nodes/n2/devices/nvidia:0/disable
curl -X POST -H "Authorization: Bearer $TOKEN" https://n1.example.com/api/v1/nodes/n2/devices/nvidia:0/enable

障害時の動作と所要時間

所要時間は、上で説明した定数(ハートビート 1 秒、停止の判定 5 秒、選挙 1〜2 秒、クールダウン 30 秒)から決まります。

障害動作所要時間の目安
リーダー以外のノードが停止ハートビートが 5 秒を超えて途絶えると node.down になります。次の周期でワークロードを移し(move、理由 node_down)、移動先のノードがワークロードを開始します。strict のピン留めは移しません停止から移動まで約 6〜9 秒(3 台構成での実測)
リーダーが停止選挙の間、書き込みは 503(retryable: true)になります。新しいリーダーは検出の状態を作り直し、5 秒の猶予の後に停止したノードを node.down にします選挙 1〜2 秒 + 猶予 5 秒 + 計画 1 秒程度(定数からの見積もり)
過半数を失う書き込みは、最大 5 秒待ってから 503 になります。読み込みは手元の状態から返します。動作中のワークロードはそのまま動きますが、再配置はされません回復すれば、書き込みはすぐにできるようになります
停止したノードが復帰次の周期で node.up になります。最新のログに追いついた後、自分のワークロードを照合します。prefer のピン留めは、移ってから 30 秒が過ぎていれば戻ります。デグレードしたワークロードは、クールダウンの後に元のレベルへ戻りますnode.up まで約 1 秒、元のレベルへの復帰はクールダウン 30 秒の後
所有者のノードが停止配信が途切れます。配信者がいずれかのノードへ再接続すると、そのノードが新しい所有者になります。ミラーは新しい所有者から受信し直します配信者が再接続するまで
ルームを配置したノードが停止ルームは別のノードへ移ります。参加者は入り直します(どのノードに接続しても、新しいノードへ redirect されます。リアルタイムのルーム)ノードの停止と同じ
ネットワークの分断少数側のノードは、過半数(またはリーダー)との通信が 4 秒を超えて途絶えると、自分から処理を止めます(フェンシング)。pull の入力・push の出力・パイプラインを停止し、/readyz を 503 にします(ログに grid: fenced と出力されます)。多数側はハートビートの途絶(5 秒)の後にワークロードを移すので、同じワークロードが 2 か所で動くことはありません。分断が解消すると、最新の割り当てに従って再開します(別のノードへ移動済みなら再開しません)。ルームと、ノードが直接受信している配信者(RTMP など)は停止しません停止まで約 4〜5 秒、解消後は数秒で再開

関連するページ

この文書の原文は、リポジトリの docs/guide/ja/09-cluster-grid.md にあります。内容は現在の版(v0.0.1)に合わせています。