Files
MatchLiveTv/docs/infrastructure/STREAMING_AUTOSCALE.md
T
eminuxandCursor 559284f0b2 Corregge il provisioning Hetzner: Faraday, cloud-init e default cpx12.
Sistemati path API /v1, AppArmor/auth MediaMTX sul nodo e defaults nbg1 così lo smoke Cloud è ripetibile.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-10 12:20:36 +02:00

373 lines
17 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Autoscale streaming — Proxmox attuale + Hetzner Cloud
**Stato:** fasi 04 implementate sul branch — **solo test lab; nessun deploy produzione**
**Branch:** `feature/streaming-autoscale-hetzner`
**Ultimo aggiornamento:** 2026-08-09 (Fase 4 hardening + runbook)
**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 `RELAY_MAX_CONCURRENT` | **implementata sul branch** |
| **3 — Autoscaler** | Soglie + warm spare (+1), kill-switch `STREAM_AUTOSCALE_ENABLED` | **implementata sul branch** |
| **4 — Hardening** | Drain sicuro, budget, kill-switch Redis/admin, alert overflow, runbook | **implementata sul branch** (solo test lab — **non in prod**) |
| **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
### Fase 2 — dettagli implementati
- Coda Sidekiq dedicata `youtube_relay` (priorità sopra `default`)
- `YoutubeRelayEnsureJob` / `YoutubeRelayStopJob` solo su quella coda
- Stop sticky: non cancella owner da Rails; lo stop gira sullowner o requeue (`:wrong_host`)
- Cap per worker: `RELAY_MAX_CONCURRENT` (default 4) + set Redis `youtube_relay:owned:HOSTNAME`
- Ensure a capacità piena → requeue 5s invece di avviare un secondo ffmpeg locale
### Fase 3 — dettagli implementati
- `Streams::Autoscaler` + job chain `Streams::AutoscalerJob` (come health monitor)
- Kill-switch: `STREAM_AUTOSCALE_ENABLED=1` per attivare
- Scale-out se `free_slots ≤ STREAM_AUTOSCALE_SOFT_FREE_SLOTS`
- Warm spare (+N) se carico/soglia e `spare_ready < WARM_SPARE`
- Scale-in overflow idle da `IDLE_MINUTES` (non tocca home; rispetta warm spare)
- Cap `STREAM_AUTOSCALE_MAX_NODES`; kind `lab|cloud`
- Metriche in admin Nodi stream
### Fase 4 — hardening (implementata sul branch)
> **NON rilasciare in produzione** finché lab + smoke Cloud non sono verdi. Su questo branch restano `STREAM_AUTOSCALE_ENABLED=0` e `STREAM_AUTOSCALE_ALLOW_CLOUD=0`.
- Scale-in **sicuro**: `drain!` → attesa sessioni zero → `decommission!`
- Budget soft: `STREAM_AUTOSCALE_MONTHLY_BUDGET_EUR` + `STREAM_AUTOSCALE_NODE_EUR_PER_HOUR` (stima 24/7); scale-out bloccato se sforerebbe
- Cloud autoscale solo con `STREAM_AUTOSCALE_ALLOW_CLOUD=1` **e** `HCLOUD_TOKEN`
- Kill-switch runtime Redis `streams:autoscaler:kill_switch` (bottoni admin ON/OFF) oltre allENV
- Health check `stream_overflow` → incidente Ops/ntfy su nodi idle orfani, over-budget, capacità al max
- Admin: metriche budget + stato kill-switch
#### Runbook operativo (lab / pre-prod)
| Situazione | Azione |
|------------|--------|
| Dubbio / picco anomalo | Admin → **Kill-switch ON** (o `STREAM_AUTOSCALE_ENABLED=0`) |
| Nodo spillato | Drain dal admin; dopo fine live → Destroy |
| Budget alert Ops | Abbassa `MAX_NODES` / alza budget solo dopo review costi Hetzner |
| Smoke Cloud | Provision **manuale** admin (kind cloud), non autoscaler; verifica WG + RTMP 1935 |
| Scale-out lab OK | Solo dopo: considerare `ENABLED=1` + `KIND=lab` su staging/lab Proxmox |
| Produzione overflow | Solo dopo checklist §12 punto 6 |
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=nbg1
HCLOUD_SERVER_TYPE=cpx12
HCLOUD_IMAGE=debian-12
HCLOUD_SSH_KEY=matchlivetv-stream-hetzner
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
```