Cum am făcut pornografia de 20 de ori mai eficientă cu Python Video Streaming
Publicat: 2022-03-11Introducere
Pornografia este o mare industrie. Nu există multe site-uri pe Internet care să rivalizeze cu traficul celor mai mari jucători ai săi.
Și jonglarea cu acest trafic imens este dificilă. Pentru a face lucrurile și mai dificile, o mare parte din conținutul difuzat de site-urile porno este alcătuit din fluxuri video live cu latență scăzută, mai degrabă decât conținut video static simplu. Dar pentru toate provocările implicate, rareori am citit despre dezvoltatorii Python care le acceptă. Așa că am decis să scriu despre propria mea experiență la locul de muncă.
Care este problema?
În urmă cu câțiva ani, lucram pentru al 26-lea (la acea vreme) cel mai vizitat site web din lume, nu doar pentru industria porno: lumea.
La acea vreme, site-ul a oferit cereri de streaming video porno cu protocolul de mesagerie în timp real (RTMP). Mai precis, a folosit o soluție Flash Media Server (FMS), construită de Adobe, pentru a oferi utilizatorilor fluxuri live. Procesul de bază a fost următorul:
- Utilizatorul solicită acces la un stream live
- Serverul răspunde cu o sesiune RTMP redând filmarea dorită
Din câteva motive, FMS nu a fost o alegere bună pentru noi, începând cu costurile sale, care au inclus achiziționarea ambelor:
- Licențe Windows pentru fiecare mașină pe care am rulat FMS.
- ~4.000 USD licențe specifice FMS, dintre care a trebuit să achiziționăm câteva sute (și mai multe în fiecare zi) din cauza dimensiunii noastre.
Toate aceste taxe au început să crească. Și costuri deoparte, FMS a fost un produs lipsit, mai ales în funcție de funcționalitate (mai multe despre asta într-un pic). Așa că am decis să renunț la FMS și să scriu propriul meu parser Python RTMP de la zero.
În cele din urmă, am reușit să fac serviciul nostru de aproximativ 20 de ori mai eficient.
Noțiuni de bază
Au fost două probleme de bază implicate: în primul rând, RTMP și alte protocoale și formate Adobe nu erau deschise (adică, disponibile public), ceea ce le făcea greu de lucrat. Cum puteți inversa sau analiza fișiere într-un format despre care nu știți nimic? Din fericire, au existat unele eforturi de inversare disponibile în sfera publică (nu produse de Adobe, ci mai degrabă de un grup numit OS Flash, acum dispărut) pe care ne-am bazat munca.
Notă: Adobe a lansat ulterior „specificații” care nu conțineau mai multe informații decât ceea ce era deja dezvăluit în wiki-ul și documentele inverse neproduse de Adobe. Specificațiile lor (de la Adobe) erau de o calitate absurd de scăzută și făceau aproape imposibilă utilizarea efectivă a bibliotecilor lor. Mai mult decât atât, protocolul în sine părea uneori înșelător în mod intenționat. De exemplu:
- Au folosit numere întregi de 29 de biți.
- Au inclus antete de protocol cu formatare big endian peste tot - cu excepția unui câmp specific (încă nemarcat), care era little endian.
- Au strâns datele într-un spațiu mai puțin cu prețul puterii de calcul atunci când transportau cadre video de 9k, ceea ce avea puțin sau deloc sens, deoarece câștigau biți sau octeți la un moment dat - câștiguri nesemnificative pentru o astfel de dimensiune a fișierului.
Și în al doilea rând: RTMP este foarte orientat spre sesiune, ceea ce a făcut practic imposibilă difuzarea multiplă a unui flux de intrare. În mod ideal, dacă mai mulți utilizatori ar dori să vizioneze același stream live, le-am putea transmite indicii înapoi către o singură sesiune în care acel stream este difuzat (aceasta ar fi streaming video multicast). Dar cu RTMP, a trebuit să creăm o instanță complet nouă a fluxului pentru fiecare utilizator care dorea acces. Aceasta a fost o risipă completă.
Soluția mea de streaming video multicast
Având în vedere asta, am decis să reambalez/parsez fluxul de răspuns tipic în „etichete” FLV (unde o „etichetă” este doar niște date video, audio sau meta). Aceste etichete FLV ar putea călători în RTMP fără probleme.
Beneficiile unei astfel de abordări:
- Aveam nevoie să reambalăm un flux o singură dată (reambalarea a fost un coșmar din cauza lipsei de specificații și a ciudateniilor de protocol prezentate mai sus).
- Am putea reutiliza orice flux între clienți cu foarte puține probleme, furnizându-le pur și simplu un antet FLV, în timp ce un pointer intern către etichetele FLV (împreună cu un fel de offset pentru a indica unde se află în flux) le permitea accesul la continutul.
Am început dezvoltarea în limbajul pe care îl cunoșteam cel mai bine la acea vreme: C. Cu timpul, această alegere a devenit greoaie; așa că am început să învăț elementele de bază ale Python în timp ce portam codul meu C. Procesul de dezvoltare s-a accelerat, dar după câteva demonstrații, m-am lovit rapid de problema epuizării resurselor. Gestionarea socket-ului Python nu a fost menită să gestioneze aceste tipuri de situații: în special, în Python ne-am trezit efectuând mai multe apeluri de sistem și comutări de context per acțiune, adăugând o cantitate uriașă de supraîncărcare.
Îmbunătățirea performanței de streaming video: amestecarea Python, RTMP și C
După profilarea codului, am ales să mut funcțiile critice pentru performanță într-un modul Python scris în întregime în C. Acestea erau lucruri de nivel destul de scăzut: în special, a folosit mecanismul epoll al nucleului pentru a oferi o ordine de creștere logaritmică. .
În programarea socket-urilor asincrone, există facilități care vă pot oferi informații dacă un anumit socket este citibil/inscriptibil/umplut cu erori. În trecut, dezvoltatorii au folosit apelul de sistem select() pentru a obține aceste informații, care se scalează prost. Poll() este o versiune mai bună a select, dar încă nu este atât de grozav, deoarece trebuie să treceți o grămadă de descriptori de socket la fiecare apel.

