複数台で動かす
このページの見出し
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: true | ingest(protocol: "device" は capture) |
| 出力(push) | enabled: true | egress |
| パイプライン | 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_policy | prefer | ピン留め先が停止したときの扱い。prefer は別のノードへ移し、ピン留め先が復帰してクールダウンが過ぎたら戻します。strict は停止したままにします |
priority | normal | 優先度。low < normal < high < critical の順に高くなります |
strategy | spread | spread は配置後の空きが多いノードを、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) | 150 | 64 MB | 10 | 0 | — |
入力(protocol: "device") | 500 | 64 MB | 0 | 0 | キャプチャデバイスを専有します。options.codec が H.264/H.265 なら GPU のエンコードセッションを 1 つ使います |
| 出力 | 100 | 64 MB | 0 | 10 | 元のストリームを受信しているノードを優先します |
| ルーム(参加者の上限 N) | 200 + 20N | 64 + 2N MB | 2N | 2N × min(N, 8) | 動作中のメディアパイプラインごとに CPU + 20N、メモリ + 8N MB |
| パイプライン | 下の表の合計 | 下の表の合計 | 10 | 10 | GPU のセッションはすべて同じデバイスに配置します |
ルームの 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)。
デグレード・追い出し・復帰
スケジューラー(配置を決める仕組み)はリーダーで毎秒動きます。同じ状況なら、常に同じ配置を決めます。各周期の処理は次のとおりです。
-
現在の割り当ての検査: ノードの停止、デバイスの故障、条件の変更、アンチアフィニティ(同じグループを同じノードに置かない指定)によって条件に合わなくなったものを、再配置の対象にします。条件に合うものは動かしません。
-
容量超過の解消: ノードの容量が減って予約量が容量を超えた場合は、優先度の低い順にデグレードレベルを下げます。それでも足りなければ、ワークロードを追い出します(理由
overcommitted)。 -
優先度の高い順(critical → high → normal → low)に、再配置の対象と新しいワークロードを配置します。デグレードレベルごとに(デグレードなしの full から順に)、次の順で試します。
- 空いている資源に配置する。
- より低い優先度のワークロードのデグレードレベルを下げる。
- より低い優先度のワークロードを追い出す。
- どれもできなければ、自分の次のレベル(デグレード)で同じことを試す。
デグレード・追い出しの案が複数あるときは、追い出す数、追い出すワークロードの優先度の合計、下げるレベルの数の順に比べて、小さいものを選びます。 同じか高い優先度のワークロードを押しのけることはありません(配置できなければ
unschedulable、理由contended)。 -
デグレードしているワークロードは、レベルが変わってから 30 秒(クールダウン) が過ぎたら元のレベルへ戻します(理由
capacity_returned)。 試す順は、同じノードの空きを使う、別のノードの空きへ移す、同じノードで低い優先度のワークロードをデグレードさせるか追い出す、の順です。 -
必須でない移動: ドレイン中のノードのワークロードと、
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}/drain | 202 |
| ドレインの解除 | POST /api/v1/nodes/{id}/undrain | 202 |
| デバイスを手動で無効化 | POST /api/v1/nodes/{node}/devices/{device}/disable | 202 |
| デバイスを有効に戻す | POST /api/v1/nodes/{node}/devices/{device}/enable | 202 |
| グリッドの状態 | GET /api/v1/grid | 200 |
ドレイン
保守の前にノードを空けるときに使います。ドレインしたノードには新しいワークロードを配置せず、配置済みのワークロードを空きのある別のノードへ移します
(理由 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_id | data |
|---|---|---|
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)に合わせています。