ffmpeg fa solo remux copy A/V; le app fissano AAC 48 kHz mono; sync deploy non cancella più garage.prod.toml. Co-authored-by: Cursor <cursoragent@cursor.com>
409 lines
18 KiB
Markdown
409 lines
18 KiB
Markdown
# Piano: overflow relay YouTube su Hetzner Cloud
|
||
|
||
**Stato:** piano / design — non implementato
|
||
**Ultimo aggiornamento:** 2026-07-29
|
||
**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).
|
||
|
||
---
|
||
|
||
## 1. Scenario
|
||
|
||
### Problema di prodotto
|
||
|
||
Match Live TV disaccoppia il telefono da YouTube:
|
||
|
||
```text
|
||
Telefono (rete mobile instabile)
|
||
│ RTMP
|
||
▼
|
||
MediaMTX (sempre attivo, slate / alwaysAvailable)
|
||
│
|
||
├── HLS → sito
|
||
└── YoutubeRelay (ffmpeg) → RTMPS YouTube
|
||
```
|
||
|
||
Se cade solo il tratto telefono→MediaMTX, YouTube resta in onda (slate).
|
||
Se il telefono andasse diretto a YouTube (o a un ingest cloud senza hold), la live pubblica cadrebbe con la rete mobile. Inoltre YouTube non è pensato come “Go Live dall’app mobile” senza vincoli di canale: noi usiamo API + RTMPS dal server.
|
||
|
||
### Problema di capacità
|
||
|
||
Ogni diretta `platform=youtube` avvia un processo `ffmpeg` in Sidekiq (`Streams::YoutubeRelay`: video+audio **copy** / remux).
|
||
Il carico scala con **N processi**, non con gli spettatori HLS.
|
||
|
||
Sul box attuale (~4 CPU / 8 GB, stack completo sullo stesso host):
|
||
|
||
| Live YT contemporanee | Situazione attesa |
|
||
|-----------------------|-------------------|
|
||
| 1–4 | Comodo |
|
||
| 5–8 | Teso (CPU/RAM, latenza job Sidekiq) |
|
||
| ≥10 | Rischioso (relay instabili, watchdog, slate prolungate su YT) |
|
||
|
||
**Obiettivo:** mantenere MediaMTX (e Rails) sul dedicated; quando N supera la soglia comoda, **accendere capacità Hetzner Cloud solo per i relay**, per il tempo del picco, poi spegnerla.
|
||
|
||
### Cosa NON è questo piano
|
||
|
||
- Non sostituisce MediaMTX con Mux/AWS MediaLive.
|
||
- Non sposta l’ingest RTMP del telefono sul cloud (cold start e sticky URL sono incompatibili con “zero nodi a riposo”).
|
||
- Non è autoscaling del monolite Docker Compose intero.
|
||
|
||
---
|
||
|
||
## 2. Principio di progettazione
|
||
|
||
| Componente | Dove resta | Perché |
|
||
|------------|------------|--------|
|
||
| MediaMTX (RTMP ingest + slate + HLS) | **Dedicated sempre acceso** | URL stabile per l’app; continuità YouTube via slate |
|
||
| Rails / Postgres / Redis / Garage / edge | **Dedicated** (o gestiti fissi) | Stato, auth, replay; non sono il collo di bottiglia video live |
|
||
| `YoutubeRelay` (ffmpeg) | **Dedicated fino a soglia + overflow Cloud** | Unico pezzo che moltiplica CPU con N live YT |
|
||
| Job Sidekiq non-video (mail, ops, post-process) | Dedicated (coda separata) | Non devono competere con ffmpeg sui worker overflow |
|
||
|
||
Formula mentale:
|
||
|
||
```text
|
||
capacità_yt ≈ core_dedicati_relay + Σ core_vm_overflow_calde
|
||
```
|
||
|
||
---
|
||
|
||
## 3. Architettura target (overflow)
|
||
|
||
```text
|
||
[ telefoni RTMP ]
|
||
│
|
||
▼
|
||
┌─────────────────────────┐
|
||
│ Dedicated (sempre on) │
|
||
│ MediaMTX · Rails · DB │
|
||
│ Redis · Garage · edge │
|
||
│ Sidekiq "core" │
|
||
│ (+ pochi relay locali) │
|
||
└───────────┬─────────────┘
|
||
│
|
||
intake RTMP/HLS │ (rete privata / tunnel / IP allowlist)
|
||
│
|
||
┌───────────────┼───────────────┐
|
||
▼ ▼ ▼
|
||
[relay-local] [overflow-1] [overflow-N]
|
||
Sidekiq+ffmpeg Hetzner Cloud Hetzner Cloud
|
||
YOUTUBE_RELAY=1 a ore a ore
|
||
│ │ │
|
||
└───────────────┴───────────────┘
|
||
│
|
||
▼
|
||
YouTube RTMPS
|
||
```
|
||
|
||
### Ruolo delle VM overflow
|
||
|
||
Immagine minimale (o Compose snello):
|
||
|
||
- Sidekiq con `YOUTUBE_RELAY_WORKER=1`
|
||
- `ffmpeg` disponibile
|
||
- Connessione a **Redis** del dedicated (coda + lock `youtube_relay:owner:*`)
|
||
- Lettura intake da MediaMTX (RTMP preferito, HLS fallback come oggi)
|
||
- **Niente** Postgres scrittura diretta obbligatoria se i job relay usano già Redis + API; in pratica oggi Rails/Sidekiq legge Postgres → le VM overflow devono raggiungere anche DB **oppure** si isola una coda “relay-only” con worker che riceve payload già risolto (vedi §5.2)
|
||
|
||
Stato attuale del codice: `YoutubeRelay` gira in processo Sidekiq Rails completo (accesso a `StreamSession`, MediaMTX client, Redis PID/owner). Le VM overflow sono quindi **worker Sidekiq Rails**, non demoni ffmpeg nudi — almeno nella fase 1.
|
||
|
||
---
|
||
|
||
## 4. Colli di bottiglia (cosa può rompersi comunque)
|
||
|
||
Anche con overflow infinito di CPU, restano limiti. Ordine di probabilità/impatto:
|
||
|
||
### 4.1 MediaMTX sul dedicated (alto)
|
||
|
||
- N publisher RTMP + N reader (HLS siti + N ffmpeg che rileggono lo stesso path).
|
||
- Ogni relay in più è un **reader** su MediaMTX (RTMP pull o HLS).
|
||
- Sintomi: path `ready` flaky, HLS a scatti sul sito, relay che rientrano in fallback HLS, CPU MediaMTX alta anche con relay scarichi.
|
||
|
||
**Mitigazione futura (fuori da questo overflow):** secondo nodo MediaMTX o sharding path; per ora monitorare `readers`, CPU `mediamtx`, bitrate aggregato.
|
||
|
||
### 4.2 Uplink Internet del dedicated (alto)
|
||
|
||
- Telefoni → dedicated (ingresso RTMP).
|
||
- Dedicated → YouTube **se** i relay restano locali.
|
||
- Con overflow Cloud: il dedicated manda **copia dello stream** verso le VM (pull dalle VM = traffico **uscita** dal dedicated verso Hetzner Cloud), poi le VM mandano a YouTube.
|
||
|
||
Attenzione al verso:
|
||
|
||
| Dove gira il relay | Traffico tipico |
|
||
|--------------------|-----------------|
|
||
| Sul dedicated | In: telefoni. Out: N× verso YouTube |
|
||
| Su Hetzner Cloud | Out dedicated → Cloud (intake). Out Cloud → YouTube |
|
||
|
||
Se l’uplink casa/datacenter verso Internet è stretto, spostare i relay sul Cloud **sposta** il carico out YouTube fuori dal dedicated, ma **aggiunge** out dedicated→Cloud (stesso ordine di grandezza del video).
|
||
Vantaggio reale solo se:
|
||
|
||
- l’uplink del dedicated satura soprattutto per **CPU** (oggi sì), oppure
|
||
- dedicated e Cloud sono in **stessa regione Hetzner con rete privata** (traffico interno economico/veloce) e l’uscita verso YouTube esce dalla rete Hetzner Cloud (migliore peering).
|
||
|
||
**Decisione di rete obbligatoria prima di implementare** (vedi §6).
|
||
|
||
### 4.3 Redis / lock owner (medio)
|
||
|
||
Oggi:
|
||
|
||
- `youtube_relay:pid:%s` — PID locale al worker
|
||
- `youtube_relay:owner:%s` — `HOSTNAME` del worker
|
||
- `running?` considera attivo un owner remoto se TTL > 30s
|
||
|
||
Con più worker:
|
||
|
||
- Due ensure concorrenti possono avviare due ffmpeg (debounce 5s aiuta poco cross-host).
|
||
- `stop` da Rails manda job: deve raggiungere il worker **owner**, non un overflow a caso.
|
||
- Kill del PID funziona solo sulla macchina owner (`/proc`).
|
||
|
||
Serve disciplina di coda / routing (§5.3).
|
||
|
||
### 4.4 Cold start VM (medio)
|
||
|
||
Creare una CPX/CCX Hetzner: ordine di **30–90+ secondi** (API + boot + pull image + Sidekiq ready).
|
||
Se la 15ª live parte e non c’è capacità, la broadcast YouTube resta “in attesa” finché non c’è worker.
|
||
|
||
**Requisito prodotto:** overflow **a caldo** = pool minimo già acceso nei weekend / fasce note, non “da zero al fischio”.
|
||
|
||
### 4.5 YouTube / account / quote (basso–medio, esterno)
|
||
|
||
- Limiti API, token OAuth, ban temporanei, ingest RTMPS rifiutato.
|
||
- Non risolvibili con più CPU; restano nel runbook ops esistente.
|
||
|
||
### 4.6 Post-process replay (basso in live, alto a fine giornata)
|
||
|
||
`UploadFromSession` / thumbnail competono su Sidekiq e disco.
|
||
I worker overflow **non** devono prendere coda `default` di merge se sono pensati solo per relay — altrimenti spegnendo le VM a fine picco uccidete job a metà.
|
||
|
||
### 4.7 Costo e idle (operativo)
|
||
|
||
VM dimenticate accese = bolletta. Serve TTL, scale-in aggressivo, alert “overflow acceso da >Xh con 0 relay”.
|
||
|
||
---
|
||
|
||
## 5. Complessità di implementazione
|
||
|
||
### 5.1 Rete: come le VM leggono MediaMTX
|
||
|
||
Opzioni (sceglierne **una** in fase 0):
|
||
|
||
| Opzione | Pro | Contro |
|
||
|---------|-----|--------|
|
||
| **A. Stessa location Hetzner + private network** (dedicated Robot + Cloud nella stessa rete) | Bassa latenza, traffico interno, modello pulito | Richiede che il dedicated sia (o diventi) in ecosistema Hetzner collegabile |
|
||
| **B. WireGuard/Tailscale** dedicated ↔ Cloud | Funziona anche da Proxmox casa | Ops tunnel, MTU, failover; latenza |
|
||
| **C. Esporre RTMP/HLS intake in lettura** (IP allowlist VM overflow) | Semplice | Superficie attacco; non esporre senza auth/TLS dove possibile |
|
||
| **D. Relay locale che pusha a Cloud** | Dedicated controlla egress | Doppia hop, più pezzi |
|
||
|
||
Raccomandazione di piano: **A se il dedicated è/andrà su Hetzner; altrimenti B**. Evitare C in chiaro su WAN.
|
||
|
||
Variabili da prevedere sulle VM:
|
||
|
||
- `MEDIAMTX_INTERNAL_RTMP_URL` → URL raggiungibile dalle VM (non `rtmp://mediamtx:1935` Docker-locale)
|
||
- `MEDIAMTX_HLS_URL` → idem
|
||
- MediaMTX API (`:9997`) raggiungibile in privato per `intake_available?` / PublisherOnline, **mai** su Internet aperto
|
||
|
||
### 5.2 Accesso Postgres e segreti
|
||
|
||
Worker Sidekiq Rails oggi necessitano:
|
||
|
||
- `DATABASE_URL` (o replica read + job che non scrivono — irrealistico all’inizio)
|
||
- `REDIS_URL`
|
||
- `SECRET_KEY_BASE` / credenziali YouTube via DB
|
||
- stessi env di produzione (ridotti)
|
||
|
||
Complessità: ogni overflow è un **nodo fidato** della rete app.
|
||
Immagine Docker identica a `sidekiq` prod, env da secret manager o file scp/cloud-init, **nessuna** porta Rails pubblica sulle VM.
|
||
|
||
### 5.3 Code Sidekiq e affinità relay
|
||
|
||
Stato oggi: ensure/stop via job + processo locale con owner in Redis.
|
||
|
||
Per multi-host serve esplicitare:
|
||
|
||
1. **Coda dedicata** `youtube_relay` (solo worker con `YOUTUBE_RELAY_WORKER=1`).
|
||
2. **Cap locale**: ogni worker ha `RELAY_MAX_CONCURRENT` (es. cores−1).
|
||
3. **Scheduling**:
|
||
- Fase 1 (manuale): overflow sempre in ascolto sulla coda; Sidekiq distribuisce i job; `ensure_on_worker!` no-op se a cap (re-enqueue o lascia ad altro worker).
|
||
- Fase 2: controller di capacità che alza/abbassa N VM in base a `relay_attivi` e profondità coda.
|
||
4. **Stop**: job `YoutubeRelayStopJob` deve eseguire **solo sull’owner** (Sidekiq unique + check `owner == HOSTNAME`, altrimenti requeue con delay breve).
|
||
|
||
Nota: i PID in Redis non sono globalmente killabili — il modello owner è già abbozzato; va reso robusto cross-host.
|
||
|
||
### 5.4 Scale-out / scale-in (lifecycle VM)
|
||
|
||
```text
|
||
Metriche → Decisione → hcloud CLI/API → cloud-init → Sidekiq ready → in coda
|
||
↘ fallisce → alert ops, non spegnere dedicated
|
||
```
|
||
|
||
**Scale-out trigger (esempi):**
|
||
|
||
- `youtube_relay` depth > 0 per >60s **oppure**
|
||
- `relay_attivi_local >= RELAY_SOFT_CAP` **e** nuove sessioni YT in `connecting|live`
|
||
|
||
**Scale-in trigger:**
|
||
|
||
- `relay_attivi` sulle VM overflow = 0 per >15–30 min **e**
|
||
- fuori dalla finestra “weekend caldo” (opzionale)
|
||
|
||
**Warm pool:** sabato 8:00 → min 1–2 VM già up; domenica 23:00 → min 0.
|
||
|
||
### 5.5 Idempotenza e split-brain
|
||
|
||
Scenario da evitare: dedicated e overflow avviano entrambi ffmpeg sulla stessa `stream_key` → YouTube flappa.
|
||
|
||
Mitigazioni:
|
||
|
||
- Lock Redis con fencing token / TTL heartbeat rinnovato dal processo owner ogni N secondi
|
||
- Watchdog (`YoutubeIngestWatchdogJob`) deve rispettare owner remoto (già parzialmente così via TTL)
|
||
- Un solo writer della broadcast activate
|
||
|
||
### 5.6 Observability
|
||
|
||
Senza metriche l’overflow è cieco. Minimo:
|
||
|
||
| Metrica | Dove |
|
||
|---------|------|
|
||
| N relay ffmpeg vivi (local / per host) | Redis + `pgrep` / heartbeat |
|
||
| CPU MediaMTX, CPU Sidekiq | node exporter / docker stats |
|
||
| Profondità coda `youtube_relay` | Sidekiq API |
|
||
| Bitrate / errori ffmpeg per session | log già in `log/youtube_relay_*.log` |
|
||
| VM overflow accese, € stimati | tag Hetzner + cron report |
|
||
| Latenza intake Cloud (RTT dedicated↔VM) | check periodico |
|
||
|
||
Alert ntfy esistenti: estendere con `relay_overflow_saturated`, `mediamtx_cpu_high`, `overflow_vm_orphan`.
|
||
|
||
### 5.7 Sicurezza
|
||
|
||
- Firewall: VM → solo Redis, Postgres, MediaMTX (porte interne), YouTube egress 443
|
||
- Nessun SSH password; chiave o console Hetzner
|
||
- Stream key YouTube solo in DB/encrypted; non in user-data in chiaro se evitabile
|
||
- Image aggiornata; spegnimento = destroy, non “pausare” dischi dimenticati per mesi
|
||
|
||
---
|
||
|
||
## 6. Decisioni aperte (bloccare prima del codice)
|
||
|
||
| # | Domanda | Impatto |
|
||
|---|---------|---------|
|
||
| D1 | Dedicated resta su LAN casa/Proxmox o migra/affianca Hetzner Robot? | Sceglie rete A vs B (§5.1) |
|
||
| D2 | Soft cap relay sul dedicated (es. 4? 6?) | Quando scatta overflow |
|
||
| D3 | Warm pool fisso weekend vs solo reactive | Cold start vs costo |
|
||
| D4 | Tipo VM (CPX shared vs CCX dedicated CPU) | €/live e jitter AAC |
|
||
| D5 | Stessa immagine `sidekiq` vs worker slim | Tempo boot e superficie |
|
||
| D6 | Chi orchestra le VM? (script cron su dedicated, Terraform, small Go/Ruby service) | Ops ownership |
|
||
|
||
Finché D1 non è chiusa, non stimare bandwitdh né SLA del picco.
|
||
|
||
---
|
||
|
||
## 7. Dimensionamento indicativo
|
||
|
||
Ipotesi: 720p, remux copy A/V — **~0.15–0.4 vCPU** medi per relay (picchi su analyze/reconnect).
|
||
|
||
| Target live YT | Dedicated (relay) | Overflow tipico | Note |
|
||
|----------------|-------------------|-----------------|------|
|
||
| ≤6 | Tutto locale | 0 | Situazione attuale “comoda” se box dedicato ai relay |
|
||
| 10–15 | soft cap 4–6 | 1× VM 4–8 vCPU | Warm consigliato in giornata evento |
|
||
| 20–30 | soft cap 4–6 | 2–4× VM | MediaMTX e uplink diventano critici |
|
||
| 50+ | softire anche ingest | N VM + 2° MediaMTX | Fuori scope overflow-only |
|
||
|
||
Questi numeri vanno **calibati** con un load test (N sessioni fake + ffmpeg contro slate MediaMTX) prima di promettere SLA commerciali.
|
||
|
||
---
|
||
|
||
## 8. Piano a fasi (pronti a reagire, non a over-build)
|
||
|
||
### Fase 0 — Misura e soglie (1–2 giorni ops)
|
||
|
||
- [ ] Dashboard/manuale: N live YT, CPU `sidekiq`, CPU `mediamtx`, uplink
|
||
- [ ] Definire `RELAY_SOFT_CAP` empirico sul dedicated
|
||
- [ ] Chiudere **D1** (rete)
|
||
- [ ] Load test controllato: 8–12 relay solo slate (senza telefoni) per vedere dove rompe
|
||
|
||
**Criterio go/no-go overflow:** saturazione CPU Sidekiq/ffmpeg **prima** di MediaMTX/uplink. Se saturano prima rete o MediaMTX, overflow Cloud non basta.
|
||
|
||
### Fase 1 — Overflow manuale “a caldo” (MVP ops)
|
||
|
||
- [ ] Snapshot/immagine Sidekiq relay-ready su Hetzner Cloud
|
||
- [ ] cloud-init: env, WireGuard o private net, `docker compose up sidekiq`
|
||
- [ ] Runbook: “accendi 1 VM”, verifica `YoutubeRelay` su HOSTNAME nuovo, “spegni”
|
||
- [ ] Soft cap sul dedicated: nuovi ensure preferiscono coda se locali pieni (anche patch minima)
|
||
- [ ] Alert se VM overflow up da >3h con 0 owner keys
|
||
|
||
**Reazione a incident:** operatore umano accende/spegne; nessun autoscaler ancora.
|
||
|
||
### Fase 2 — Routing coda robusto
|
||
|
||
- [ ] Coda `youtube_relay` isolata
|
||
- [ ] Heartbeat owner Redis
|
||
- [ ] Stop/ensure sticky all’owner
|
||
- [ ] Cap per worker + requeue
|
||
|
||
### Fase 3 — Autoscaling controllato
|
||
|
||
- [ ] Job/cron: se saturazione → `hcloud server create` fino a `OVERFLOW_MAX`
|
||
- [ ] Scale-in solo se idle e fuori warm window
|
||
- [ ] Budget mensile + kill switch
|
||
|
||
### Fase 4 — Solo se necessario
|
||
|
||
- Secondo MediaMTX / sharding
|
||
- ~~Audio copy se l’encoder telefono è già YouTube-compatibile (meno CPU)~~ → fatto (app AAC 48k mono + relay `-c:a copy`)
|
||
- Dedicated più grosso come baseline (spesso più economico dell’overflow cronico ogni weekend)
|
||
|
||
---
|
||
|
||
## 9. Runbook di reazione (anche prima dell’autoscaler)
|
||
|
||
### Sintomo: YouTube “in programma” / niente video con molte live
|
||
|
||
1. `docker stats` / CPU Sidekiq sul dedicated
|
||
2. Contare relay: log `[YoutubeRelay] started` vs sessioni `platform=youtube` live
|
||
3. Se CPU ~100% e MediaMTX ok → **accendere overflow** (Fase 1) o abbassare nuove live YT
|
||
4. Se MediaMTX alto / path non ready → overflow **non** aiuta; ridurre reader o spostare HLS, non aggiungere pull Cloud ciechi
|
||
|
||
### Sintomo: live YT che si spezza solo sulle VM Cloud
|
||
|
||
1. RTT e packet loss dedicated↔VM
|
||
2. Verificare che intake usi RTMP interno, non HLS pubblico via Internet
|
||
3. Controllare doppio owner / due ffmpeg sulla stessa key
|
||
|
||
### Sintomo: bolletta Cloud anomala
|
||
|
||
1. Lista server con label `matchlivetv-overflow`
|
||
2. Destroy idle
|
||
3. Abbassare warm pool
|
||
|
||
### Sintomo: job replay morti dopo scale-in
|
||
|
||
1. Verificare che overflow **non** consumi coda `default` / `recordings`
|
||
2. Riprocessare dead set Sidekiq
|
||
|
||
---
|
||
|
||
## 10. Criteri di successo
|
||
|
||
| Criterio | Misura |
|
||
|----------|--------|
|
||
| Picco gestito | ≥15 live YT contemporanee stabili in test |
|
||
| Continuità prodotto | Drop telefono → slate YouTube ancora attiva (invariato) |
|
||
| Costo | Overflow acceso solo in finestre picco; €/h documentato |
|
||
| Ops | Runbook Fase 1 eseguibile in <10 min da operatore |
|
||
| Non regressione | Live solo-sito (`matchlivetv`) e replay invariati |
|
||
|
||
---
|
||
|
||
## 11. Riepilogo esecutivo
|
||
|
||
Il pezzo che manca oltre ~10 live YouTube è **CPU dei relay**, non l’idea MediaMTX.
|
||
Hetzner Cloud è la leva economica giusta se:
|
||
|
||
1. scalate **solo** i worker `YoutubeRelay`,
|
||
2. tenete MediaMTX caldo sul dedicated,
|
||
3. risolvete **rete privata** dedicated↔Cloud,
|
||
4. partite da **warm pool + runbook manuale**, poi automate.
|
||
|
||
Il rischio principale non è “creare una VM”, è **leggere N volte MediaMTX e saturare uplink/MediaMTX** mentre pensate di aver risolto solo la CPU. La Fase 0 (misura) decide se l’overflow è sufficiente o se serve anche più ingest/baseline hardware.
|
||
)
|