Epoll este uimitor, deoarece tot ce trebuie să faceți este să înregistrați o priză, iar sistemul își va aminti acea priză distinctă, gestionând toate detaliile grele în interior. Deci, nu există nicio trecere de argumente cu fiecare apel. De asemenea, se scalează mult mai bine și returnează doar socket-urile la care îți pasă, ceea ce este mult mai bine decât trecerea printr-o listă de 100.000 descriptori de socket pentru a vedea dacă au avut evenimente cu bitmasks - ceea ce trebuie să faci dacă folosești celelalte soluții.
Dar pentru creșterea performanței, am plătit un preț: această abordare a urmat un model de design complet diferit de cel de până acum. Abordarea anterioară a site-ului a fost (dacă îmi amintesc bine) un proces monolitic care bloca primirea și trimiterea; Dezvoltam o soluție bazată pe evenimente, așa că a trebuit să refactorizez și restul codului pentru a se potrivi cu acest nou model.
Mai exact, în noua noastră abordare, am avut o buclă principală, care gestiona primirea și trimiterea după cum urmează:
- Datele primite au fost transmise (ca mesaje) la nivelul RTMP.
- RTMP a fost disecat și au fost extrase etichetele FLV.
- Datele FLV au fost trimise la stratul de buffering și multicasting, care a organizat fluxurile și a umplut bufferele de nivel scăzut ale expeditorului.
- Expeditorul a păstrat o structură pentru fiecare client, cu un index de ultimul trimis și a încercat să trimită cât mai multe date posibil către client.
Aceasta a fost o fereastră continuă de date și a inclus câteva euristici pentru a elimina cadre atunci când clientul a fost prea lent pentru a primi. Lucrurile au funcționat destul de bine.
Probleme la nivel de sisteme, arhitecturale și hardware
Dar ne-am confruntat cu o altă problemă: schimbările de context ale nucleului deveneau o povară. Ca rezultat, am ales să scriem doar la fiecare 100 de milisecunde, mai degrabă decât instantaneu. Acest lucru a agregat pachetele mai mici și a prevenit o explozie de comutare de context.
Poate că o problemă mai mare se afla în domeniul arhitecturilor de server: aveam nevoie de un cluster capabil de echilibrare a încărcăturii și failover - pierderea utilizatorilor din cauza defecțiunilor serverului nu este distractiv. La început, am optat pentru o abordare cu un director separat, în care un „director” desemnat ar încerca să creeze și să distrugă fluxurile de radiodifuzori prin prognoza cererii. Acest lucru a eșuat spectaculos. De fapt, tot ce am încercat a eșuat destul de mult. În cele din urmă, am optat pentru o abordare relativ brută, de partajare aleatorie a radiodifuzorilor între nodurile clusterului, egalând traficul.
Acest lucru a funcționat, dar cu un dezavantaj: deși cazul general a fost tratat destul de bine, am văzut performanțe groaznice când toată lumea de pe site (sau un număr disproporționat de utilizatori) a urmărit un singur radiodifuzor. Vestea bună: acest lucru nu se întâmplă niciodată în afara unei campanii de marketing. Am implementat un cluster separat pentru a gestiona acest scenariu, dar, de fapt, ne-am gândit că a pune în pericol experiența utilizatorului plătitor pentru un efort de marketing nu avea sens - de fapt, acesta nu a fost cu adevărat un scenariu real (deși ar fi fost frumos să ne ocupăm de orice imaginabil). caz).
Concluzie
Câteva statistici din rezultatul final: traficul zilnic pe cluster a fost de aproximativ 100.000 de utilizatori la vârf (încărcare de 60%), ~50.000 în medie. Am gestionat două clustere (HUN și US); fiecare dintre ei a manipulat aproximativ 40 de mașini pentru a împărți sarcina. Lățimea de bandă agregată a clusterelor a fost de aproximativ 50 Gbps, de la care au folosit aproximativ 10 Gbps la sarcină de vârf. În cele din urmă, am reușit să scot cu ușurință 10 Gbps/mașină; teoretic 1 , acest număr ar fi putut ajunge până la 30 Gbps/mașină, ceea ce se traduce prin aproximativ 300.000 de utilizatori care vizionează fluxuri simultan de pe un server.
Clusterul FMS existent conținea mai mult de 200 de mașini, care ar fi putut fi înlocuite cu cele 15 ale mele, dintre care doar 10 ar fi făcut o treabă reală. Acest lucru ne-a oferit aproximativ o îmbunătățire de 200/10 = 20x.
Probabil cea mai mare concluzie a mea din proiectul de streaming video Python a fost că nu ar trebui să mă las oprit de perspectiva de a fi nevoit să învăț un nou set de abilități. În special, Python, transcodarea și programarea orientată pe obiecte au fost toate conceptele cu care am avut o experiență foarte subprofesională înainte de a prelua acest proiect video multicast.
Asta și acea soluție proprie poate plăti mare.
1 Mai târziu, când am pus codul în producție, am avut probleme hardware, deoarece am folosit servere Intel sr2500 mai vechi, care nu puteau face față plăcilor Ethernet de 10 Gbit din cauza lățimii de bandă PCI reduse. În schimb, le-am folosit în legături Ethernet 1-4x1 Gbit (agregând performanța mai multor plăci de interfață de rețea într-un card virtual). În cele din urmă, am primit unele dintre cele mai noi Intel-uri sr2600 i7, care au servit 10 Gbps peste optică, fără nicio problemă de performanță. Toate calculele proiectate se referă la acest hardware.