Jak udało mi się 20 razy zwiększyć wydajność pornografii dzięki strumieniowaniu wideo w Pythonie?
Opublikowany: 2022-03-11Wprowadzenie
Pornografia to wielki przemysł. Nie ma wielu witryn w Internecie, które mogą konkurować z ruchem największych graczy.
A żonglowanie tym ogromnym ruchem jest trudne. Aby było jeszcze trudniej, większość treści dostarczanych z witryn pornograficznych składa się z strumieni wideo na żywo o niskim opóźnieniu, a nie z prostych, statycznych treści wideo. Ale pomimo wszystkich związanych z tym wyzwań, rzadko czytam o programistach Pythona, którzy je podejmują. Postanowiłem więc napisać o własnych doświadczeniach w pracy.
Jaki jest problem?
Kilka lat temu pracowałem dla 26. (wówczas) najczęściej odwiedzanej witryny na świecie — nie tylko dla branży porno: świata.
W tym czasie witryna obsługiwała żądania strumieniowego przesyłania filmów porno za pomocą protokołu Real Time Messaging (RTMP). Dokładniej, wykorzystał rozwiązanie Flash Media Server (FMS), zbudowane przez Adobe, aby zapewnić użytkownikom transmisje na żywo. Podstawowy proces wyglądał następująco:
- Użytkownik prosi o dostęp do transmisji na żywo
- Serwer odpowiada sesją RTMP odtwarzając żądany materiał filmowy
Z kilku powodów FMS nie był dla nas dobrym wyborem, zaczynając od kosztów, które obejmowały zakup obu:
- Licencje Windows na każdą maszynę, na której uruchomiliśmy FMS.
- ~4 000 $ licencji na FMS, z których musieliśmy kupować kilkaset (i więcej każdego dnia) ze względu na naszą skalę.
Wszystkie te opłaty zaczęły rosnąć. Pomijając koszty, FMS był produktem, którego brakowało, zwłaszcza pod względem funkcjonalności (więcej o tym za chwilę). Postanowiłem więc wyrzucić FMS i napisać od podstaw własny parser Pythona RTMP.
W końcu udało mi się sprawić, że nasza usługa będzie mniej więcej 20x bardziej wydajna.
Rozpoczęcie pracy
Wystąpiły dwa główne problemy: po pierwsze, RTMP i inne protokoły i formaty Adobe nie były otwarte (tj. publicznie dostępne), co utrudniało pracę z nimi. Jak możesz odwrócić lub przeanalizować pliki w formacie, o którym nic nie wiesz? Na szczęście w sferze publicznej pojawiły się pewne próby odwrócenia (nie wyprodukowane przez Adobe, ale przez nieistniejącą już grupę OS Flash), na których oparliśmy naszą pracę.
Uwaga: firma Adobe opublikowała później „specyfikacje”, które nie zawierały więcej informacji niż te, które zostały już ujawnione w wiki i dokumentach dotyczących odwracania, które nie zostały wyprodukowane przez firmę Adobe. Ich specyfikacje (Adobe) były absurdalnie niskiej jakości i praktycznie uniemożliwiały korzystanie z ich bibliotek. Co więcej, sam protokół wydawał się czasami celowo wprowadzać w błąd. Na przykład:
- Użyli 29-bitowych liczb całkowitych.
- Zawierały nagłówki protokołów z formatowaniem big endian wszędzie — z wyjątkiem określonego (jeszcze nieoznaczonego) pola, którym było little endian.
- Ściskali dane na mniejszej przestrzeni kosztem mocy obliczeniowej podczas transportu 9k klatek wideo, co nie miało sensu, ponieważ zarabiali bity lub bajty na raz – nieznaczne zyski jak na taki rozmiar pliku.
Po drugie: protokół RTMP jest wysoce zorientowany na sesje, co praktycznie uniemożliwiło multicastowanie strumienia przychodzącego. W idealnej sytuacji, gdyby wielu użytkowników chciało oglądać tę samą transmisję na żywo, moglibyśmy po prostu przekazać im wskaźniki do pojedynczej sesji, w której ten strumień jest emitowany (będzie to strumieniowe przesyłanie wideo w trybie multiemisji). Ale dzięki RTMP musieliśmy stworzyć zupełnie nową instancję strumienia dla każdego użytkownika, który chciał uzyskać dostęp. To była kompletna strata.
Moje rozwiązanie do przesyłania strumieniowego multiemisji wideo
Mając to na uwadze, postanowiłem przepakować/przeanalizować typowy strumień odpowiedzi do „tagów” FLV (gdzie „tag” to tylko niektóre dane wideo, audio lub meta). Te tagi FLV mogą podróżować w RTMP bez problemu.
Korzyści z takiego podejścia:
- Musieliśmy przepakować strumień tylko raz (przepakowywanie było koszmarem z powodu braku specyfikacji i dziwactw protokołu opisanych powyżej).
- Moglibyśmy ponownie użyć dowolnego strumienia między klientami z bardzo nielicznymi problemami, dostarczając im po prostu nagłówek FLV, podczas gdy wewnętrzny wskaźnik do tagów FLV (wraz z pewnym przesunięciem wskazującym, gdzie się znajdują w strumieniu) umożliwiał dostęp do Zawartość.
Zacząłem rozwijać się w języku, który wtedy znałem najlepiej: C. Z biegiem czasu wybór ten stał się kłopotliwy; więc zacząłem uczyć się podstaw Pythona podczas portowania mojego kodu C. Proces rozwoju przyspieszył, ale po kilku demach szybko natknąłem się na problem wyczerpywania się zasobów. Obsługa gniazd w Pythonie nie była przeznaczona do obsługi tego typu sytuacji: w szczególności w Pythonie musieliśmy wykonywać wiele wywołań systemowych i przełączników kontekstu na akcję, co powodowało ogromne narzuty.
Poprawa wydajności przesyłania strumieniowego wideo: mieszanie Pythona, RTMP i C
Po sprofilowaniu kodu zdecydowałem się przenieść funkcje krytyczne dla wydajności do modułu Pythona napisanego w całości w C. To było dość niskopoziomowe zadanie: w szczególności wykorzystano mechanizm epoll jądra, aby zapewnić logarytmiczny porządek wzrostu .
W asynchronicznym programowaniu gniazd istnieją udogodnienia, które mogą dostarczyć informacji, czy dane gniazdo jest możliwe do odczytu/zapisu/wypełnione błędami. W przeszłości programiści używali wywołania systemowego select(), aby uzyskać te informacje, które źle się skalują. Poll() jest lepszą wersją funkcji select, ale nadal nie jest tak wspaniała, ponieważ przy każdym wywołaniu trzeba przekazywać kilka deskryptorów gniazd.

