Aggiunge registry nodi stream e provisioning Hetzner/lab per lo scale-out.

Prepara l'architettura multi-nodo (MediaMTX+ffmpeg) con assignment URL per sessione, admin di provision/drain e provider Cloud/DNS astratti verso mltv-stream.net.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-08-09 19:52:47 +02:00
co-authored by Cursor
parent 45bdda7c95
commit c3878cdc6d
48 changed files with 1980 additions and 25 deletions
@@ -1,10 +1,12 @@
# Piano: overflow relay YouTube su Hetzner Cloud
**Stato:** piano / design — non implementato
**Ultimo aggiornamento:** 2026-07-29
> **Superseded (2026-08-09):** il design corrente è [`STREAMING_AUTOSCALE.md`](STREAMING_AUTOSCALE.md) (Auction + Proxmox, scale di MediaMTX **e** ffmpeg, warm spare, secondo dominio DNS, Garage ora / B2 dopo). Questo file resta come riferimento storico sul solo overflow dei relay.
**Stato:** piano / design storico — non implementato come doc standalone
**Ultimo aggiornamento:** 2026-07-29 (header superseded 2026-08-09)
**Contesto:** produzione attuale su host dedicato (es. Proxmox/Hetzner) con MediaMTX + Sidekiq sullo stesso box; obiettivo >10 live YouTube contemporanee senza abbandonare il middle-tier.
Documenti correlati: [`LIVE_STREAMING.md`](../LIVE_STREAMING.md), [`ARCHITECTURE.md`](../ARCHITECTURE.md), [`OPS_MONITORING.md`](../OPS_MONITORING.md).
Documenti correlati: [`STREAMING_AUTOSCALE.md`](STREAMING_AUTOSCALE.md), [`LIVE_STREAMING.md`](../LIVE_STREAMING.md), [`ARCHITECTURE.md`](../ARCHITECTURE.md), [`OPS_MONITORING.md`](../OPS_MONITORING.md).
---
+332
View File
@@ -0,0 +1,332 @@
# Autoscale streaming — Proxmox attuale + Hetzner Cloud
**Stato:** design approvato (decisioni chiuse) — pronto per implementazione
**Branch:** `feature/streaming-autoscale-hetzner`
**Ultimo aggiornamento:** 2026-08-09 (dominio ingest: `mltv-stream.net`)
**Sostituisce / estende:** il piano overflow-only [`HETZNER_CLOUD_RELAY_OVERFLOW.md`](HETZNER_CLOUD_RELAY_OVERFLOW.md). Questo documento copre **MediaMTX + ffmpeg**, warm spare, DNS, storage, disco e lab sul Proxmox di produzione.
Documenti correlati: [`ARCHITECTURE.md`](../ARCHITECTURE.md), [`LIVE_STREAMING.md`](../LIVE_STREAMING.md), [`REPLAY_MODULE.md`](../REPLAY_MODULE.md), [`OPS_MONITORING.md`](../OPS_MONITORING.md).
---
## 1. Obiettivo
Scalare molte dirette contemporanee (sito e/o YouTube) senza saturare un unico box:
- **Control plane (ora):** Proxmox **già in produzione** (`192.168.1.146` / `/opt/matchlivetv`) — sito, API, DB, Redis, Garage, MediaMTX/ffmpeg “home”.
- **Data plane elastico:** **Hetzner Cloud** — nodi `MediaMTX + ffmpeg` con warm spare (+1).
- **DNS ingest:** secondo dominio su **Hetzner DNS** (API).
- **Lab:** stesso Proxmox, con `ProxmoxLabProvider`, prima di affidarsi al Cloud in produzione.
- **Futuro (non bloccante):** migrazione control plane su **Hetzner Auction + Proxmox** (vSwitch nativo, hardware dedicato).
---
## 2. Decisioni chiuse
| # | Decisione | Scelta |
|---|----------|--------|
| D1 | Host primario **ora** | **Proxmox già usato in produzione** (non attendere Auction) |
| D1b | Host primario **futuro** | Hetzner Server Auction + Proxmox (quando si migra) |
| D2 | Overflow / warm spare | **Hetzner Cloud** (attivato per questo progetto) |
| D3 | Unità di scala | Nodo **stream** = MediaMTX + worker `youtube_relay` (ffmpeg remux copy) |
| D4 | Warm pool | Almeno **1 spare ready** quando il carico si avvicina al cap |
| D5 | DNS ingest | Dominio **`mltv-stream.net`** (registrar Aruba, NS → Hetzner DNS). `matchlivetv.it` resta su Aruba intatto |
| D6 | Aruba | Nessuna delega NS; niente Floating IP prenotate |
| D7a | Rete **ora** (casa/Proxmox ↔ Cloud) | **WireGuard** (o Tailscale) privato: Redis, DB, API MediaMTX home. Non esporre Redis/Postgres su WAN |
| D7b | Rete **futuro** (Auction ↔ Cloud) | vSwitch Robot + Cloud Network (stessa location) |
| D8 | Object storage | **Garage resta**. **B2** = migrazione pronta quando misurato |
| D9 | Staging | Segmenti su disco VM `stream-*` in live; upload Garage a fine sessione |
| D10 | Prodotto | Consigliare **YouTube** per alleggerire storage/egress replay |
| D11 | Disco | Separazione volumi + **monitoraggio attivo** obbligatorio (§6) |
| D12 | Lab prima del Cloud prod | `ProxmoxLabProvider` + DNS lab sullo stesso Proxmox; poi `HetznerCloudProvider` |
---
## 3. Topologia — fase attuale (Proxmox prod + Cloud)
```text
Internet
│ HTTPS :443 · RTMP :1935
┌──────────────────────────────────────────────────────────┐
│ Proxmox produzione (attuale) │
│ eminux@192.168.1.146 · /opt/matchlivetv │
│ │
│ edge · rails · sidekiq-core · postgres · redis │
│ garage · stream-home (MediaMTX + youtube_relay) │
│ autoscaler (Rails/Sidekiq) │
│ │
│ Lab (opz.): clone VM stream-* via ProxmoxLabProvider │
└────────────────────────┬─────────────────────────────────┘
│ WireGuard / tunnel privato
┌──────────────────────────────────────────────────────────┐
│ Hetzner Cloud — pool stream nodes │
│ stream-01 … stream-N MediaMTX + ffmpeg │
│ + 1 spare ready (warm) │
└────────────────────────┬─────────────────────────────────┘
Hetzner DNS (secondo dominio)
ingest-XX.<dominio> → IP pubblico nodo
```
**Futuro Auction:** stesso schema, tunnel sostituito da vSwitch; control plane migrato sul bare metal Hetzner.
**Principio:** Rails solo controllo. Live sui MediaMTX del nodo assegnato. Replay su Garage (poi eventualmente B2).
---
## 4. DNS (secondo dominio)
RTMP **non** tollera round-robin su un unico hostname.
1. Dominio **`mltv-stream.net`** registrato su Aruba; zona DNS su Hetzner Console (progetto `matchlivetv-stream`).
2. NS Aruba del dominio → `hydrogen` / `oxygen` / `helium` Hetzner (delegazione OK).
3. Autoscaler a VM ready → upsert `A` `ingest-03.mltv-stream.net` → health-check → nodo `ready`.
4. API create/start sessione → URL del nodo assegnato:
```text
rtmp://ingest-03.mltv-stream.net:1935/live/match_<uuid>
https://ingest-03.mltv-stream.net/hls/... # o via edge matchlivetv.it che proxya il nodo
```
5. Scale-in: drain → delete DNS → destroy VM.
Hostname pubblici dei nodi vivono sotto `*.mltv-stream.net`. In lab: `LabDnsProvider` (`/etc/hosts`, dnsmasq).
---
## 5. Autoscaler e warm spare
Ogni nodo: `max_publishers` / `max_relays` (es. 4).
| Regola | Comportamento |
|--------|----------------|
| Scale-out | `free_slots` sotto soglia soft → crea 1 nodo |
| Warm spare | Se `spare_ready < 1` vicino al carico → crea spare |
| Scale-in | Idle da X min e non unica spare → destroy |
| Floor | `stream-home` sul Proxmox sempre on |
`CloudProvider`: `HetznerCloudProvider` (prod overflow) + `ProxmoxLabProvider` (test).
Code Sidekiq: coda `youtube_relay` isolata; sticky owner Redis.
---
## 6. Disco Proxmox — requisito critico
**Non** mettere OS, DB e video sullo stesso volume che può riempirsi al 100%.
Vale **già** sul Proxmox di produzione (rinforzare ora), e di nuovo alla migrazione Auction.
### 6.1 Layout volumi (target)
| Volume / disco | Contenuto | Note |
|----------------|-----------|------|
| `os` | Proxmox / sistema host | Quasi statico |
| `vm-system` / root stack | rails, edge, redis, … | Snapshot |
| `vm-data-db` | Postgres | Separato; backup prioritari |
| `vm-data-video` | Garage + staging MediaMTX | **Disco che può riempirsi** |
| `backup` | Backup / PBS | **Mai** sullo stesso disco dei video |
Regole: cap Garage/staging; se staging pieno → **rifiutare nuove sessioni**, non far cadere DB/API.
### 6.2 Monitoraggio attivo (obbligatorio)
| Metrica | Warn | Critical |
|---------|------|----------|
| Uso `%` volume video | ≥ 70% | ≥ 85% |
| Spazio libero GB | sotto soglia | &lt; X GB |
| Inode liberi | bassi | esaurimento |
| Crescita GB/giorno Garage | anomalia | — |
| Purge/retention falliti | — | immediato |
| Staging per nodo `stream-*` | ≥ 70% | ≥ 85% + blocco live su quel nodo |
Checklist:
- [ ] Alert ntfy disco testati
- [ ] Vista ops “disco video”
- [ ] Test periodico “80% pieno”
- [ ] Restore di prova Postgres
---
## 7. Storage: Garage ora, B2 dopo
| Ora | **Garage** |
| Dopo | **Backblaze B2** quando metriche/€ lo giustificano |
Client S3-ready; staging locale invariato. Spinta prodotto YouTube riduce pressione replay.
---
## 8. Prodotto: YouTube consigliato
Suggerire YouTube in app/dashboard quando ha senso → meno storage/egress MatchLiveTV; middle-tier (slate + relay) resta il valore. Live solo-sito restano supportate.
---
## 9. Astrazioni codice
```text
CloudProvider
create_node / destroy_node / list_nodes / wait_until_running / public_ip
→ HetznerCloudProvider | ProxmoxLabProvider
DnsProvider
upsert_a / delete_a
→ HetznerDnsProvider | LabDnsProvider
StreamNodeRegistry
register / mark_ready / allocate_for_session / drain / release
Autoscaler
reconcile!(metrics) → ensure spare / scale-in idle
```
Tag VM Cloud: `matchlivetv`, `role=stream-node`, `env=prod|lab`.
---
## 10. Lab sul Proxmox attuale
Prima (o in parallelo) al Cloud “vero”:
1. Template VM `stream-node` su Proxmox.
2. `CLOUD_PROVIDER=proxmox_lab` → clone/start/stop via API Proxmox.
3. DNS lab (hosts/dnsmasq).
4. Soft cap basso; N sessioni di test; verifica assignment, spare, scale-in, alert disco.
5. Poi smoke su Hetzner Cloud (1 CX piccolo) + DNS reale.
Cosa il lab **non** replica al 100%: tempi boot Hetzner, vSwitch, RTMP 4G multi-nodo (smoke WAN dopo).
---
## 11. Fasi di implementazione
| Fase | Cosa | Esito |
|------|------|--------|
| **L — Lab Proxmox** | `LocalLab` / `ProxmoxLab`, DNS lab, admin nodi | **implementata sul branch** |
| **0 — Multi-nodo ready** | Registry + URL RTMP/HLS in API (`StreamNode`, `Streams::NodeRegistry`) | **implementata sul branch** |
| **1 — Hetzner Cloud + DNS** | `HetznerCloudProvider` + `HetznerDnsProvider`, `mltv-stream.net`, cloud-init, WireGuard (§16) | **implementata sul branch** (WG ops manuale) |
| **2 — Routing relay** | Coda `youtube_relay`, sticky owner, cap | Niente doppi ffmpeg |
| **3 — Autoscaler** | Soglie + warm spare (+1) | Automatico |
| **4 — Hardening** | Drain, budget, runbook, kill-switch | Picchi weekend |
| **A — Auction (futuro)** | Migrazione control plane + vSwitch al posto di WireGuard | Hardware dedicato Hetzner |
| **B2 (opz.)** | Switch object storage | Quando misurato |
### Fase 0 — dettagli implementati
- Tabella `stream_nodes` + `stream_sessions.stream_node_id`
- `Streams::NodeRegistry.ensure_home_from_env!` / `allocate!` (least-loaded)
- `Sessions::Create` assegna il nodo e crea il path MediaMTX sul client del nodo
- URL RTMP/HLS/API/internal per sessione derivati dal nodo (fallback ENV se nodo assente)
- API JSON espone `stream_node` (slug)
- Dominio ingest pubblico futuro: `*.mltv-stream.net`
### Fase L — dettagli implementati
- `Streams::CloudProviders` (`local_lab`, `proxmox_lab`, stub `hetzner`)
- `Streams::DnsProviders` (`lab` su Redis, stub `hetzner`)
- `Streams::NodeProvisioner` (provision / drain / decommission)
- Admin **Nodi stream** (`/admin/stream_nodes`): lista, provision lab, drain, delete
- Rake: `streams:nodes:ensure_home`, `streams:nodes:provision_lab`, `streams:nodes:dns_lab_dump`
- Default sicuro: `STREAM_CLOUD_PROVIDER=local_lab` (nessuna chiamata Proxmox finché non configuri token)
### Fase 1 — dettagli implementati
- `Streams::CloudProviders::Hetzner` (create/destroy server, wait running, labels)
- `Streams::DnsProviders::Hetzner` (upsert/delete A su zona `mltv-stream.net`)
- `NodeProvisioner#provision_cloud!` + bottone admin **Provisiona nodo Hetzner**
- Cloud-init: `infra/stream-node/cloud-init.yaml` (`HCLOUD_USER_DATA_FILE`)
- Decommission usa il provider del nodo (non solo ENV globale)
- WireGuard: vedi §16
Ordine di lavoro consigliato sul branch: **0 → L → 1 → 2 → 3 → 4**; **A** quando si decide di lasciare il Proxmox casa.
---
## 12. Ordine operativo “ora” (senza Auction)
1. Rinforzare dischi/alert sul Proxmox prod (§6).
2. Secondo dominio + zona Hetzner DNS.
3. Progetto Hetzner Cloud + immagine/snapshot `stream-node`.
4. Tunnel WireGuard Proxmox ↔ Cloud (Redis/DB/API private).
5. Implementare fasi 0 → L → 1… sul branch.
6. Cutover overflow in produzione solo dopo lab + smoke Cloud verdi.
---
## 13. Criteri di successo
| Criterio | Misura |
|----------|--------|
| Lab | Scale-out/in e assignment verificati su Proxmox senza Hetzner |
| Picco | N live oltre soft cap home con spare Cloud ready |
| Continuità | Drop telefono → slate YT attiva |
| Disco | Nessun outage DB/API per disco video; alert prima del critical |
| DNS | Aruba intatto; ingest sul secondo dominio |
| Futuro | Path chiaro verso Auction senza riscrivere autoscaler |
---
## 14. Aperto / da calibrare
| Voce | Note |
|------|------|
| Nome secondo dominio | **`mltv-stream.net`** (chiuso) |
| Soft cap `stream-home` | es. 4 vs 6 |
| Tipo VM Cloud | CPX vs CCX |
| `OVERFLOW_MAX` / budget | — |
| Spare di notte | 0 vs 1 |
| Dettaglio WireGuard | subnet, peer, MTU |
| Soglie GB disco | da size reale volume video |
---
## 15. Riepilogo esecutivo
**Ora:** restiamo sul Proxmox di produzione; attiviamo Hetzner Cloud (spare MediaMTX+ffmpeg) + Hetzner DNS (secondo dominio) + WireGuard. Testiamo prima in lab sullo stesso Proxmox. Garage resta; B2 dopo. YouTube consigliato in prodotto. Disco e alert sono vincolo non negoziabile.
**Dopo:** eventuale Auction Hetzner sostituisce il control plane casa e WireGuard → vSwitch, senza cambiare il modello nodi/autoscaler.
---
## 16. WireGuard (Proxmox casa ↔ Hetzner Cloud)
Obiettivo: Rails/Sidekiq sullhost di produzione raggiungono `api_base_url` / `internal_rtmp_url` dei nodi Cloud (es. `http://10.0.0.9:9997`) **senza** esporre Redis/Postgres/MediaMTX API su Internet.
### Setup consigliato (una tantum)
1. Su Hetzner Console: crea **Network** privata (es. `10.0.0.0/16`) nella location `fsn1`, subnet `10.0.1.0/24` per i server Cloud.
2. Imposta `HCLOUD_NETWORK_ID=<id>` così i nodi stream entrano nella network al create.
3. Su Proxmox (VM o LXC gateway): installa WireGuard; peer verso una VM “gateway” Cloud **sempre on** (CX22 piccola) *oppure* peer site-to-site verso un server Cloud fisso.
4. Alternativa più semplice per i primi test: **Tailscale** su Proxmox host + su ogni stream-node (cloud-init) — stesso effetto, meno ops.
5. Firewall Hetzner Cloud:
- WAN: `1935/tcp` (RTMP), `443/tcp` o `8888` se HLS diretto
- Solo rete privata / WG: `9997` (MediaMTX API), niente Postgres/Redis sui nodi stream
6. Verifica da Rails: `curl http://<private_ip>:9997/v3/paths/list`
Finché WireGuard/Tailscale non è pronto, `api_base_url` punta comunque alla private IP (o pubblica se manca private_net): il path MediaMTX da Create fallirà dal control plane se non raggiungibile — provisionare nodi Cloud solo dopo connettività privata OK, oppure smoke con API su IP pubblico temporaneo + firewall allowlist IP casa.
### ENV produzione (stream autoscale)
```bash
HCLOUD_TOKEN=...
HCLOUD_LOCATION=fsn1
HCLOUD_SERVER_TYPE=cpx21
HCLOUD_IMAGE=debian-12
HCLOUD_SSH_KEY=matchlivetv-stream
HCLOUD_NETWORK_ID= # opzionale
HCLOUD_USER_DATA_FILE=/opt/matchlivetv/infra/stream-node/cloud-init.yaml
STREAM_DNS_ZONE=mltv-stream.net
STREAM_DNS_TTL=60
STREAM_CLOUD_DNS_SUFFIX=mltv-stream.net
STREAM_CLOUD_MAX_PUBLISHERS=4
STREAM_NODE_ENV=prod
# Default lab sicuro; in prod per overflow usa hetzner esplicitamente dal bottone admin
STREAM_CLOUD_PROVIDER=local_lab
STREAM_DNS_PROVIDER=lab
```