Wie ich mit Python-Video-Streaming Pornos 20-mal effizienter gemacht habe
Veröffentlicht: 2022-03-11Einleitung
Pornos sind eine große Industrie. Es gibt nicht viele Websites im Internet, die mit dem Datenverkehr der größten Player mithalten können.
Und es ist schwierig, diesen immensen Verkehr zu jonglieren. Um die Sache noch schwieriger zu machen, besteht ein Großteil der Inhalte, die von Pornoseiten bereitgestellt werden, aus Live-Videostreams mit geringer Latenz und nicht aus einfachen statischen Videoinhalten. Aber bei all den damit verbundenen Herausforderungen habe ich selten etwas über die Python-Entwickler gelesen, die sich ihnen stellen. Also beschloss ich, über meine eigenen Erfahrungen im Job zu schreiben.
Was ist das Problem?
Vor ein paar Jahren arbeitete ich für die 26. (damals) meistbesuchte Website der Welt – nicht nur für die Pornoindustrie: die Welt.
Zu dieser Zeit bediente die Website Streaming-Anfragen für Pornovideos mit dem Real Time Messaging-Protokoll (RTMP). Genauer gesagt wurde eine von Adobe entwickelte Flash Media Server (FMS)-Lösung verwendet, um Benutzern Live-Streams bereitzustellen. Der grundlegende Ablauf war wie folgt:
- Der Benutzer fordert Zugriff auf einen Live-Stream an
- Der Server antwortet mit einer RTMP-Sitzung, die das gewünschte Filmmaterial abspielt
Aus mehreren Gründen war FMS keine gute Wahl für uns, angefangen bei den Kosten, die den Kauf von beidem beinhalteten:
- Windows-Lizenzen für jede Maschine, auf der wir FMS ausgeführt haben.
- ~4.000 US-Dollar FMS-spezifische Lizenzen, von denen wir aufgrund unserer Größe mehrere hundert (und täglich mehr) kaufen mussten.
All diese Gebühren begannen sich zu häufen. Abgesehen von den Kosten war FMS ein mangelhaftes Produkt, insbesondere in seiner Funktionalität (mehr dazu gleich). Also beschloss ich, FMS abzuschaffen und meinen eigenen Python-RTMP-Parser von Grund auf neu zu schreiben.
Am Ende ist es mir gelungen, unseren Service rund 20x effizienter zu machen.
Einstieg
Dabei gab es zwei Kernprobleme: Erstens waren RTMP und andere Adobe-Protokolle und -Formate nicht offen (dh öffentlich verfügbar), was die Arbeit mit ihnen erschwerte. Wie können Sie Dateien in einem Format, über das Sie nichts wissen, umkehren oder analysieren? Glücklicherweise gab es im öffentlichen Raum einige Umkehrversuche (nicht von Adobe, sondern von einer Gruppe namens OS Flash, die jetzt nicht mehr existiert), auf denen wir unsere Arbeit basierten.
Hinweis: Adobe veröffentlichte später „Spezifikationen“, die nicht mehr Informationen enthielten als die bereits in dem nicht von Adobe produzierten Reversing-Wiki und -Dokumenten. Ihre (Adobes) Spezifikationen waren von absurd niedriger Qualität und machten es nahezu unmöglich, ihre Bibliotheken tatsächlich zu verwenden. Darüber hinaus schien das Protokoll selbst manchmal absichtlich irreführend zu sein. Zum Beispiel:
- Sie verwendeten 29-Bit-Ganzzahlen.
- Sie schlossen überall Protokollheader mit Big-Endian-Formatierung ein – mit Ausnahme eines bestimmten (noch nicht markierten) Felds, das Little-Endian war.
- Beim Transport von 9k-Videoframes haben sie Daten auf Kosten der Rechenleistung auf weniger Platz gequetscht, was wenig bis gar keinen Sinn machte, weil sie Bits oder Bytes auf einmal zurückerhielten – unbedeutende Gewinne für eine solche Dateigröße.
Und zweitens: RTMP ist stark sitzungsorientiert, was es praktisch unmöglich machte, einen eingehenden Stream per Multicast zu übertragen. Wenn mehrere Benutzer denselben Livestream ansehen möchten, könnten wir ihnen im Idealfall einfach Verweise auf eine einzelne Sitzung zurückgeben, in der dieser Stream ausgestrahlt wird (dies wäre ein Multicast-Videostreaming). Aber mit RTMP mussten wir für jeden Benutzer, der Zugriff wünschte, eine völlig neue Instanz des Streams erstellen. Das war eine komplette Verschwendung.
Meine Multicast-Video-Streaming-Lösung
Vor diesem Hintergrund habe ich mich entschieden, den typischen Antwortstrom in FLV-„Tags“ umzupacken/zu parsen (wobei ein „Tag“ nur ein paar Video-, Audio- oder Metadaten sind). Diese FLV-Tags könnten problemlos innerhalb des RTMP übertragen werden.
Die Vorteile eines solchen Ansatzes:
- Wir mussten einen Stream nur einmal umpacken (das Umpacken war ein Albtraum, da es an Spezifikationen und oben beschriebenen Protokollfehlern fehlte).
- Wir konnten jeden Stream zwischen Clients mit sehr wenigen Problemen wiederverwenden, indem wir ihnen einfach einen FLV-Header zur Verfügung stellten, während ein interner Zeiger auf FLV-Tags (zusammen mit einer Art Offset, um anzugeben, wo sie sich im Stream befinden) den Zugriff ermöglichte der Inhalt.
Ich begann mit der Entwicklung in der Sprache, die ich damals am besten kannte: C. Mit der Zeit wurde diese Wahl umständlich; Also fing ich an, die Grundlagen von Python zu lernen, während ich meinen C-Code portierte. Der Entwicklungsprozess beschleunigte sich, aber nach einigen Demos stieß ich schnell auf das Problem der Ressourcenerschöpfung. Pythons Socket-Handling war nicht für diese Art von Situationen gedacht: Insbesondere in Python mussten wir mehrere Systemaufrufe und Kontextwechsel pro Aktion durchführen, was einen enormen Overhead verursachte.
Verbesserung der Video-Streaming-Leistung: Mischen von Python, RTMP und C
Nachdem ich den Code profiliert hatte, entschied ich mich, die leistungskritischen Funktionen in ein Python-Modul zu verschieben, das vollständig in C geschrieben war. Das war ziemlich einfacher Kram: Insbesondere nutzte es den Epoll-Mechanismus des Kernels, um eine logarithmische Wachstumsreihenfolge bereitzustellen .
Bei der asynchronen Socket-Programmierung gibt es Einrichtungen, die Ihnen Informationen darüber liefern können, ob ein bestimmter Socket lesbar/schreibbar/mit Fehlern gefüllt ist. In der Vergangenheit haben Entwickler den Systemaufruf select() verwendet, um diese Informationen zu erhalten, die sich schlecht skalieren lassen. Poll() ist eine bessere Version von select, aber es ist immer noch nicht so toll, da Sie bei jedem Aufruf eine Reihe von Socket-Deskriptoren übergeben müssen.

