# 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. )