Epoll jest niesamowity, ponieważ wszystko, co musisz zrobić, to zarejestrować gniazdo, a system zapamięta to odrębne gniazdo, obsługując wewnętrznie wszystkie drobiazgi. Więc nie ma narzutu na przekazywanie argumentów przy każdym wywołaniu. Skaluje się również znacznie lepiej i zwraca tylko te gniazda, na których Ci zależy, co jest o wiele lepsze niż przeglądanie listy deskryptorów gniazd 100k, aby sprawdzić, czy mają zdarzenia z maskami bitowymi — co musisz zrobić, jeśli używasz innych rozwiązań.
Ale za wzrost wydajności zapłaciliśmy cenę: to podejście opierało się na zupełnie innym wzorcu projektowym niż wcześniej. Poprzednie podejście witryny było (jeśli dobrze pamiętam) jeden monolityczny proces, który blokował odbieranie i wysyłanie; Opracowywałem rozwiązanie oparte na zdarzeniach, więc musiałem również zrefaktoryzować resztę kodu, aby pasowała do tego nowego modelu.
W szczególności w naszym nowym podejściu mieliśmy główną pętlę, która obsługiwała odbieranie i wysyłanie w następujący sposób:
- Otrzymane dane były przekazywane (jako komunikaty) do warstwy RTMP.
- RTMP wypreparowano i wyekstrahowano znaczniki FLV.
- Dane FLV zostały przesłane do warstwy buforowania i multiemisji, która uporządkowała strumienie i wypełniła bufory niskiego poziomu nadawcy.
- Nadawca utrzymywał strukturę dla każdego klienta z ostatnio wysłanym indeksem i próbował wysłać klientowi jak najwięcej danych.
To było ruchome okno danych i zawierało pewne mechanizmy heurystyczne, aby porzucić ramki, gdy klient był zbyt wolny, aby odebrać. Sprawy działały całkiem dobrze.
Kwestie systemowe, architektoniczne i sprzętowe
Natknęliśmy się jednak na inny problem: przełączniki kontekstu jądra stawały się obciążeniem. W rezultacie zdecydowaliśmy się pisać tylko co 100 milisekund, a nie natychmiast. Spowodowało to agregację mniejszych pakietów i zapobiegło serii zmian kontekstu.
Być może większy problem leżał w dziedzinie architektur serwerowych: potrzebowaliśmy klastra równoważącego obciążenie i zdolnego do przełączania awaryjnego — utrata użytkowników z powodu awarii serwera nie jest zabawna. Na początku przyjęliśmy podejście oddzielnego dyrektora, w którym wyznaczony „dyrektor” próbowałby tworzyć i niszczyć kanały nadawców, przewidując popyt. To nie powiodło się spektakularnie. W rzeczywistości wszystko, czego próbowaliśmy, zawiodło dość znacząco. Ostatecznie zdecydowaliśmy się na stosunkowo brutalne podejście polegające na losowym udostępnianiu nadawców między węzłami klastra, wyrównując ruch.
To zadziałało, ale z jedną wadą: chociaż ogólna sprawa została załatwiona całkiem dobrze, widzieliśmy straszną wydajność, gdy wszyscy w witrynie (lub nieproporcjonalna liczba użytkowników) oglądali jednego nadawcę. Dobra wiadomość: to się nigdy nie zdarza poza kampanią marketingową. Wdrożyliśmy osobny klaster, aby obsłużyć ten scenariusz, ale tak naprawdę uznaliśmy, że narażanie doświadczenia płacącego użytkownika w celu działań marketingowych jest bezsensowne — w rzeczywistości nie był to prawdziwy scenariusz (chociaż miło byłoby poradzić sobie z każdym możliwym do wyobrażenia walizka).
Wniosek
Niektóre statystyki z wyniku końcowego: Dzienny ruch w klastrze wynosił w szczycie około 100 tys. użytkowników (obciążenie 60%), średnio ~50 tys. Zarządzałem dwoma klastrami (HUN i US); każdy z nich obsługiwał około 40 maszyn do podziału obciążenia. Łączna przepustowość klastrów wynosiła około 50 Gb/s, z czego wykorzystywały około 10 Gb/s przy szczytowym obciążeniu. W końcu udało mi się z łatwością wypchnąć 10 Gb/s/maszynę; teoretycznie 1 , liczba ta mogłaby wzrosnąć do 30 Gb/s/maszynę, co przekłada się na około 300 tys. użytkowników jednocześnie oglądających strumienie z jednego serwera.
Istniejący klaster FMS zawierał ponad 200 maszyn, które można było zastąpić moimi 15 – z których tylko 10 wykonałoby jakąkolwiek prawdziwą pracę. To dało nam około 200/10 = 20-krotną poprawę.
Prawdopodobnie moim największym wnioskiem z projektu strumieniowego przesyłania wideo w Pythonie było to, że nie powinnam dać się powstrzymać perspektywą konieczności uczenia się nowego zestawu umiejętności. W szczególności Python, transkodowanie i programowanie obiektowe były koncepcjami, z którymi miałem bardzo subprofesjonalne doświadczenie przed podjęciem tego projektu wideo multicast.
To i to, że rozwijanie własnego rozwiązania może dużo zapłacić.
1 Później, gdy wprowadziliśmy kod do produkcji, napotkaliśmy problemy sprzętowe, ponieważ używaliśmy starszych serwerów Intel sr2500, które nie mogły obsługiwać kart Ethernet 10 Gbit z powodu ich niskiej przepustowości PCI. Zamiast tego użyliśmy ich w połączeniach 1-4x1 Gbit Ethernet (agregując wydajność kilku kart sieciowych w jedną kartę wirtualną). W końcu otrzymaliśmy niektóre z nowszych procesorów Intel sr2600 i7, które obsługiwały 10 Gb/s na optyce bez żadnych problemów z wydajnością. Wszystkie przewidywane obliczenia odnoszą się do tego sprzętu.