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:
@@ -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 | < 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 sull’host 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
|
||||
```
|
||||
Reference in New Issue
Block a user