Epoll ist erstaunlich, da alles, was Sie tun müssen, ist, einen Socket zu registrieren, und das System wird sich an diesen bestimmten Socket erinnern und alle groben Details intern handhaben. Es gibt also bei jedem Aufruf keinen Overhead für die Übergabe von Argumenten. Es lässt sich auch viel besser skalieren und gibt nur die Sockets zurück, die Ihnen wichtig sind, was viel besser ist, als eine Liste von 100.000 Socket-Deskriptoren durchzugehen, um zu sehen, ob sie Ereignisse mit Bitmasken hatten – was Sie tun müssen, wenn Sie die anderen Lösungen verwenden.
Aber für die Leistungssteigerung haben wir einen Preis bezahlt: Dieser Ansatz folgte einem völlig anderen Designmuster als zuvor. Der vorherige Ansatz der Seite war (wenn ich mich richtig erinnere) ein monolithischer Prozess, der das Empfangen und Senden blockierte; Ich habe eine ereignisgesteuerte Lösung entwickelt, also musste ich auch den Rest des Codes umgestalten, um zu diesem neuen Modell zu passen.
Insbesondere hatten wir in unserem neuen Ansatz eine Hauptschleife, die das Empfangen und Senden wie folgt handhabte:
- Die empfangenen Daten wurden (als Nachrichten) bis zur RTMP-Schicht weitergeleitet.
- Das RTMP wurde seziert und FLV-Tags wurden extrahiert.
- Die FLV-Daten wurden an die Buffering- und Multicasting-Schicht gesendet, die die Streams organisierte und die Low-Level-Puffer des Senders füllte.
- Der Absender hat für jeden Client eine Struktur mit einem zuletzt gesendeten Index gespeichert und versucht, so viele Daten wie möglich an den Client zu senden.
Dies war ein rollierendes Datenfenster und enthielt einige Heuristiken, um Frames zu verwerfen, wenn der Client zu langsam zum Empfangen war. Die Dinge funktionierten ziemlich gut.
Probleme auf Systemebene, Architektur und Hardware
Aber wir stießen auf ein anderes Problem: Die Kontextwechsel des Kernels wurden zu einer Belastung. Aus diesem Grund haben wir uns entschieden, nur alle 100 Millisekunden zu schreiben, anstatt sofort. Dies aggregierte die kleineren Pakete und verhinderte einen Burst von Kontextwechseln.
Vielleicht lag ein größeres Problem im Bereich der Serverarchitekturen: Wir brauchten einen Load-Balancing- und Failover-fähigen Cluster – es macht keinen Spaß, Benutzer aufgrund von Serverfehlfunktionen zu verlieren. Zunächst entschieden wir uns für einen Ansatz mit separatem Regisseur, bei dem ein designierter „Regisseur“ versuchte, Sender-Feeds zu erstellen und zu zerstören, indem er die Nachfrage vorhersagte. Das ist spektakulär gescheitert. Tatsächlich schlug alles, was wir versuchten, ziemlich substanziell fehl. Am Ende haben wir uns für einen relativ brutalen Ansatz entschieden, bei dem Broadcaster zufällig auf die Knoten des Clusters verteilt werden, um den Datenverkehr auszugleichen.
Dies funktionierte, aber mit einem Nachteil: Obwohl der allgemeine Fall ziemlich gut gehandhabt wurde, sahen wir eine schreckliche Leistung, wenn alle auf der Website (oder eine unverhältnismäßige Anzahl von Benutzern) einen einzelnen Sender sahen. Die gute Nachricht: Außerhalb einer Marketingkampagne passiert das nie. Wir haben einen separaten Cluster implementiert, um dieses Szenario zu handhaben, aber in Wahrheit dachten wir, dass es sinnlos sei, die Erfahrung des zahlenden Benutzers für eine Marketingaktion zu gefährden – tatsächlich war dies nicht wirklich ein echtes Szenario (obwohl es schön gewesen wäre, alle erdenklichen zu handhaben Fall).
Fazit
Einige Statistiken aus dem Endergebnis: Der tägliche Datenverkehr auf dem Cluster betrug zu Spitzenzeiten (60 % Last) etwa 100.000 Benutzer, im Durchschnitt etwa 50.000. Ich habe zwei Cluster verwaltet (HUN und US); Jeder von ihnen handhabte ungefähr 40 Maschinen, um die Last zu teilen. Die aggregierte Bandbreite der Cluster lag bei etwa 50 Gbit/s, wovon sie bei Spitzenlast etwa 10 Gbit/s nutzten. Am Ende gelang es mir, 10 Gbit/s/Rechner problemlos herauszudrücken; theoretisch 1 hätte diese Zahl bis zu 30 Gbps/Rechner erreichen können, was bedeutet, dass etwa 300.000 Benutzer gleichzeitig Streams von einem Server ansehen.
Der vorhandene FMS-Cluster enthielt mehr als 200 Maschinen, die durch meine 15 hätten ersetzt werden können – von denen nur 10 wirklich funktionieren würden. Dies gab uns ungefähr eine 200/10 = 20-fache Verbesserung.
Meine wohl größte Erkenntnis aus dem Python-Video-Streaming-Projekt war, dass ich mich nicht von der Aussicht aufhalten lassen sollte, neue Fähigkeiten erlernen zu müssen. Insbesondere Python, Transkodierung und objektorientierte Programmierung waren alles Konzepte, mit denen ich sehr wenig Erfahrung hatte, bevor ich dieses Multicast-Videoprojekt übernahm.
Das und das Rollieren Ihrer eigenen Lösung kann sich auszahlen.
1 Später, als wir den Code in die Produktion überführten, stießen wir auf Hardwareprobleme, da wir ältere sr2500-Intel-Server verwendeten, die aufgrund ihrer geringen PCI-Bandbreite keine 10-Gbit-Ethernet-Karten verarbeiten konnten. Stattdessen haben wir sie in 1-4x1-Gbit-Ethernet-Verbindungen verwendet (Aggregierung der Leistung mehrerer Netzwerkschnittstellenkarten in einer virtuellen Karte). Schließlich bekamen wir einige der neueren sr2600 i7 Intels, die 10 Gbit/s über Optik ohne Leistungseinbußen lieferten. Alle projektierten Berechnungen beziehen sich auf diese Hardware.