Come ho reso il porno 20 volte più efficiente con lo streaming video Python
Pubblicato: 2022-03-11Introduzione
Il porno è una grande industria. Non ci sono molti siti su Internet che possono competere con il traffico dei suoi più grandi giocatori.
E destreggiarsi in questo immenso traffico è difficile. Per rendere le cose ancora più difficili, gran parte del contenuto offerto dai siti porno è costituito da flussi video live a bassa latenza piuttosto che da semplici contenuti video statici. Ma nonostante tutte le sfide coinvolte, raramente ho letto degli sviluppatori Python che li affrontano. Così ho deciso di scrivere della mia esperienza sul lavoro.
Qual è il problema?
Alcuni anni fa, stavo lavorando per il 26° (all'epoca) sito web più visitato al mondo, non solo per l'industria del porno: il mondo.
A quel tempo, il sito forniva richieste di streaming di video porno con il protocollo di messaggistica in tempo reale (RTMP). In particolare, ha utilizzato una soluzione Flash Media Server (FMS), realizzata da Adobe, per fornire agli utenti flussi live. Il processo di base era il seguente:
- L'utente richiede l'accesso ad alcuni live streaming
- Il server risponde con una sessione RTMP che riproduce il metraggio desiderato
Per un paio di motivi, FMS non è stata una buona scelta per noi, a cominciare dai suoi costi, che includevano l'acquisto di entrambi:
- Licenze Windows per ogni macchina su cui abbiamo eseguito FMS.
- ~$4.000 Licenze specifiche per FMS, di cui abbiamo dovuto acquistare diverse centinaia (e più ogni giorno) a causa della nostra portata.
Tutte queste tasse hanno cominciato ad accumularsi. E costi a parte, FMS era un prodotto carente, soprattutto nella sua funzionalità (ne parleremo tra poco). Quindi ho deciso di eliminare FMS e scrivere da zero il mio parser Python RTMP.
Alla fine, sono riuscito a rendere il nostro servizio circa 20 volte più efficiente.
Iniziare
C'erano due problemi principali coinvolti: in primo luogo, RTMP e altri protocolli e formati Adobe non erano aperti (cioè pubblicamente disponibili), il che li rendeva difficili da lavorare. Come puoi invertire o analizzare i file in un formato di cui non sai nulla? Fortunatamente, c'erano alcuni tentativi di inversione disponibili nella sfera pubblica (non prodotti da Adobe, ma piuttosto da un gruppo chiamato OS Flash, ora defunto) su cui abbiamo basato il nostro lavoro.
Nota: Adobe ha successivamente rilasciato "specifiche" che non contenevano più informazioni di quelle già divulgate nel wiki e nei documenti di inversione non prodotti da Adobe. Le loro specifiche (di Adobe) erano di una qualità assurdamente bassa e rendevano quasi impossibile utilizzare effettivamente le loro librerie. Inoltre, a volte il protocollo stesso sembrava intenzionalmente fuorviante. Per esempio:
- Hanno usato numeri interi a 29 bit.
- Includevano intestazioni di protocollo con formattazione big endian ovunque, ad eccezione di un campo specifico (ma non contrassegnato), che era little endian.
- Spremevano i dati in meno spazio a scapito della potenza di calcolo durante il trasporto di fotogrammi video da 9k, il che non aveva molto senso, perché guadagnavano bit o byte alla volta, guadagni insignificanti per una tale dimensione di file.
E in secondo luogo: RTMP è altamente orientato alla sessione, il che ha reso praticamente impossibile il multicast di un flusso in entrata. Idealmente, se più utenti volessero guardare lo stesso live streaming, potremmo semplicemente ritrasferire loro i puntatori a una singola sessione in cui viene trasmesso quel flusso (questo sarebbe lo streaming video multicast). Ma con RTMP, dovevamo creare un'istanza completamente nuova del flusso per ogni utente che desiderava l'accesso. Questo è stato uno spreco completo.
La mia soluzione per lo streaming video multicast
Con questo in mente, ho deciso di riconfezionare/analizzare il tipico flusso di risposta in "tag" FLV (dove un "tag" è solo un video, audio o metadati). Questi tag FLV potrebbero viaggiare all'interno dell'RTMP con pochi problemi.
I vantaggi di un tale approccio:
- Abbiamo dovuto riconfezionare uno stream solo una volta (il riconfezionamento è stato un incubo a causa della mancanza di specifiche e stranezze del protocollo descritte sopra).
- Potremmo riutilizzare qualsiasi flusso tra client con pochissimi problemi fornendo loro semplicemente un'intestazione FLV, mentre un puntatore interno ai tag FLV (insieme a una sorta di offset per indicare dove si trovano nello stream) consentiva l'accesso a il contenuto.
Ho iniziato lo sviluppo nel linguaggio che conoscevo meglio all'epoca: C. Col tempo, questa scelta è diventata macchinosa; così ho iniziato a imparare le basi di Python durante il porting del mio codice C. Il processo di sviluppo è accelerato, ma dopo alcune demo mi sono subito imbattuto nel problema dell'esaurimento delle risorse. La gestione dei socket di Python non era pensata per gestire questo tipo di situazioni: in particolare, in Python ci siamo trovati a fare più chiamate di sistema e cambi di contesto per azione, aggiungendo un'enorme quantità di sovraccarico.
Miglioramento delle prestazioni di streaming video: mix di Python, RTMP e C
Dopo aver profilato il codice, ho scelto di spostare le funzioni critiche per le prestazioni in un modulo Python scritto interamente in C. Si trattava di roba di livello piuttosto basso: in particolare, utilizzava il meccanismo epoll del kernel per fornire un ordine di crescita logaritmico .
Nella programmazione socket asincrona ci sono funzionalità che possono fornirti informazioni se un determinato socket è leggibile/scrivibile/pieno di errori. In passato, gli sviluppatori hanno utilizzato la chiamata di sistema select() per ottenere queste informazioni, che si adattano male. Poll() è una versione migliore di select, ma non è comunque eccezionale poiché devi passare un sacco di descrittori di socket ad ogni chiamata.

Epoll è sorprendente in quanto tutto ciò che devi fare è registrare un socket e il sistema ricorderà quel socket distinto, gestendo internamente tutti i dettagli grintosi. Quindi non c'è alcun sovraccarico di passaggio di argomenti con ogni chiamata. Inoltre si ridimensiona molto meglio e restituisce solo i socket che ti interessano, il che è molto meglio che scorrere un elenco di descrittori di socket da 100.000 per vedere se avevano eventi con maschere di bit, cosa che devi fare se usi le altre soluzioni.
Ma per l'aumento delle prestazioni, abbiamo pagato un prezzo: questo approccio ha seguito un modello di progettazione completamente diverso rispetto a prima. L'approccio precedente del sito era (se ricordo bene) un processo monolitico che bloccava la ricezione e l'invio; Stavo sviluppando una soluzione basata sugli eventi, quindi ho dovuto rifattorizzare anche il resto del codice per adattarlo a questo nuovo modello.
In particolare, nel nostro nuovo approccio, avevamo un ciclo principale, che gestiva la ricezione e l'invio come segue:
- I dati ricevuti sono stati passati (come messaggi) al livello RTMP.
- L'RTMP è stato sezionato e sono stati estratti i tag FLV.
- I dati FLV sono stati inviati al livello di buffering e multicasting, che ha organizzato i flussi e riempito i buffer di basso livello del mittente.
- Il mittente ha mantenuto una struct per ogni client, con un indice dell'ultimo inviato, e ha cercato di inviare quanti più dati possibile al client.
Questa era una finestra di dati scorrevole e includeva alcune euristiche per eliminare i frame quando il client era troppo lento per ricevere. Le cose hanno funzionato abbastanza bene.
Problemi a livello di sistema, architetturale e hardware
Ma ci siamo imbattuti in un altro problema: i cambi di contesto del kernel stavano diventando un peso. Di conseguenza, abbiamo scelto di scrivere solo ogni 100 millisecondi, anziché istantaneamente. Ciò ha aggregato i pacchetti più piccoli e ha impedito un'esplosione di cambi di contesto.
Forse un problema più grande risiedeva nel regno delle architetture server: avevamo bisogno di un cluster con capacità di bilanciamento del carico e failover: perdere utenti a causa di malfunzionamenti del server non è divertente. All'inizio, abbiamo adottato un approccio a regista separato, in cui un "direttore" designato avrebbe cercato di creare e distruggere i feed delle emittenti prevedendo la domanda. Questo è fallito in modo spettacolare. In effetti, tutto ciò che abbiamo provato ha fallito sostanzialmente. Alla fine, abbiamo optato per un approccio di forza bruta relativamente alla condivisione casuale delle emittenti tra i nodi del cluster, eguagliando il traffico.
Ha funzionato, ma con uno svantaggio: sebbene il caso generale sia stato gestito abbastanza bene, abbiamo visto prestazioni terribili quando tutti sul sito (o un numero sproporzionato di utenti) hanno guardato una singola emittente. La buona notizia: questo non accade mai al di fuori di una campagna di marketing. Abbiamo implementato un cluster separato per gestire questo scenario, ma in realtà pensavamo che mettere a repentaglio l'esperienza dell'utente pagante per uno sforzo di marketing non avesse senso, in effetti, questo non era davvero uno scenario reale (anche se sarebbe stato bello gestire ogni immaginabile Astuccio).
Conclusione
Alcune statistiche dal risultato finale: il traffico giornaliero sul cluster era di circa 100.000 utenti al picco (60% di carico), ~50.000 in media. Ho gestito due cluster (HUN e US); ognuno di loro ha gestito circa 40 macchine per condividere il carico. La larghezza di banda aggregata dei cluster era di circa 50 Gbps, da cui utilizzavano circa 10 Gbps durante il carico di picco. Alla fine, sono riuscito a spingere facilmente 10 Gbps/macchina; teoricamente 1 , questo numero potrebbe arrivare fino a 30 Gbps/macchina, il che si traduce in circa 300.000 utenti che guardano i flussi contemporaneamente da un server.
Il cluster FMS esistente conteneva più di 200 macchine, che avrebbero potuto essere sostituite dalle mie 15, solo 10 delle quali avrebbero svolto un lavoro reale. Questo ci ha dato all'incirca un miglioramento di 200/10 = 20 volte.
Probabilmente il mio più grande risultato dal progetto di streaming video Python è stato che non dovevo lasciarmi fermare dalla prospettiva di dover imparare un nuovo set di abilità. In particolare, Python, transcodifica e programmazione orientata agli oggetti, erano tutti concetti con i quali avevo un'esperienza molto sub-professionale prima di intraprendere questo progetto video multicast.
Quello, e quello rotolare la tua soluzione può pagare molto.
1 Successivamente, quando abbiamo messo in produzione il codice, abbiamo riscontrato problemi hardware, poiché abbiamo utilizzato server Intel sr2500 meno recenti che non potevano gestire schede Ethernet da 10 Gbit a causa delle loro basse larghezze di banda PCI. Invece, li abbiamo usati in legami Ethernet 1-4x1 Gbit (aggregando le prestazioni di diverse schede di interfaccia di rete in una scheda virtuale). Alla fine, abbiamo ottenuto alcuni dei più recenti Intel sr2600 i7, che hanno servito 10 Gbps sull'ottica senza alcun problema di prestazioni. Tutti i calcoli previsti si riferiscono a questo hardware.